Skip to content

Commit 7c382d4

Browse files
Merge upstream main to fix out-of-date branch
2 parents e4e3e5f + e2ee35e commit 7c382d4

10 files changed

Lines changed: 102 additions & 14 deletions

File tree

doc/changes/dev/13915.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix iteration errors in :func:`mne.io.read_raw_nihon` when reading log files, by `Tom Ma`_.

doc/changes/dev/13920.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix :func:`mne.set_eeg_reference` so that passing a list of channel types with ``projection=False`` and ``ref_channels="average"`` now references each channel type independently by default (matching the ``projection=True`` path), by :newcontrib:`Ben Tang`. Previously a single average was computed across the union of the listed types. Pass ``joint=True`` for the previous behavior.

doc/changes/dev/13923.other.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Convert non-empty ``drop_log`` entries from ``numpy.str_`` to regular Python strings, by `Clemens Brunner`_.

doc/changes/names.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
.. _Baris Talar: https://github.com/baris-talar
4040
.. _Beige Jin: https://github.com/BeiGeJin
4141
.. _Ben Beasley: https://github.com/musicinmybrain
42+
.. _Ben Tang: https://github.com/bentang18
4243
.. _Benedikt Ehinger: https://www.benediktehinger.de
4344
.. _Bhargav Kowshik: https://github.com/bkowshik
4445
.. _Bradley Voytek: https://github.com/voytek

mne/_fiff/reference.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,10 @@ def set_eeg_reference(
402402
re-referencing the data.
403403
ref_data : array
404404
Array of reference data subtracted from EEG channels. This will be
405-
``None`` if ``projection=True``, or if ``ref_channels`` is ``"REST"`` or a
406-
:class:`dict`.
405+
``None`` if ``projection=True``, if ``ref_channels`` is ``"REST"`` or a
406+
:class:`dict`, or if a per-channel-type average reference was applied
407+
(``ref_channels="average"`` with a multi-type ``ch_type`` and
408+
``joint=False``).
407409
%(set_eeg_reference_see_also_notes)s
408410
"""
409411
from ..forward import Forward
@@ -465,14 +467,32 @@ def set_eeg_reference(
465467
del projection # not used anymore
466468

467469
inst = inst.copy() if copy else inst
468-
ch_dict = {**{type_: True for type_ in ch_type}, "meg": False, "ref_meg": False}
469-
ch_sel = [inst.ch_names[i] for i in pick_types(inst.info, **ch_dict)]
470470

471471
if ref_channels == "REST":
472472
_validate_type(forward, Forward, 'forward when ref_channels="REST"')
473473
else:
474474
forward = None # signal to _apply_reference not to do REST
475475

476+
# When ch_type is a list of >1 types and ref_channels="average", default to
477+
# computing one reference per channel type so each subset is referenced
478+
# independently. Mirrors the projection=True path. Pass joint=True to keep
479+
# the legacy union-of-types behavior.
480+
if ref_channels == "average" and len(ch_type) > 1 and not joint:
481+
for this_ch_type in ch_type:
482+
ch_dict = {this_ch_type: True}
483+
ch_sel = [inst.ch_names[i] for i in pick_types(inst.info, **ch_dict)]
484+
logger.info(
485+
f"Applying average reference for "
486+
f"{DEFAULTS['titles'][this_ch_type]} channels."
487+
)
488+
inst, _ = _apply_reference(
489+
inst, ch_sel, ch_sel, forward, ch_type=[this_ch_type]
490+
)
491+
return inst, None
492+
493+
ch_dict = {**{type_: True for type_ in ch_type}, "meg": False, "ref_meg": False}
494+
ch_sel = [inst.ch_names[i] for i in pick_types(inst.info, **ch_dict)]
495+
476496
if ref_channels in ("average", "REST"):
477497
logger.info(f"Applying {ref_channels} reference.")
478498
ref_channels = ch_sel

mne/_fiff/tests/test_reference.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,17 @@ def test_set_eeg_reference_ch_type(ch_type, msg, projection):
284284
else:
285285
ref_ch = raw.copy().pick(picks=ch_type).ch_names
286286

287+
# joint=True forces the union-of-types behavior so we can keep validating
288+
# ref_data against the joint mean; per-type default is covered by
289+
# test_set_eeg_reference_ch_type_list_per_type below.
290+
joint = isinstance(ch_type, list) and len(ch_type) > 1
287291
with catch_logging() as log:
288292
reref, ref_data = set_eeg_reference(
289-
raw.copy(), ch_type=ch_type, projection=projection, verbose=True
293+
raw.copy(),
294+
ch_type=ch_type,
295+
projection=projection,
296+
joint=joint,
297+
verbose=True,
290298
)
291299

292300
if not projection:
@@ -304,6 +312,47 @@ def test_set_eeg_reference_ch_type(ch_type, msg, projection):
304312
set_eeg_reference(raw2, ch_type="auto", projection=projection)
305313

306314

315+
@pytest.mark.parametrize("projection", [False, True])
316+
def test_set_eeg_reference_ch_type_list_per_type(projection):
317+
"""Test list ch_type defaults to one reference per channel type (gh-13913)."""
318+
# Build seeg at +10 uV and ecog at -10 uV (constant). Union mean is zero,
319+
# so a single union average reference would leave each subset's original
320+
# offset intact. A per-type CAR zeros both subsets.
321+
sfreq = 1000.0
322+
n = 500
323+
data = np.vstack([np.full((4, n), 10.0), np.full((4, n), -10.0)]) * 1e-6
324+
ch_names = [f"S{i}" for i in range(4)] + [f"E{i}" for i in range(4)]
325+
info = create_info(ch_names, sfreq, ["seeg"] * 4 + ["ecog"] * 4)
326+
raw = RawArray(data, info)
327+
328+
# Default joint=False: per-type reference. ref_data is None.
329+
reref, ref_data = set_eeg_reference(
330+
raw.copy(),
331+
ref_channels="average",
332+
ch_type=["seeg", "ecog"],
333+
projection=projection,
334+
)
335+
if projection:
336+
reref.apply_proj()
337+
assert ref_data is None
338+
assert_allclose(reref.get_data(picks="seeg").mean(), 0.0, atol=1e-15)
339+
assert_allclose(reref.get_data(picks="ecog").mean(), 0.0, atol=1e-15)
340+
341+
# joint=True: legacy union-of-types behavior. Union mean is zero, so
342+
# neither subset gets zeroed.
343+
reref_joint, _ = set_eeg_reference(
344+
raw.copy(),
345+
ref_channels="average",
346+
ch_type=["seeg", "ecog"],
347+
projection=projection,
348+
joint=True,
349+
)
350+
if projection:
351+
reref_joint.apply_proj()
352+
assert_allclose(reref_joint.get_data(picks="seeg").mean(), 10e-6, atol=1e-15)
353+
assert_allclose(reref_joint.get_data(picks="ecog").mean(), -10e-6, atol=1e-15)
354+
355+
307356
@testing.requires_testing_data
308357
def test_set_eeg_reference_rest():
309358
"""Test setting a REST reference."""

mne/io/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ def _check_bad_segment(
562562
)
563563
for descr in annot.description[overlaps]:
564564
if descr.lower().startswith("bad"):
565-
return descr
565+
return str(descr)
566566
return self._getitem((picks, slice(start, stop)), return_times=False)
567567

568568
@verbose

mne/io/nihon/nihon.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,21 +289,23 @@ def _read_nihon_header(fname):
289289

290290

291291
def _read_event_log_block(fid, t_block, version):
292+
empty_logs = np.empty(0, dtype="|S45")
293+
292294
fid.seek(0x92 + t_block * 20)
293295
data = np.fromfile(fid, np.uint32, 1)
294296
if data.size == 0 or data[0] == 0:
295-
return
297+
return empty_logs
296298
t_blk_address = data[0]
297299

298300
fid.seek(t_blk_address + 0x1)
299301
data = np.fromfile(fid, "|S16", 1).astype("U16")
300302
if data.size == 0 or data[0] != version:
301-
return
303+
warn(f"Event log version mismatch: expected '{version}', got '{data}'")
302304

303305
fid.seek(t_blk_address + 0x12)
304306
data = np.fromfile(fid, np.uint8, 1)
305307
if data.size == 0:
306-
return
308+
return empty_logs
307309
n_logs = data[0]
308310

309311
fid.seek(t_blk_address + 0x14)

mne/tests/test_epochs.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,9 @@ def _assert_drop_log_types(drop_log):
482482
assert all(isinstance(log, tuple) for log in drop_log), (
483483
"drop_log[ii] should be tuple"
484484
)
485-
assert all(isinstance(s, str) for log in drop_log for s in log), (
486-
"drop_log[ii][jj] should be str"
485+
# enforce exact built-in str (reject np.str_ and other str subclasses)
486+
assert all(type(s) is str for log in drop_log for s in log), (
487+
"drop_log[ii][jj] should be built-in str"
487488
)
488489

489490

@@ -775,6 +776,7 @@ def test_reject_by_annotations_reject_tmin_reject_tmax():
775776
epochs = mne.Epochs(
776777
raw, events, tmin=-1, tmax=1, preload=True, reject_by_annotation=True
777778
)
779+
_assert_drop_log_types(epochs.drop_log)
778780
assert len(epochs) == 0
779781

780782
# Setting `reject_tmin` to prevent rejection of epoch.
@@ -787,6 +789,7 @@ def test_reject_by_annotations_reject_tmin_reject_tmax():
787789
preload=True,
788790
reject_by_annotation=True,
789791
)
792+
_assert_drop_log_types(epochs.drop_log)
790793
assert len(epochs) == 1
791794

792795
# Same check but bad segment overlapping from 2.5s to 3s: use `reject_tmax`
@@ -800,6 +803,7 @@ def test_reject_by_annotations_reject_tmin_reject_tmax():
800803
preload=True,
801804
reject_by_annotation=True,
802805
)
806+
_assert_drop_log_types(epochs.drop_log)
803807
assert len(epochs) == 1
804808

805809

mne/utils/docs.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,10 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
717717
.. versionadded:: 0.19
718718
.. versionchanged:: 1.2
719719
``list-of-str`` is now supported with ``projection=True``.
720+
.. versionchanged:: 1.13
721+
``list-of-str`` with ``projection=False`` and ``ref_channels="average"``
722+
now applies a per-channel-type reference by default (set ``joint=True``
723+
for the previous union-of-types behavior).
720724
"""
721725

722726
_ch_type_topomap_base = """\
@@ -2310,11 +2314,16 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
23102314

23112315
docdict["joint_set_eeg_reference"] = """
23122316
joint : bool
2313-
How to handle list-of-str ``ch_type``. If False (default), one projector
2314-
is created per channel type. If True, one projector is created across
2315-
all channel types. This is only used when ``projection=True``.
2317+
How to handle list-of-str ``ch_type``. If False (default), the reference is
2318+
computed per channel type (one projector per type when ``projection=True``;
2319+
one average reference subtracted per type when ``projection=False`` and
2320+
``ref_channels="average"``). If True, a single reference is computed across
2321+
all listed channel types.
23162322
23172323
.. versionadded:: 1.2
2324+
.. versionchanged:: 1.13
2325+
Now also applies when ``projection=False``. Previously, the
2326+
``projection=False`` path silently behaved as if ``joint=True``.
23182327
"""
23192328

23202329
# %%

0 commit comments

Comments
 (0)