Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`_)
Expand Down
37 changes: 31 additions & 6 deletions moabb/analysis/timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,14 +542,15 @@ 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(
Expand All @@ -558,7 +559,31 @@ 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"))
# 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
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
Expand Down
7 changes: 6 additions & 1 deletion moabb/datasets/bnci/bnci_2014.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, "
Expand Down Expand Up @@ -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(
Expand Down
13 changes: 13 additions & 0 deletions moabb/tests/test_timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Loading