Skip to content

Commit 3998bc0

Browse files
scott-hubertyPragnyaKhandelwal
authored andcommitted
FIX:resolve merge conflicts
1 parent 32ad604 commit 3998bc0

4 files changed

Lines changed: 60 additions & 44 deletions

File tree

mne/io/egi/egi.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def read_raw_egi(
102102
channel_naming: str = "E%d",
103103
*,
104104
events_as_annotations: bool = True,
105+
event_key: str | None = None,
105106
verbose: bool | str | int | None = None,
106107
) -> "RawEGI":
107108
"""Read EGI simple binary as raw object.
@@ -142,6 +143,15 @@ def read_raw_egi(
142143
The default will change from False to True in version 1.9.
143144
144145
.. versionadded:: 1.8.0
146+
event_key : str | None
147+
The MFF event key whose value is appended to annotation descriptions and extras
148+
when ``events_as_annotations=True``, and to stim channel names when
149+
``events_as_annotations=False``. For example, ``event_key='cel#'`` will convert
150+
an MFF event code ``'stim'`` with ``cel#=1`` to ``'stim_1'``. These will also
151+
appear as ``'event_key_cel#': 1`` in annotations.extras
152+
Only supported for MFF files.
153+
154+
.. versionadded:: 1.11.0
145155
%(verbose)s
146156
147157
Returns
@@ -156,22 +166,31 @@ def read_raw_egi(
156166
157167
Notes
158168
-----
159-
When ``events_from_annotations=True``, event codes on stimulus channels like
169+
When ``events_as_annotations=True``, event codes on stimulus channels like
160170
``DIN1`` are stored as annotations with the ``description`` set to the stimulus
161171
channel name.
162172
163-
When ``events_from_annotations=False`` and events are present on the included
164-
stimulus channels, a new stim channel ``STI014`` will be synthesized from the
173+
When ``events_as_annotations=False`` and events are present on the included
174+
stimulus channels, a new stim channel ``STI 014`` will be synthesized from the
165175
events. It will contain 1-sample pulses where the Netstation file had event
166176
timestamps. A ``raw.event_id`` dictionary is added to the raw object that will have
167177
arbitrary sequential integer IDs for the events. This will fail if any timestamps
168178
are duplicated. The ``event_id`` will also not survive a save/load roundtrip.
169179
170180
For these reasons, it is recommended to use ``events_as_annotations=True``.
181+
182+
MFF event track XML files can store additional metadata in a
183+
``<keys>`` child element of each ``<event>`` element. This corresponds to
184+
the ECI Event Data Stream "Key List" described in the
185+
`EGI Amp Server Pro SDK User Guide
186+
<https://www.egi.com/images/stories/manuals/amp-server-pro-sdk-3-0-network-apis-user-guide-rev-01.pdf>`__.
187+
For example, E-Prime driven experiments sometimes store the experimental condition
188+
in a ``'cel#'`` event key.
171189
"""
172190
_validate_type(input_fname, "path-like", "input_fname")
173191
input_fname = str(input_fname)
174192
_validate_type(events_as_annotations, bool, "events_as_annotations")
193+
_validate_type(event_key, (str, None), "event_key")
175194

176195
if input_fname.rstrip("/\\").endswith(".mff"): # allows .mff or .mff/
177196
return _read_raw_egi_mff(
@@ -183,8 +202,10 @@ def read_raw_egi(
183202
preload,
184203
channel_naming,
185204
events_as_annotations=events_as_annotations,
205+
event_key=event_key,
186206
verbose=verbose,
187207
)
208+
188209
return RawEGI(
189210
input_fname,
190211
eog,

mne/io/egi/egimff.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ def _read_raw_egi_mff(
369369
channel_naming="E%d",
370370
*,
371371
events_as_annotations=True,
372+
event_key=None,
372373
verbose=None,
373374
):
374375
"""Read EGI mff binary as raw object."""
@@ -381,6 +382,7 @@ def _read_raw_egi_mff(
381382
preload,
382383
channel_naming,
383384
events_as_annotations=events_as_annotations,
385+
event_key=event_key,
384386
verbose=verbose,
385387
)
386388

@@ -402,6 +404,7 @@ def __init__(
402404
channel_naming="E%d",
403405
*,
404406
events_as_annotations=True,
407+
event_key=None,
405408
verbose=None,
406409
):
407410
"""Init the RawMff class."""
@@ -422,7 +425,9 @@ def __init__(
422425
misc = np.where(np.array(egi_info["chan_type"]) != "eeg")[0].tolist()
423426

424427
logger.info(" Reading events ...")
425-
egi_events, egi_info, mff_events = _read_events(input_fname, egi_info)
428+
egi_events, egi_info, mff_events = _read_events(
429+
input_fname, egi_info, event_key=event_key
430+
)
426431
cals = np.array([CAL_SCALES[t] for t in egi_info["chan_unit"]])
427432
logger.info(" Assembling measurement info ...")
428433
event_codes = egi_info["event_codes"]

mne/io/egi/events.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from ...utils import _soft_import, _validate_type, logger, warn
1111

1212

13-
def _read_events(input_fname, info):
13+
def _read_events(input_fname, info, *, event_key=None):
1414
"""Read events for the record.
1515
1616
Parameters
@@ -22,7 +22,7 @@ def _read_events(input_fname, info):
2222
"""
2323
n_samples = info["last_samps"][-1]
2424
mff_events, event_codes = _read_mff_events(
25-
input_fname, info["sfreq"], info["meas_dt_local"]
25+
input_fname, info["sfreq"], info["meas_dt_local"], event_key=event_key
2626
)
2727
info["n_events"] = len(event_codes)
2828
info["event_codes"] = event_codes
@@ -36,7 +36,7 @@ def _read_events(input_fname, info):
3636
return events, info, mff_events
3737

3838

39-
def _read_mff_events(filename, sfreq, start_time):
39+
def _read_mff_events(filename, sfreq, start_time, *, event_key=None):
4040
"""Extract the events with mffpy."""
4141
mffpy = _soft_import("mffpy", purpose="reading EGI MFF data", min_version="0.11")
4242
from mffpy.xml_files import XML
@@ -69,9 +69,10 @@ def _read_mff_events(filename, sfreq, start_time):
6969
for track in tracks:
7070
for event in track.events:
7171
code_str = event["code"]
72-
cel = event.get("keys", {}).get("cel#")
73-
if cel is not None:
74-
code_str = f"{code_str}_{int(cel)}"
72+
keys = event.get("keys", {})
73+
key_value = keys.get(event_key) if event_key is not None else None
74+
if key_value is not None:
75+
code_str = f"{code_str}_{key_value}"
7576
if code_str not in code:
7677
code.append(code_str)
7778

@@ -86,6 +87,14 @@ def _read_mff_events(filename, sfreq, start_time):
8687
)
8788
else:
8889
extras = {}
90+
extras.update(
91+
{
92+
f"event_key_{key}": value.item()
93+
if isinstance(value, np.generic)
94+
else value
95+
for key, value in keys.items()
96+
}
97+
)
8998

9099
markers.append(
91100
{

mne/io/egi/tests/test_egi.py

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -600,39 +600,20 @@ def test_egi_mff_bad_xml(tmp_path):
600600
# little check that the bad XML doesn't affect the parsing of other xml files
601601
assert "DIN1" in raw.annotations.description
602602

603-
604603
@requires_testing_data
605-
def test_egi_mff_cel_condition_codes():
606-
"""Test that cel# compound codes are only generated for repeated trial events.
607-
608-
EGI records a ``cel#`` key on each event to identify the experimental
609-
condition. When a code appears multiple times per condition (e.g. ``stm+``
610-
firing on every trial) ``read_raw_egi`` should append the cel# value and
611-
produce codes like ``stm+_1`` / ``stm+_2``.
612-
613-
However, some files use a once-per-condition-block ``CELL`` marker where
614-
each cel# value (1–5) fires exactly once. Splitting those into separate
615-
channels would create consecutive-sample trigger values that confuse
616-
``find_events`` and prevent ``STI 014`` from being synthesised. The reader
617-
must leave such codes unsplit.
618-
"""
619-
import mffpy
620-
621-
for fname in (egi_pause_fname, egi_eprime_pause_fname):
622-
r = mffpy.Reader(str(fname))
623-
_, codes = _read_mff_events(str(fname), 250.0, r.startdatetime)
624-
assert "CELL" in codes, f"CELL missing in {fname.name}"
625-
assert not any(c.startswith("CELL_") for c in codes), (
626-
f"CELL should not be split into per-condition channels in "
627-
f"{fname.name}; got {codes}"
628-
)
629-
630-
# Also verify end-to-end: STI 014 must be synthesised and CELL must appear
631-
# in raw.event_id (not split) when reading the full file.
632-
for fname in (egi_pause_fname, egi_eprime_pause_fname):
633-
raw = read_raw_egi(fname, events_as_annotations=False)
634-
assert "CELL" in raw.event_id, f"CELL missing from event_id in {fname.name}"
635-
assert not any(k.startswith("CELL_") for k in raw.event_id), (
636-
f"CELL should not be split in {fname.name}; got {list(raw.event_id)}"
604+
@pytest.mark.parametrize(
605+
"fname, expected",
606+
[pytest.param(egi_pause_fname, "AM40_3", id="paused")],
607+
)
608+
def test_read_event_keys(fname, expected):
609+
"""Test event metadata extraction from``<keys>`` child elements."""
610+
with pytest.warns(RuntimeWarning, match="Acquisition skips detected"):
611+
raw = _test_raw_reader(
612+
read_raw_egi,
613+
input_fname=fname,
614+
test_scaling=False,
615+
test_rank="less",
616+
events_as_annotations=True,
617+
event_key="cel#",
637618
)
638-
assert "STI 014" in raw.ch_names, f"STI 014 not synthesised in {fname.name}"
619+
assert expected in raw.annotations.description

0 commit comments

Comments
 (0)