Repository: mne-tools/mne-python

Issue:
`mne.io.read_raw_fil` crashes if bad channels are specified prior to import
### Description of the problem

The format used for the FIL OPM system uses a tsv file to specify channel names and statuses, so functionality to check for bad channels from the `*channels.tsv` metadata was implemented to automatically assign the bad channels on import without user intervention. Its taken me over a year to discover this part of `read_raw_fil` never worked. Sorry! 

### Steps to reproduce

```Python
import mne
import tempfile
import shutil
import os
import pandas as pd
import traceback

subject = "sub-002"
data_path = mne.datasets.ucl_opm_auditory.data_path()
opm_orig_path = (
data_path / subject / "ses-001" / "meg"
)

# Copy test data to temporary directory
temp_dir = tempfile.TemporaryDirectory()
base_dir_name = os.path.basename(opm_orig_path)
dest_dir = os.path.join(temp_dir.name, base_dir_name)
shutil.copytree(opm_orig_path, dest_dir)


opm_temp_data_path = os.path.join(dest_dir, 'sub-002_ses-001_task-aef_run-001_meg.bin')

# raw = mne.io.read_raw_fil(opm_temp_data_path)
# print('orignal attempt read in ok')

# read in channels tsv, set one to bad (G2-17-TAN) and write out
chans_tsv_path = os.path.join(dest_dir, 'sub-002_ses-001_task-aef_run-001_channels.tsv')

chans_df = pd.read_csv(chans_tsv_path, delimiter='\t')
index = chans_df[chans_df['name'] == 'G2-17-TAN'].index
chans_df.loc[index, 'status'] = 'bad'
chans_df.to_csv(chans_tsv_path, sep='\t', index=False)

try:
    raw = mne.io.read_raw_fil(opm_temp_data_path)
except Exception as e:
    traceback.print_tb(e.__traceback__)
    
temp_dir.cleanup()
```


### Link to data

_No response_

### Expected results

An instance of `raw` should be created with the bad channels listed in the edited  `*_channels.tsv`  (this case `G2-17-TAN`) being populated in `raw.info['bads']`.

### Actual results

Instance of `raw` not created, the following traceback below

Refining sensor orientations...
[done]
  File "c:\users\george o'neill\documents\python\bad_channel_bug_test.py", line 35, in <module>
    raw = mne.io.read_raw_fil(opm_temp_data_path)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-383>", line 12, in read_raw_fil
  File "C:\Users\goneill\miniconda3\envs\mne\Lib\site-packages\mne\io\fil\fil.py", line 51, in read_raw_fil
    return RawFIL(binfile, precision=precision, preload=preload)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\goneill\miniconda3\envs\mne\Lib\site-packages\mne\io\fil\fil.py", line 120, in __init__
    info = _compose_meas_info(meg, chans)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\goneill\miniconda3\envs\mne\Lib\site-packages\mne\io\fil\fil.py", line 274, in _compose_meas_info
    info["bads"] = _read_bad_channels(chans)
    ~~~~^^^^^^^^
  File "C:\Users\goneill\miniconda3\envs\mne\Lib\site-packages\mne\_fiff\meas_info.py", line 1579, in __setitem__
    val = self._attributes[key](
          ^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\goneill\miniconda3\envs\mne\Lib\site-packages\mne\_fiff\meas_info.py", line 992, in _check_bads
    return MNEBadsList(bads=bads, info=info)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\goneill\miniconda3\envs\mne\Lib\site-packages\mne\_fiff\meas_info.py", line 966, in __init__
    _check_bads_info_compat(bads, info)
  File "C:\Users\goneill\miniconda3\envs\mne\Lib\site-packages\mne\_fiff\meas_info.py", line 959, in _check_bads_info_compat
    raise ValueError(f"bad channel(s) {missing} marked do not exist in info")

### Additional information

It appears to be due to the fact the check for bad channels is specified before the `ch_names` has been populated. This probably can be fixed by calling `info._update_redundant()` before `_check_bads` is called.


Discussion:
Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴

Thanks for investigating, PR welcome if you're up for it @georgeoneill !
On it!

Resolving patch:
diff --git a/mne/io/fil/fil.py b/mne/io/fil/fil.py
--- a/mne/io/fil/fil.py
+++ b/mne/io/fil/fil.py
@@ -266,16 +266,14 @@ def _convert_channel_info(chans):
 def _compose_meas_info(meg, chans):
     """Create info structure."""
     info = _empty_info(meg["SamplingFrequency"])
-
     # Collect all the necessary data from the structures read
     info["meas_id"] = get_new_file_id()
     tmp = _convert_channel_info(chans)
     info["chs"] = _refine_sensor_orientation(tmp)
-    # info['chs'] = _convert_channel_info(chans)
     info["line_freq"] = meg["PowerLineFrequency"]
+    info._update_redundant()
     info["bads"] = _read_bad_channels(chans)
     info._unlocked = False
-    info._update_redundant()
     return info
 
 


Test patch:
diff --git a/mne/io/fil/tests/test_fil.py b/mne/io/fil/tests/test_fil.py
--- a/mne/io/fil/tests/test_fil.py
+++ b/mne/io/fil/tests/test_fil.py
@@ -25,6 +25,21 @@
 )
 
 
+def _set_bads_tsv(chanfile, badchan):
+    """Update channels.tsv by setting target channel to bad."""
+    data = []
+    with open(chanfile, encoding="utf-8") as f:
+        for line in f:
+            columns = line.strip().split("\t")
+            data.append(columns)
+
+    with open(chanfile, "w", encoding="utf-8") as f:
+        for row in data:
+            if badchan in row:
+                row[-1] = "bad"
+            f.write("\t".join(row) + "\n")
+
+
 def unpack_mat(matin):
     """Extract relevant entries from unstructred readmat."""
     data = matin["data"]
@@ -159,3 +174,20 @@ def test_fil_no_positions(tmp_path):
     chs = raw.info["chs"]
     locs = array([ch["loc"][:] for ch in chs])
     assert isnan(locs).all()
+
+
+@testing.requires_testing_data
+def test_fil_bad_channel_spec(tmp_path):
+    """Test FIL reader when a bad channel is specified in channels.tsv."""
+    test_path = tmp_path / "FIL"
+    shutil.copytree(fil_path, test_path)
+
+    channame = test_path / "sub-noise_ses-001_task-noise220622_run-001_channels.tsv"
+    binname = test_path / "sub-noise_ses-001_task-noise220622_run-001_meg.bin"
+    bad_chan = "G2-OG-Y"
+
+    _set_bads_tsv(channame, bad_chan)
+
+    raw = read_raw_fil(binname)
+    bads = raw.info["bads"]
+    assert bad_chan in bads