From dd6e155fb47812139da33bb9226cc0e6e5792427 Mon Sep 17 00:00:00 2001 From: Scott Huberty <52462026+scott-huberty@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:59:35 -0700 Subject: [PATCH 1/3] FIX: Properly handle blink interpolation in the case of a cropped raw object (#12759) Co-authored-by: Sammi Chekroud Co-authored-by: Mathieu Scheltienne --- doc/changes/devel/12759.bugfix.rst | 1 + doc/changes/names.inc | 2 ++ .../eyetracking/_pupillometry.py | 10 +++++--- .../eyetracking/tests/test_pupillometry.py | 23 ++++++++++++------- 4 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 doc/changes/devel/12759.bugfix.rst diff --git a/doc/changes/devel/12759.bugfix.rst b/doc/changes/devel/12759.bugfix.rst new file mode 100644 index 00000000000..f2d59280fd0 --- /dev/null +++ b/doc/changes/devel/12759.bugfix.rst @@ -0,0 +1 @@ +Assure that blink times are handled correctly :func:`mne.preprocessing.eyetracking.interpolate_blinks`, even when the raw object is cropped by `Scott Huberty`_ and :newcontrib:`Sammi Chekroud`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index d54368e0607..34195e4ce47 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -627,3 +627,5 @@ .. _Zvi Baratz: https://github.com/ZviBaratz .. _Seyed Yahya Shirazi: https://neuromechanist.github.io + +.. _Sammi Chekroud: https://github.com/schekroud diff --git a/mne/preprocessing/eyetracking/_pupillometry.py b/mne/preprocessing/eyetracking/_pupillometry.py index 9b31997ff21..cb494be99c3 100644 --- a/mne/preprocessing/eyetracking/_pupillometry.py +++ b/mne/preprocessing/eyetracking/_pupillometry.py @@ -6,6 +6,7 @@ import numpy as np from ..._fiff.constants import FIFF +from ...annotations import _annotations_starts_stops from ...io import BaseRaw from ...utils import _check_preload, _validate_type, logger, warn @@ -88,12 +89,15 @@ def _interpolate_blinks(raw, buffer, blink_annots, interpolate_gaze): continue # Create an empty boolean mask mask = np.zeros_like(raw.times, dtype=bool) - for annot in blink_annots: + starts, ends = _annotations_starts_stops(raw, "BAD_blink") + starts = np.divide(starts, raw.info["sfreq"]) + ends = np.divide(ends, raw.info["sfreq"]) + for annot, start, end in zip(blink_annots, starts, ends): if "ch_names" not in annot or not annot["ch_names"]: msg = f"Blink annotation missing values for 'ch_names' key: {annot}" raise ValueError(msg) - start = annot["onset"] - pre_buffer - end = annot["onset"] + annot["duration"] + post_buffer + start -= pre_buffer + end += post_buffer if ch_info["ch_name"] not in annot["ch_names"]: continue # skip if the channel is not in the blink annotation # Update the mask for times within the current blink period diff --git a/mne/preprocessing/eyetracking/tests/test_pupillometry.py b/mne/preprocessing/eyetracking/tests/test_pupillometry.py index bda1cf09f75..f82d0a6a360 100644 --- a/mne/preprocessing/eyetracking/tests/test_pupillometry.py +++ b/mne/preprocessing/eyetracking/tests/test_pupillometry.py @@ -6,6 +6,7 @@ import pytest from mne import create_info +from mne.annotations import _annotations_starts_stops from mne.datasets.testing import data_path, requires_testing_data from mne.io import RawArray, read_raw_eyelink from mne.preprocessing.eyetracking import interpolate_blinks @@ -16,17 +17,20 @@ @requires_testing_data @pytest.mark.parametrize( - "buffer, match, cause_error, interpolate_gaze", + "buffer, match, cause_error, interpolate_gaze, crop", [ - (0.025, "BAD_blink", False, False), - (0.025, "BAD_blink", False, True), - ((0.025, 0.025), ["random_annot"], False, False), - (0.025, "BAD_blink", True, False), + (0.025, "BAD_blink", False, False, False), + (0.025, "BAD_blink", False, True, True), + ((0.025, 0.025), ["random_annot"], False, False, False), + (0.025, "BAD_blink", True, False, False), ], ) -def test_interpolate_blinks(buffer, match, cause_error, interpolate_gaze): +def test_interpolate_blinks(buffer, match, cause_error, interpolate_gaze, crop): """Test interpolating pupil data during blinks.""" raw = read_raw_eyelink(fname, create_annotations=["blinks"], find_overlaps=True) + if crop: + raw.crop(tmin=2) + assert raw.first_time == 2.0 # Create a dummy stim channel # this will hit a certain line in the interpolate_blinks function info = create_info(["STI"], raw.info["sfreq"], ["stim"]) @@ -35,8 +39,11 @@ def test_interpolate_blinks(buffer, match, cause_error, interpolate_gaze): raw.add_channels([stim_raw], force_update_info=True) # Get the indices of the first blink - first_blink_start = raw.annotations[0]["onset"] - first_blink_end = raw.annotations[0]["onset"] + raw.annotations[0]["duration"] + blink_starts, blink_ends = _annotations_starts_stops(raw, "BAD_blink") + blink_starts = np.divide(blink_starts, raw.info["sfreq"]) + blink_ends = np.divide(blink_ends, raw.info["sfreq"]) + first_blink_start = blink_starts[0] + first_blink_end = blink_ends[0] if match == ["random_annot"]: msg = "No annotations matching" with pytest.warns(RuntimeWarning, match=msg): From a362e5ac3b2965d9cf129b9461e2907747618c30 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Mon, 29 Jul 2024 15:32:12 -0500 Subject: [PATCH 2/3] fix first_samp matching in maxwell_filter_prepare_emptyroom (#12760) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- doc/changes/devel/12760.bugfix.rst | 1 + mne/preprocessing/maxwell.py | 3 ++- mne/preprocessing/tests/test_maxwell.py | 17 +++++++++++------ 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 doc/changes/devel/12760.bugfix.rst diff --git a/doc/changes/devel/12760.bugfix.rst b/doc/changes/devel/12760.bugfix.rst new file mode 100644 index 00000000000..3afe215c57e --- /dev/null +++ b/doc/changes/devel/12760.bugfix.rst @@ -0,0 +1 @@ +Fix bug in :func:`~mne.preprocessing.maxwell_filter_prepare_emptyroom` where a difference in sampling frequencies between data and emptyroom files was ignored, by `Daniel McCloy`_. \ No newline at end of file diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py index 1b2c7c459ef..36a58535b16 100644 --- a/mne/preprocessing/maxwell.py +++ b/mne/preprocessing/maxwell.py @@ -181,7 +181,8 @@ def maxwell_filter_prepare_emptyroom( # handle first_samp raw_er_prepared.annotations.onset += raw.first_time - raw_er_prepared.first_time - raw_er_prepared._cropped_samp = raw._cropped_samp + # don't copy _cropped_samp directly, as sfreqs may differ + raw_er_prepared._cropped_samp = raw_er_prepared.time_as_index(raw.first_time).item() # handle annotations if annotations != "keep": diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py index 8ee2c0d6ba1..97bc3ea0f96 100644 --- a/mne/preprocessing/tests/test_maxwell.py +++ b/mne/preprocessing/tests/test_maxwell.py @@ -1820,8 +1820,9 @@ def test_prepare_emptyroom_bads(bads): @pytest.mark.parametrize("set_annot_when", ("before", "after")) @pytest.mark.parametrize("raw_meas_date", ("orig", None)) @pytest.mark.parametrize("raw_er_meas_date", ("orig", None)) +@pytest.mark.parametrize("equal_sfreq", (False, True)) def test_prepare_emptyroom_annot_first_samp( - set_annot_when, raw_meas_date, raw_er_meas_date + set_annot_when, raw_meas_date, raw_er_meas_date, equal_sfreq ): """Test prepare_emptyroom.""" raw = read_raw_fif(raw_fname, allow_maxshield="yes", verbose=False) @@ -1861,12 +1862,15 @@ def test_prepare_emptyroom_annot_first_samp( assert set_annot_when == "after" meas_date = "from_raw" want_date = raw.info["meas_date"] + if not equal_sfreq: + with raw_er.info._unlock(): + raw_er.info["sfreq"] -= 100 raw_er_prepared = maxwell_filter_prepare_emptyroom( raw_er=raw_er, raw=raw, meas_date=meas_date, emit_warning=True ) assert raw_er.first_samp == raw_er_first_samp_orig assert raw_er_prepared.info["meas_date"] == want_date - assert raw_er_prepared.first_samp == raw.first_samp + assert raw_er_prepared.first_time == raw.first_time # Ensure (movement) annotations carry over regardless of whether they're # set before or after preparation @@ -1878,7 +1882,8 @@ def test_prepare_emptyroom_annot_first_samp( prop_bad = np.isnan(raw.get_data([0], reject_by_annotation="nan")).mean() assert 0.3 < prop_bad < 0.4 assert len(raw_er_prepared.annotations) == want_annot - prop_bad_er = np.isnan( - raw_er_prepared.get_data([0], reject_by_annotation="nan") - ).mean() - assert_allclose(prop_bad, prop_bad_er) + if equal_sfreq: + prop_bad_er = np.isnan( + raw_er_prepared.get_data([0], reject_by_annotation="nan") + ).mean() + assert_allclose(prop_bad, prop_bad_er) From 868746b427f8123dd0e6b4594a2bd18c7fa49331 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 19:31:58 -0400 Subject: [PATCH 3/3] [pre-commit.ci] pre-commit autoupdate (#12761) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 196b4b74f17..d3469025ae4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: # Ruff mne - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.4 + rev: v0.5.5 hooks: - id: ruff name: ruff lint mne