From 32eb8841cd255219f651526342d6d8393c6f7445 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 12:02:48 +0000 Subject: [PATCH 1/4] Initial plan From 572edce83f796a021cd7f2b67220c6778a7bfa3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 12:09:15 +0000 Subject: [PATCH 2/4] Fix BNCI2014_001 stimulus timeline Agent-Logs-Url: https://github.com/NeuroTechX/moabb/sessions/43729bfe-3d86-49d4-afa9-dac038b40e33 Co-authored-by: bruAristimunha <42702466+bruAristimunha@users.noreply.github.com> --- moabb/analysis/timeline.py | 31 +++++++++++++++++++++++++------ moabb/datasets/bnci/bnci_2014.py | 7 ++++++- moabb/tests/test_timeline.py | 13 +++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/moabb/analysis/timeline.py b/moabb/analysis/timeline.py index 567b81adf..62cf111d8 100644 --- a/moabb/analysis/timeline.py +++ b/moabb/analysis/timeline.py @@ -542,14 +542,13 @@ def _extract_mi_timeline(metadata, dataset: BaseDataset) -> StimulusTimeline | N ) ) - # Cue arrow - if cue_onset_s is not None: - if cue_dur_s is None: - cue_dur_s = (feedback_onset_s - cue_onset_s) if feedback_onset_s else 1.25 - phases.append(TimelinePhase("Cue", cue_onset_s, cue_dur_s, "cue", "arrow_left")) + if cue_onset_s is not None and cue_dur_s is None: + cue_dur_s = (feedback_onset_s - cue_onset_s) if feedback_onset_s else 1.25 # Motor imagery / feedback if feedback_onset_s is not None: + if cue_onset_s is not None and cue_dur_s is not None: + phases.append(TimelinePhase("Cue", cue_onset_s, cue_dur_s, "cue", "arrow_left")) if feedback_dur_s is None: feedback_dur_s = trial_dur_s - feedback_onset_s phases.append( @@ -558,7 +557,27 @@ def _extract_mi_timeline(metadata, dataset: BaseDataset) -> StimulusTimeline | N elif cue_onset_s is not None and cue_dur_s is not None: img_onset = cue_onset_s + cue_dur_s img_dur = imagery_dur_s or (trial_dur_s - img_onset) - phases.append(TimelinePhase("Motor Imagery", img_onset, img_dur, "imagery")) + if ( + imagery_dur_s is not None + and cue_onset_s + cue_dur_s + imagery_dur_s > trial_dur_s + and cue_onset_s + imagery_dur_s <= trial_dur_s + ): + phases.append( + TimelinePhase("Cue + MI", cue_onset_s, cue_dur_s, "cue", "arrow_left") + ) + remaining_img_dur = imagery_dur_s - cue_dur_s + if remaining_img_dur > 0: + phases.append( + TimelinePhase( + "Motor Imagery", + cue_onset_s + cue_dur_s, + remaining_img_dur, + "imagery", + ) + ) + else: + phases.append(TimelinePhase("Cue", cue_onset_s, cue_dur_s, "cue", "arrow_left")) + phases.append(TimelinePhase("Motor Imagery", img_onset, img_dur, "imagery")) else: # Simple: just event interval img_onset = fix_end diff --git a/moabb/datasets/bnci/bnci_2014.py b/moabb/datasets/bnci/bnci_2014.py index 37334af90..d0e6ce1e9 100644 --- a/moabb/datasets/bnci/bnci_2014.py +++ b/moabb/datasets/bnci/bnci_2014.py @@ -345,6 +345,11 @@ class BNCI2014_001(MNEBNCI): mode="offline", events={"left_hand": 1, "right_hand": 2, "foot": 3, "no_control": 0}, instructions="Subjects instructed to perform motor imagery during cued periods", + stimulus_presentation={ + "cross_onset": "0 s", + "arrow_cue": "2 s", + "trial_duration": "6 s", + }, hed_tags={ "left_hand": ( "(Sensory-event, Experimental-stimulus, Visual-presentation, " @@ -440,7 +445,7 @@ class BNCI2014_001(MNEBNCI): paradigm_specific=ParadigmSpecificMetadata( detected_paradigm="imagery", imagery_tasks=["left_hand", "right_hand", "foot"], - cue_duration_s=4.0, + cue_duration_s=1.25, imagery_duration_s=4.0, ), data_structure=DataStructureMetadata( diff --git a/moabb/tests/test_timeline.py b/moabb/tests/test_timeline.py index 131cba3b6..64bf62659 100644 --- a/moabb/tests/test_timeline.py +++ b/moabb/tests/test_timeline.py @@ -64,6 +64,19 @@ def test_p300_paradigm(self): result = extract_stimulus_timeline(ds) assert result.paradigm == "p300" + def test_bnci2014_001_stimulus_protocol(self): + from moabb.analysis.timeline import extract_stimulus_timeline + from moabb.datasets import BNCI2014_001 + + result = extract_stimulus_timeline(BNCI2014_001()) + + assert result.total_duration_s == 6.0 + assert [(p.label, p.onset_s, p.duration_s) for p in result.phases] == [ + ("Fixation +", 0.0, 2.0), + ("Cue + MI", 2.0, 1.25), + ("Motor Imagery", 3.25, 2.75), + ] + class TestPlotClassBalance(unittest.TestCase): """Tests for plot_class_balance.""" From 84b382304ed8dc8ac6bb2a4e359cb85b4d8ed87a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 12:11:17 +0000 Subject: [PATCH 3/4] Clarify MI cue overlap handling Agent-Logs-Url: https://github.com/NeuroTechX/moabb/sessions/43729bfe-3d86-49d4-afa9-dac038b40e33 Co-authored-by: bruAristimunha <42702466+bruAristimunha@users.noreply.github.com> --- moabb/analysis/timeline.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/moabb/analysis/timeline.py b/moabb/analysis/timeline.py index 62cf111d8..bdb83494b 100644 --- a/moabb/analysis/timeline.py +++ b/moabb/analysis/timeline.py @@ -557,6 +557,8 @@ def _extract_mi_timeline(metadata, dataset: BaseDataset) -> StimulusTimeline | N elif cue_onset_s is not None and cue_dur_s is not None: img_onset = cue_onset_s + cue_dur_s img_dur = imagery_dur_s or (trial_dur_s - img_onset) + # Some MI protocols start imagery at cue onset, with the cue covering + # only the first part of the imagery period. if ( imagery_dur_s is not None and cue_onset_s + cue_dur_s + imagery_dur_s > trial_dur_s From 7aca3e3ea35433034cf840e95e5484456e88b10b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 13:07:39 +0000 Subject: [PATCH 4/4] Address pre-commit and changelog feedback Agent-Logs-Url: https://github.com/NeuroTechX/moabb/sessions/67fa9242-9a37-408c-8bdf-efac12b8517c Co-authored-by: bruAristimunha <42702466+bruAristimunha@users.noreply.github.com> --- docs/source/whats_new.rst | 1 + moabb/analysis/timeline.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/source/whats_new.rst b/docs/source/whats_new.rst index 106d75e30..646f99ea8 100644 --- a/docs/source/whats_new.rst +++ b/docs/source/whats_new.rst @@ -46,6 +46,7 @@ Requirements Bugs ~~~~ +- Fix :class:`moabb.datasets.BNCI2014_001` stimulus protocol timing in the generated documentation figure to show 2 s fixation, 1.25 s cue, and motor imagery through t=6 s (by `Bruno Aristimunha`_) - Fix stim-marker placement in :class:`moabb.datasets.BCIComp2020WalkingERP` (Track 5): ``build_raw_from_epochs`` was called with ``onset_sample=0``, which placed the event at sample 0 of each trial — the start of the pre-stim baseline (t=-190 ms), not the actual stimulus onset. With ``interval=[-0.19, 0.8]``, the paradigm was therefore reading the leading zero buffer as "pre-stim data" and missing the last 180 ms of real post-stim data. Now the loader passes ``onset_sample=19`` so the marker lands on t=0 and the interval picks the real 100-sample epoch as published (by `Bruno Aristimunha`_) - Fix session key off-by-one in :class:`moabb.datasets.Lee2019` that caused silent data loss when filtering sessions, and improve session filtering in :class:`moabb.datasets.base.BaseDataset` to match compound session keys (e.g., ``"0train"``) by integer prefix (:gh:`1046` by `Benedetto Leto`_ and `Bruno Aristimunha`_). - Fix BIDS conversion failures across multiple datasets: crop BDF/EDF signals to exact data records in ``bids_interface``, add standard montage fiducials when missing, fix :class:`moabb.datasets.BNCI2016_002` ``KeyError`` in event mapping, handle lowercase ``trigger`` attribute in :class:`moabb.datasets.BNCI2022_001` ``.mat`` files, detect and re-download truncated files in :class:`moabb.datasets.Kaneshiro2015`, add stim-channel annotations in :class:`moabb.datasets.Lee2024` for BIDS compatibility, convert µV to V in :class:`moabb.datasets.MartinezCagigal2023Checker` and :class:`moabb.datasets.MartinezCagigal2023Pary` to fix BDF physical range overflow, and handle alternate ``data`` key in :class:`moabb.datasets.Zuo2025` ``.mat`` files (by `Bruno Aristimunha`_) diff --git a/moabb/analysis/timeline.py b/moabb/analysis/timeline.py index bdb83494b..e873c1113 100644 --- a/moabb/analysis/timeline.py +++ b/moabb/analysis/timeline.py @@ -548,7 +548,9 @@ def _extract_mi_timeline(metadata, dataset: BaseDataset) -> StimulusTimeline | N # Motor imagery / feedback if feedback_onset_s is not None: if cue_onset_s is not None and cue_dur_s is not None: - phases.append(TimelinePhase("Cue", cue_onset_s, cue_dur_s, "cue", "arrow_left")) + phases.append( + TimelinePhase("Cue", cue_onset_s, cue_dur_s, "cue", "arrow_left") + ) if feedback_dur_s is None: feedback_dur_s = trial_dur_s - feedback_onset_s phases.append( @@ -578,7 +580,9 @@ def _extract_mi_timeline(metadata, dataset: BaseDataset) -> StimulusTimeline | N ) ) else: - phases.append(TimelinePhase("Cue", cue_onset_s, cue_dur_s, "cue", "arrow_left")) + phases.append( + TimelinePhase("Cue", cue_onset_s, cue_dur_s, "cue", "arrow_left") + ) phases.append(TimelinePhase("Motor Imagery", img_onset, img_dur, "imagery")) else: # Simple: just event interval