Repository: mne-tools/mne-python

Issue:
mne.io.read_raw_fil should support datasets without sensor positions
### Describe the new feature or enhancement

It would be great to generalise mne.io.read_raw_fil so that it would read in data which didn't have channel positions, or the documentation should reflect that the positions.tsv file is required to use this method.

### Describe your proposed implementation

The existing method can be used, but should have a case to handle an instance of this file missing.

### Describe possible alternatives

n/a

### Additional context

_No response_


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

@georgeoneill 
Maybe a new `on_pos_missing='raise'` would be good. Then set the loc to nan if no positions provided
This should be pretty easy to implement. The code already assumes there is [no position information for any sensors](https://github.com/mne-tools/mne-python/blob/4ce984f4458652551e2997da3749c2cbdb92a64e/mne/io/fil/fil.py#LL102C39-L102C39). For some reason I forgot to get to check if the position files exists in the current iteration of the code, and populate positions based on that.
Thanks for looking into this

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
@@ -90,9 +90,7 @@ def __init__(self, binfile, precision="single", preload=False):
         files = _get_file_names(binfile)
 
         chans = _from_tsv(files["chans"])
-        chanpos = _from_tsv(files["positions"])
         nchans = len(chans["name"])
-        nlocs = len(chanpos["name"])
         nsamples = _determine_nsamples(files["bin"], nchans, precision) - 1
         sample_info["nsamples"] = nsamples
 
@@ -101,13 +99,21 @@ def __init__(self, binfile, precision="single", preload=False):
 
         chans["pos"] = [None] * nchans
         chans["ori"] = [None] * nchans
-
-        for ii in range(0, nlocs):
-            idx = chans["name"].index(chanpos["name"][ii])
-            tmp = np.array([chanpos["Px"][ii], chanpos["Py"][ii], chanpos["Pz"][ii]])
-            chans["pos"][idx] = tmp.astype(np.float64)
-            tmp = np.array([chanpos["Ox"][ii], chanpos["Oy"][ii], chanpos["Oz"][ii]])
-            chans["ori"][idx] = tmp.astype(np.float64)
+        if files["positions"].is_file():
+            chanpos = _from_tsv(files["positions"])
+            nlocs = len(chanpos["name"])
+            for ii in range(0, nlocs):
+                idx = chans["name"].index(chanpos["name"][ii])
+                tmp = np.array(
+                    [chanpos["Px"][ii], chanpos["Py"][ii], chanpos["Pz"][ii]]
+                )
+                chans["pos"][idx] = tmp.astype(np.float64)
+                tmp = np.array(
+                    [chanpos["Ox"][ii], chanpos["Oy"][ii], chanpos["Oz"][ii]]
+                )
+                chans["ori"][idx] = tmp.astype(np.float64)
+        else:
+            warn("No sensor position information found.")
 
         with open(files["meg"], "r") as fid:
             meg = json.load(fid)
@@ -180,7 +186,8 @@ def _convert_channel_info(chans):
     """Convert the imported _channels.tsv into the chs element of raw.info."""
     nmeg = nstim = nmisc = nref = 0
 
-    units, sf = _get_pos_units(chans["pos"])
+    if not all(p is None for p in chans["pos"]):
+        _, sf = _get_pos_units(chans["pos"])
 
     chs = list()
     for ii in range(len(chans["name"])):


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
@@ -2,10 +2,13 @@
 #
 # License: BSD-3-Clause
 
-from numpy import isnan, empty
+from numpy import isnan, empty, array
 from numpy.testing import assert_array_equal, assert_array_almost_equal
+from os import remove
 
 import pytest
+import shutil
+
 
 from mne.datasets import testing
 from mne.io import read_raw_fil
@@ -127,7 +130,7 @@ def _fil_sensorpos(raw_test, raw_mat):
 
 
 @testing.requires_testing_data
-def test_fil_all():
+def test_fil_complete():
     """Test FIL reader, match to known answers from .mat file."""
     binname = fil_path / "sub-noise_ses-001_task-noise220622_run-001_meg.bin"
     matname = fil_path / "sub-noise_ses-001_task-noise220622_run-001_fieldtrip.mat"
@@ -140,3 +143,21 @@ def test_fil_all():
     _fil_megmag(raw, mat)
     _fil_stim(raw, mat)
     _fil_sensorpos(raw, mat)
+
+
+@testing.requires_testing_data
+def test_fil_no_positions(tmp_path):
+    """Test FIL reader in cases where a position file is missing."""
+    test_path = tmp_path / "FIL"
+    shutil.copytree(fil_path, test_path)
+
+    posname = test_path / "sub-noise_ses-001_task-noise220622_run-001_positions.tsv"
+    binname = test_path / "sub-noise_ses-001_task-noise220622_run-001_meg.bin"
+
+    remove(posname)
+
+    with pytest.warns(RuntimeWarning, match="No sensor position.*"):
+        raw = read_raw_fil(binname)
+    chs = raw.info["chs"]
+    locs = array([ch["loc"][:] for ch in chs])
+    assert isnan(locs).all()