Repository: mne-tools/mne-python

Issue:
`read_raw_snirf` throws `IndexError` when custom SNIRF file has DateOfBirth metadata tag
### Description of the problem

Hi,

I'm trying to produce SNIRF files that can be read by MNE, using the `PySnirf2` package. It seems that MNE has a particular dislike against the `DateOfBirth` metadata tag, throwing an exception when I try to import a file with it added (see code for minimal SNIRF sample below). I tried valid dates and gibberish - the result is the same, as long as the tag exists. Other custom string tags work just fine.

Of course, there's a chance that `PySnirf2` does something special to that particular field, but I cannot see anything special about it when I check the contents, and I'm unsure how to debug further.

### Steps to reproduce

```Python
1. Use code below in _Link to Data_ to create minimal SNIRF file. Requires PySnirf2 package.

2.

import mne
mne.io.read_raw_snirf("test.snirf", verbose=True)
```


### Link to data

```Python
import snirf
import numpy as np
from datetime import datetime

a = snirf.Snirf("test.snirf", "w")
a.nirs.appendGroup()
a.nirs[0].data.appendGroup()
a.nirs[0].data[0].dataTimeSeries = np.zeros((2, 2))
a.nirs[0].data[0].time = [0, 1]
for i in range(2):
    a.nirs[0].data[0].measurementList.appendGroup()
    a.nirs[0].data[0].measurementList[i].sourceIndex = 1
    a.nirs[0].data[0].measurementList[i].detectorIndex = 1
    a.nirs[0].data[0].measurementList[i].wavelengthIndex = 1
    a.nirs[0].data[0].measurementList[i].dataType = 99999
    a.nirs[0].data[0].measurementList[i].dataTypeIndex = 0
a.nirs[0].data[0].measurementList[0].dataTypeLabel = "HbO"
a.nirs[0].data[0].measurementList[1].dataTypeLabel = "HbR"

a.nirs[0].metaDataTags.SubjectID = "0"
a.nirs[0].metaDataTags.MeasurementDate = "1950-01-01"
a.nirs[0].metaDataTags.MeasurementTime = "00:00"
a.nirs[0].metaDataTags.LengthUnit = "m"
a.nirs[0].metaDataTags.TimeUnit = "s"
a.nirs[0].metaDataTags.FrequencyUnit = "Hz"

a.nirs[0].metaDataTags.add("DateOfBirth", "1950-01-01") # ERROR
# field content can be invalid date, or any string as far as I can tell

a.nirs[0].probe.wavelengths = [0, 0]
a.nirs[0].probe.sourcePos3D = np.zeros((1, 3))
a.nirs[0].probe.detectorPos3D = np.zeros((1, 3))

a.save()
a.close()
```

### Expected results

no output = file imported without errors

### Actual results

```
Loading C:\test\test.snirf
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\d\Virtualenvs\f\Lib\site-packages\mne\io\snirf\_snirf.py", line 54, in read_raw_snirf
    return RawSNIRF(fname, optode_frame, preload, verbose)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-318>", line 10, in __init__
  File "C:\Users\d\Virtualenvs\f\Lib\site-packages\mne\io\snirf\_snirf.py", line 441, in __init__
    str_birth = np.array((dat.get("/nirs/metaDataTags/" "DateOfBirth")))[
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
IndexError: too many indices for array: array is 0-dimensional, but 1 were indexed
```

### Additional information

```
Platform             Windows-10-10.0.22621-SP0
Python               3.11.4 (tags/v3.11.4:d2340ef, Jun  7 2023, 05:45:37) [MSC v.1934 64 bit (AMD64)]
Executable           C:\Users\d\Virtualenvs\f\Scripts\python.exe
CPU                  AMD64 Family 25 Model 68 Stepping 1, AuthenticAMD (16 cores)
Memory               30.8 GB

Core
├☑ mne               1.4.2
├☑ numpy             1.24.3 (OpenBLAS 0.3.21 with 16 threads)
├☑ scipy             1.10.1
├☑ matplotlib        3.7.1 (backend=QtAgg)
├☑ pooch             1.7.0
└☑ jinja2            3.1.2

Numerical (optional)
├☑ sklearn           1.2.2
├☑ numba             0.57.0
├☑ nibabel           5.1.0
├☑ nilearn           0.10.1
├☑ dipy              1.7.0
C:\Users\d\Virtualenvs\f\Lib\site-packages\cupy\_environment.py:214: UserWarning: CUDA path could not be detected. Set CUDA_PATH environment variable if CuPy fails to load.
  warnings.warn(
├☑ pandas            2.0.2
└☐ unavailable       openmeeg, cupy

Visualization (optional)
├☑ pyvista           0.39.1 (OpenGL 4.6.0 Compatibility Profile Context 22.20.44.50.230329 via AMD Radeon(TM) Graphics)
├☑ pyvistaqt         0.10.0
├☑ vtk               9.2.6
├☑ qtpy              2.3.1 (PySide6=6.5.1)
├☑ pyqtgraph         0.13.3
├☑ mne-qt-browser    0.5.1
└☐ unavailable       ipyvtklink, ipympl

Ecosystem (optional)
├☑ mne-nirs          0.5.0
└☐ unavailable       mne-bids, mne-features, mne-connectivity, mne-icalabel, mne-bids-pipeline
```


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


Resolving patch:
diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py
--- a/mne/io/snirf/_snirf.py
+++ b/mne/io/snirf/_snirf.py
@@ -102,7 +102,7 @@ def __init__(self, fname, optode_frame="unknown", preload=False, verbose=None):
                 optode_frame = "head"
 
             snirf_data_type = np.array(
-                dat.get("nirs/data1/measurementList1" "/dataType")
+                dat.get("nirs/data1/measurementList1/dataType")
             ).item()
             if snirf_data_type not in [1, 99999]:
                 # 1 = Continuous Wave
@@ -443,9 +443,9 @@ def natural_keys(text):
                 info["meas_date"] = meas_date
 
             if "DateOfBirth" in dat.get("nirs/metaDataTags/"):
-                str_birth = np.array((dat.get("/nirs/metaDataTags/" "DateOfBirth")))[
-                    0
-                ].decode()
+                str_birth = (
+                    np.array(dat.get("/nirs/metaDataTags/DateOfBirth")).item().decode()
+                )
                 birth_matched = re.fullmatch(r"(\d+)-(\d+)-(\d+)", str_birth)
                 if birth_matched is not None:
                     birthday = (


Test patch:
diff --git a/mne/io/snirf/tests/test_snirf.py b/mne/io/snirf/tests/test_snirf.py
--- a/mne/io/snirf/tests/test_snirf.py
+++ b/mne/io/snirf/tests/test_snirf.py
@@ -467,3 +467,39 @@ def test_annotation_duration_from_stim_groups():
     # a = Snirf(snirf_nirsport2_20219, "r+"); print(a.nirs[0].stim[0].data)
     expected_durations = np.full((10,), 10.0)
     assert_equal(expected_durations, raw.annotations.duration)
+
+
+def test_birthday(tmp_path, monkeypatch):
+    """Test birthday parsing."""
+    snirf = pytest.importorskip("snirf")
+    fname = tmp_path / "test.snirf"
+    with snirf.Snirf(str(fname), "w") as a:
+        a.nirs.appendGroup()
+        a.nirs[0].data.appendGroup()
+        a.nirs[0].data[0].dataTimeSeries = np.zeros((2, 2))
+        a.nirs[0].data[0].time = [0, 1]
+        for i in range(2):
+            a.nirs[0].data[0].measurementList.appendGroup()
+            a.nirs[0].data[0].measurementList[i].sourceIndex = 1
+            a.nirs[0].data[0].measurementList[i].detectorIndex = 1
+            a.nirs[0].data[0].measurementList[i].wavelengthIndex = 1
+            a.nirs[0].data[0].measurementList[i].dataType = 99999
+            a.nirs[0].data[0].measurementList[i].dataTypeIndex = 0
+        a.nirs[0].data[0].measurementList[0].dataTypeLabel = "HbO"
+        a.nirs[0].data[0].measurementList[1].dataTypeLabel = "HbR"
+        a.nirs[0].metaDataTags.SubjectID = "0"
+        a.nirs[0].metaDataTags.MeasurementDate = "2000-01-01"
+        a.nirs[0].metaDataTags.MeasurementTime = "00:00:00"
+        a.nirs[0].metaDataTags.LengthUnit = "m"
+        a.nirs[0].metaDataTags.TimeUnit = "s"
+        a.nirs[0].metaDataTags.FrequencyUnit = "Hz"
+        a.nirs[0].metaDataTags.add("DateOfBirth", "1950-01-01")
+        a.nirs[0].probe.wavelengths = [0, 0]
+        a.nirs[0].probe.sourcePos3D = np.zeros((1, 3))
+        a.nirs[0].probe.detectorPos3D = np.zeros((1, 3))
+        # Until https://github.com/BUNPC/pysnirf2/pull/39 is released
+        monkeypatch.setattr(a._cfg.logger, "info", lambda *args, **kwargs: None)
+        a.save()
+
+    raw = read_raw_snirf(fname)
+    assert raw.info["subject_info"]["birthday"] == (1950, 1, 1)
diff --git a/mne/tests/test_bem.py b/mne/tests/test_bem.py
--- a/mne/tests/test_bem.py
+++ b/mne/tests/test_bem.py
@@ -95,7 +95,7 @@ def _compare_bem_solutions(sol_a, sol_b):
         )
 
 
-h5py_mark = pytest.mark.skipif(not check_version("h5py"), reason="Needs h5py")
+h5io_mark = pytest.mark.skipif(not check_version("h5io"), reason="Needs h5io")
 
 
 @testing.requires_testing_data
@@ -103,7 +103,7 @@ def _compare_bem_solutions(sol_a, sol_b):
     "ext",
     [
         "fif",
-        pytest.param("h5", marks=h5py_mark),
+        pytest.param("h5", marks=h5io_mark),
     ],
 )
 def test_io_bem(tmp_path, ext):
diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py
--- a/mne/time_frequency/tests/test_spectrum.py
+++ b/mne/time_frequency/tests/test_spectrum.py
@@ -131,7 +131,7 @@ def _get_inst(inst, request, evoked):
 @pytest.mark.parametrize("inst", ("raw", "epochs", "evoked"))
 def test_spectrum_io(inst, tmp_path, request, evoked):
     """Test save/load of spectrum objects."""
-    pytest.importorskip("h5py")
+    pytest.importorskip("h5io")
     fname = tmp_path / f"{inst}-spectrum.h5"
     inst = _get_inst(inst, request, evoked)
     orig = inst.compute_psd()
diff --git a/requirements_testing_extra.txt b/requirements_testing_extra.txt
--- a/requirements_testing_extra.txt
+++ b/requirements_testing_extra.txt
@@ -7,3 +7,4 @@ EDFlib-Python
 pybv
 imageio>=2.6.1
 imageio-ffmpeg>=0.4.1
+pysnirf2