-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BUG] Correct annotation onset for exportation to EDF and EEGLAB #12656
base: main
Are you sure you want to change the base?
Conversation
for more information, see https://pre-commit.ci
mne/export/tests/test_export.py
Outdated
@pytest.mark.parametrize("tmin", (0, 1, 5, 10)) | ||
def test_export_raw_eeglab_annotations(tmp_path, tmin): | ||
"""Test that exporting EEGLAB preserves annotations and corects for raw.first_time.""" | ||
pytest.importorskip("eeglabio") | ||
raw = read_raw_fif(fname_raw, preload=True) | ||
raw.apply_proj() | ||
annotations = Annotations( | ||
onset=[0.01, 0.05, 0.90, 1.05], | ||
duration=[0, 1, 0, 0], | ||
description=["test1", "test2", "test3", "test4"], | ||
ch_names=[["MEG 0113"], ["MEG 0113", "MEG 0132"], [], ["MEG 0143"]], | ||
) | ||
raw.set_annotations(annotations) | ||
raw.crop(tmin) | ||
|
||
# export | ||
temp_fname = tmp_path / "test.set" | ||
raw.export(temp_fname) | ||
|
||
# read in the file | ||
with pytest.warns(RuntimeWarning, match="is above the 99th percentile"): | ||
raw_read = read_raw_eeglab(temp_fname, preload=True, montage_units="m") | ||
assert raw_read.first_time == 0 | ||
|
||
valid_annot = raw.annotations.onset >= tmin | ||
assert_array_almost_equal( | ||
raw.annotations.onset[valid_annot] - raw.first_time, | ||
raw_read.annotations.onset - raw_read.first_time, | ||
) | ||
assert_array_equal( | ||
raw.annotations.duration[valid_annot], raw_read.annotations.duration | ||
) | ||
assert_array_equal( | ||
raw.annotations.description[valid_annot], raw_read.annotations.description | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually should this annotation test for EEGLAB been written before, the bug should be noticable because the test Raw has actually been cropped. The saved onset would not correspond to the original onset.
if tmin % 1 == 0: | ||
expectation = nullcontext() | ||
else: | ||
expectation = pytest.warns( | ||
RuntimeWarning, match="EDF format requires equal-length data blocks" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this check doing? If you are checking for tmin
to be an integer, you could also use tmin.is_integer()
, but is this what is required to have "equal-length data blocks"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the constructed raw signal is 2 sec long and edfio can segment it into 2 data records of 1 sec. If a non-integer amount of time is cropped, then the signal is no longer a multiple of 1 sec and edfio will append zeroes and issue a RuntimeWarning. Maybe this should have been a test on its own but I'm adding it here since pytest wouldn't pass me otherwise.
As for the %1 == 0
condition, I was thinking to make space for more flexible use should I know how edfio determines data record length. For example if one can specify a data record length of .5 or 2 s, then the statement can be replaced with %data_length == 0
. But I agree it looks uncessary in its current form.
Hmmm, really? Annotations are not affected by cropping? Why would that be convenient? |
At least when Lines 1560 to 1570 in e2c8010
So that when annotations have their own time reference, cropping the data wouldn't affect them. Actually this is a good reminder that we might need to account for different |
To be honest, I didn't even know that annotations work like that. I always thought that |
I'm not too sure how that will work out. Do you mean that cropping should always reset
|
Maybe it's just me, but I gave up trying to understand how this works. The ASCII diagram is probably meant to be helpful, but for me it is the complete opposite, I have no idea how these different concepts (meas_date, orig_time, first_samp, and whatnot) actually work, sorry. |
I agree, I've tried several times over the past couple of years to decipher what it's trying to tell me and at one point just gave up. It's just been trial and error for me regarding all things annotations ever since 😅 |
It's definitely OK! As I'm re-looking at this PR after some time I'm also struggling to wrap my head around this system. FYI this diagram was copied from https://mne.tools/dev/generated/mne.Annotations.html. One potential conflict I found is, the diagram says when Lines 1562 to 1563 in 4954672
instead of
If someone who's familiar with the design can clarify that would be great. But I do confirm that the EDF and EEGLAB export will malfunction without correcting for first_time so eventually we would want this fix. |
When cropping the start of a recording,
raw.first_time
is updated whileannotations.onset
is conveniently untouched. However, when exporting to another format where times are reset (starting from zero),annotations.onset
should be corrected so that they represent relative time from the first sample.This correction has been performed when
fmt=‘brainvision’
:mne-python/mne/export/_brainvision.py
Lines 78 to 85 in e2c8010
But is curiously missing when
fmt=‘edf’
orfmt=‘eeglab’
:mne-python/mne/export/_edf.py
Lines 200 to 213 in e2c8010
mne-python/mne/export/_eeglab.py
Lines 28 to 32 in e2c8010
This PR aims to fix this by performing the similar correction (
annotations.onset - raw.first_time