MRG: Brainvision events_from_annotations#6326
Conversation
|
Can you quickly say what's different relative to #6276? |
Codecov Report
@@ Coverage Diff @@
## master #6326 +/- ##
==========================================
+ Coverage 89.23% 89.23% +<.01%
==========================================
Files 417 417
Lines 75420 75460 +40
Branches 12417 12424 +7
==========================================
+ Hits 67302 67339 +37
- Misses 5231 5233 +2
- Partials 2887 2888 +1 |
|
It simplifies the function and addresses some of the issues it had. But again this is the same as having a huge dict with the equivalences and return it. |
|
@massich do you have more commits to push? If not, I can try and set it non-WIP @jona-sassenhagen okay for you as requested by @cbrnr not to drop any of the events? I agree that just doing 2001, 2002, ... for all non-stim, non-resp BV markers in a |
|
Yes ok with me. |
|
@jona-sassenhagen @cbrnr @massich can you look and make sure that you're happy with the docstring describing the behavior of this? I've also modified tests that hopefully verify it works according to the description. |
|
The docstring looks good! If everything works as advertised, +1 for merge! |
|
Just pushed a commit to shorten the docstring, but the changes are minor |
|
@larsoner ok, I just saw what you did. The only fuzzy thing is why we need is it 'cos we need something that returns |
|
Shall we backport this one to 0.17? Will we be releasing the last 0.17.X? |
|
Usually after releasing a new version we stop backporting things, so no probably not. FYI here is the CircleCI rendering https://13163-1301584-gh.circle-artifacts.com/0/dev/generated/mne.events_from_annotations.html |
Yes it's because we need the |
|
There is a merge conflict now, feel free to rebase |
init test fix simplify parser .. .. ... tests ... FIX: Add modified test from 0.15 FIX: Flake
| 'New Segment/', 'Stimulus/S253', 'Stimulus/S255', 'Stimulus/S254', | ||
| 'Stimulus/S255', 'Stimulus/S254', 'Stimulus/S255', 'Stimulus/S253', | ||
| 'Stimulus/S255', 'Response/R255', 'Stimulus/S254', 'Stimulus/S255', | ||
| 'SyncStatus/Sync On', 'Optic/O 1' |
There was a problem hiding this comment.
Might also include an S 10 in there to ensure it's parsed correctly.
|
I don't know, I would not expect a FIF file to be parsed like a BV file, even if we're just talking about events. I'd prefer |
I agree for FIF files from the Neuromag scanner, but not for BV files that we have read in and then written to FIF format (mostly) just because it's the only output format we support. The problematic case that @jona-sassenhagen brings up seems to show the problem, basically: This behavior is not so good. In my mind the question is how big is the risk that someone using a non-BV file will add a BV-like event? Given the specificity of the event naming I think it's pretty low and we should just go to a |
|
+1 for the approach of looking at the annotations, and printing a warning if any of them look like a BV-style annotation and candidate for backport? |
|
Yeah we'll want to backport. Sounds like people prefer to stick with the class-based check / my original 3-step proposal above. @massich can you look? |
|
smarter "auto" and/or "brainvision" explicit param, either are fine with me.
Basically the parsing of the brainvision annotations is doing some black
magic
that only MNE does AFAIK. We pay a price for too magic but sometimes you
just want things to work...
… |
|
@larsoner yes I think your approach is fine, but why do you need two parameters |
|
sleeping over it. For the event_id='auto' as fif files don't have naturally
annotions we can then trigger
the check for brainvision style annotations (ie only do it for a RawFIF
instance)
|
|
let me rephrase based on @larsoner snippet: raw = read_raw_brainvision(...)
raw.save(fname)
raw_read = read_raw_fif(fname)
events_from_annotations(raw_read, event_id='auto') != events_from_annotations(raw, event_id='auto')and now we are proposing this: def events_from_annotations(raw, event_id="auto",
regexp=r'^(?![Bb][Aa][Dd]|[Ee][Dd][Gg][Ee]).*$',
use_rounding=True, chunk_duration=None,
verbose=None):
...
event_id = _check_event_id(event_id, raw)
...
def _check_event_id(event_id, raw):
from mne.io.brainvision import _BVEventParser as bv_default
from mne.io.brainvision import _check_raw_fif_from_original_bv as black_magic
if event_id is None:
return _DefaultEventParser()
elif event_id == 'brainvision':
return bv_default()
elif event_id == 'auto':
candidate = getattr(raw, '_get_auto_event_id', _DefaultEventParser)()
if black_magic(raw) and candidate != bv_default:
warn("you might want to try `event_id='brainvision'`)
return candidate
elif callable(event_id) or isinstance(event_id, dict):
return event_id
else:
raise ValueError('Invalid input event_id')So that this would happen: raw = read_raw_brainvision(...)
events_when_raw_was_bv = events_from_annotations(raw)
raw.save(fname)
raw_read = read_raw_fif(fname)
events_from_annotations(raw_read, event_id='auto') != events_when_raw_was_bv
# you get a warining
events_from_annotations(raw_read, event_id='brainvision') == events_when_raw_was_bvMy opinion is that from mne.io.brainvision import event_ids as bv_events
raw = read_raw_fif(fname_of_an_old_bv)
events, _ = events_from_annotations(raw, event_id=bv_events)If you want to get brainvision events (or any other type) you need to pass a proper Educating the users that you need I could see people arguing that using strings is better (maybe, I'm not sure. I like the idea of having to import the translator. I find it more explicit): raw = read_raw_fif(fname_of_an_old_bv)
events, _ = events_from_annotations(raw, event_id='brainvision')But I fear that if we start introspecting and guessing which was the original format At this point I would just make |
In prickle yes, but in practice BV stimulus events are extremely regular and specific, and everyone who uses events like "Stimulus/S100" will want it to be parsed to
Again, in principle yes, but right now a lot of people are working with It seems to me like the point of annotations is to make working with non-int events easier, but here we have something that the experimenter has programmed into their software as an int, is stored by BV as an int plus some predictable boiler plate, and is then universally parsed as an int. But we somehow make it hard to use? It's literally 8-bit integer codes! To be frank, this should have been handled back when annotations were conceived ... I should have been more involved there, to represent BV. Now we will end up with a less than optimal solution either way.
Ok, maybe? If we add big warning messages for every usage of events_from_annotations on RawBrainvision objs for a cycle or three, and tell every BV user to always use event_id='brain vision' in all BV-derived code. |
def _check_event_id(event_id, raw):
from mne.io.brainvision import _BVEventParser as bv_default
if event_id is None:
return getattr(raw, '_get_auto_event_id', _DefaultEventParser)()
elif event_id == 'auto':
warn("`event_id='auto'` is deprecated. Use `event_id=None` instead",
DeprecationWarning)
return getattr(raw, '_get_auto_event_id', _DefaultEventParser)()
elif event_id == 'brainvision':
return bv_default()
elif callable(event_id) or isinstance(event_id, dict):
return event_id
else:
raise ValueError('Invalid input event_id') |
-1 on this. I am with @jona-sassenhagen that this mode is useful for BV users and we can potentially extend it to others (e.g., CTF once it's done). The proposal that @agramfort seems to support now is actually not the one above (about warning) but rather a modified version of the second proposal, which I think would be:
I'm fine with this. It does not seem too fragile or risky, and simplifies things for BV users. This can also be how we treat other readers. For example for the CTF annotations we are adding, if they end up having standard names -- instance of RawFIF/RawArray with CTF-like annotations, or RawCTF: use CTF event parsing. So it's a sustainable approach, too. (And in extreme corner cases where CTF and BV-like events are found, which should be pretty much never, we can raise an error or warn or something.) |
It's not a problem of the annotation changes. Before 0.15 events_from_annotations was not general, had 'brainvision' logic embedded in it (which was not even complete. because if you had Despite having the opinion that:
I will stay out of the pull. I'll be +0 in making 'barinvision' the default of |
|
@larsoner what are you -1 ? to the warning? we could keep both approaches: def _check_event_id(event_id, raw):
from mne.io.brainvision import _BVEventParser as bv_default
if event_id is None or event_id='auto':
return getattr(raw, '_get_auto_event_id', _DefaultEventParser)()
elif event_id == 'brainvision':
return bv_default()
elif callable(event_id) or isinstance(event_id, dict):
return event_id
else:
raise ValueError('Invalid input event_id') |
|
This discussion shows that the underlying problem is really that we now have two things to represent discrete events - see #6018. Whatever we do here, it will never be perfect, because we decided to represent discrete events stored in files as annotations. Annotations contain everything you need, including string descriptions. I know that superficially it is easier to work with a NumPy array, but you could do everything with annotations. We'd only need to add support for annotations everywhere we currently accept events. |
I am -1 to deprecating
Yes in this case the "only" includes critically |
Yes, let's discuss in #6018 - but in the case of Epochs and annotations, why would you need event IDs when the user would specify the string descriptions that should be used for epoch creation? |
raw = read_raw_brainvision(...)
raw.save(fname)
raw_read = read_raw_fif(fname)
events, event_id = events_from_annotations(raw)
# or any other of those:
#
# events, event_id = events_from_annotations(raw, event_id='auto')
# events, event_id = events_from_annotations(raw, event_id='brainvision')
# event_id = mne.io.brainvision.bla
# events, _ = events_from_annotations(raw, event_id=event_id)
epoch = Epochs(raw, events=events, event_id=event_id)The thing is that until now raw = read_raw_brainvision(...)
raw.save(fname)
raw_read = read_raw_fif(fname)
my_descriptors = ['foo', 'bar']
epoch = Epochs(raw, my_descriptors) |
|
The other thing that nobody mentioned is the elephant in the room: Why everything has to be fif. If I could save brainvision files I would have no trouble. raw = read_raw_brainvision('name.vhdr')
raw.save('bla.vhdr')
raw2 = read_raw_brainvision('bla.vhdr')and then suport raw = read_raw('name.vhdr')
raw.save('bla.vhdr')
raw2 = read_raw('bla.vhdr')To me an image is an image, regardless of the format that once it was stored. |
|
what I propose has minimal consequences on project API and makes it work
for Jona....
|
|
@agramfort there's a small PR comming with those changes. To discuss in concrete terms. |
|
I’d think alex’ second proposal should work. A BV writer I don’t like. .fif isn’t perfect, but a lot of work has gone into making it a standard. And you’ll save your epochs and Ica Objs as fif too. |
|
Great thanks @massich |
And why shouldn't this be possible? We could check whether the second argument is an nd-array and treat it as events or a string/list of strings and get the onsets from Re the elephant, I agree that FIFF is not a universal format. Few tools outside of MNE can even read FIFF, so if I really want to store some data I go for something else, e.g. the BIDS-EEG supported formats EDF or BrainVision, or even EEGLAB, which is just a MAT file many tools can read. (For the sake of reproducibility, it's always best to only use the raw data, but I know that there are situations where you want to store some intermediate results to disk.) Currently, MNE can't export any of these three formats for different reasons, so I've started to implement that in MNELAB. Currently, I can export to EEGLAB and EDF/BDF, and I will add BrainVision export based on the implementation by @palday soon. We could discuss if any of these formats might be suitable for direct integration into MNE. Re |
|
And why shouldn't this be possible? We could check whether the second argument is an nd-array and treat it as events or a string/list of strings and get the onsets from raw.annotations. Doesn't look too difficult, but maybe I'm missing something (because I've never really used Epochs).
that could work but a number of functions that take epochs as input will
use the epochs.events attribute. We need to be careful that certain epochs
will not work for certain functions. I am afraid of a rabbit hole here.
|
But we could start with adding this feature while not touching the old behavior at all (i.e. leaving the events-based interface as default). |
|
I'm in favor of all those things. But we need a discussion and a roadmap. |
|
events_from_annotations is one more line of code and keeps the magic
level one step lower
but I am open to discussion to see what we can cook up
|
This has actually been a huge pain for me in the past too. None if my non-MNE-using colleagues can read those FIFF files, let alone knows which software could potentially be used to open them. |
Closes #6267
Closes #6276