Skip to content

FIX: allow raw_fif to parse brainvision events#6417

Merged
larsoner merged 12 commits intomne-tools:masterfrom
massich:bv_events
Jun 11, 2019
Merged

FIX: allow raw_fif to parse brainvision events#6417
larsoner merged 12 commits intomne-tools:masterfrom
massich:bv_events

Conversation

@massich
Copy link
Copy Markdown
Contributor

@massich massich commented Jun 5, 2019

This PR fixes this (see discussion in #6326):

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') # now they are equal!!

candidate = getattr(raw, '_get_auto_event_id', _DefaultEventParser)
if candidate == _DefaultEventParser and \
_check_bv_annot(raw.annotations.description):
return _BVEventParser()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was an additional qualifier @agramfort wanted which was that raw be an instance of RawFIF (or I think it should also include RawArray)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why I started by implementing this inside a class method of Raw, but then getattr does nothing because raw is a subclass and its always present

Copy link
Copy Markdown
Contributor Author

@massich massich Jun 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

up to some point, this part was easy: just get read of the default and the base class would already give you _DefaultEventParser.

But the method itself was weird:

    @classmethod
    def _get_auto_event_id(cls):
        """Return default ``event_id`` behavior unless it matches Brainvision."""
        if _check_annotation_descriptions:  # self is not present
            return _DefaultEventParser()
        else:
            return _BVEventParser()  # return a free helper function of a class that inherates from RaW.

returning _BVEventParser is not a problem perse, but it breaks the conceptual structure.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should be able to default to _BVEventParser() only if you have a RawBrainVision, RawFIF or RawArray

Copy link
Copy Markdown
Contributor Author

@massich massich Jun 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the contract then we need to create RawFiF which we don't have. (we only have an alias for backward compativility)

class A:
    pass
class B(A):
    pass
a = A()
b = B()
print(isinstance(a, A), isinstance(b,A), issubclass(type(b),A), issubclass(type(a),A))
True True True True

Copy link
Copy Markdown
Contributor Author

@massich massich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to make it as clean as I could. But I think that it would be simpler just to inspect raw.annotations.description and make the guess best on that (we have to execute _check_bv_annot anyway).


def _check_event_id(event_id, raw):
from mne.io.brainvision.brainvision import _BVEventParser
from mne.io.brainvision.brainvision import _check_bv_annot
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware of the imports (I'll push a commit to change them)

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 5, 2019

The other concern I have with this implementation is that right now is strict. Only if all the descriptors belong to 'brainvision' it would be considered brainvision. If you have some extra markers, then it would be considered not brainvision. Which to me is fine. If you have been messing up the markers, saving and reloading etc.. you should be knowing what you are doing.

@codecov
Copy link
Copy Markdown

codecov bot commented Jun 5, 2019

Codecov Report

Merging #6417 into master will increase coverage by 0.01%.
The diff coverage is 82.6%.

@@            Coverage Diff             @@
##           master    #6417      +/-   ##
==========================================
+ Coverage   89.22%   89.24%   +0.01%     
==========================================
  Files         411      411              
  Lines       74623    74643      +20     
  Branches    12328    12331       +3     
==========================================
+ Hits        66582    66614      +32     
+ Misses       5185     5159      -26     
- Partials     2856     2870      +14

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 5, 2019

I would implement it like this:

def _check_event_id(event_id, raw):
    from mne.io.brainvision.brainvision import _BVEventParser
    from mne.io.brainvision.brainvision import _check_bv_annot

    if event_id is None:
        return _DefaultEventParser()
    elif event_id == 'auto':
        if _check_bv_annot(raw.annotations.description):
            return _BVEventParser()
        else:
            return _DefaultEventParser()
    elif callable(event_id) or isinstance(event_id, dict):
        return event_id
    else:
        raise ValueError('Invalid input event_id')

@@ -501,7 +501,7 @@ def test_read_vhdr_annotations_and_events():
raw.annotations.append([1, 2, 3], 10, ['ZZZ', s_10, 'YYY'])
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version from e64fd4f simplifies _check_event_id and allow to remove getattr and the class methods. But requires to pass event_id because we are appending extra markers.

-    _, event_id = events_from_annotations(raw)
+    _, event_id = events_from_annotations(raw, event_id='brainvision')

If we want to keep the default, in this case, we need to keep the class method etc. Or relax
_check_bv_annot and as soon as we detect one 'brainvision' marker, parse everything as 'brainvision'

WDYT @agramfort @jona-sassenhagen? How would you like to write the call?

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 5, 2019

I've made the imports relative but they can't go outside of the function.

expected_event_id.update(YYY=10001, ZZZ=10002) # others starting at 10001
expected_event_id[s_10] = 10
_, event_id = events_from_annotations(raw)
_, event_id = events_from_annotations(raw, event_id='brainvision')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this change necessary? or is it just to have this option covered by a test?

just checking if there is any public API change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agramfort
Copy link
Copy Markdown
Member

agramfort commented Jun 6, 2019 via email

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 6, 2019

Not as it is here:

from .fiff import Raw as RawFIF

This is the problem of fif being the base object.

But we can always resurrect RawFIF

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 6, 2019

Scratch anything I said. RawBrainVision(BaseRaw) not Raw. Raw and RawBrainVision they are indeed siblings.

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 6, 2019

I think this makes it. Merge if everyone is happy.

@agramfort
Copy link
Copy Markdown
Member

good enough for me !

@massich can you see why travis complains?

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 6, 2019

I would say its not related. its a precision error in lcmv.

@agramfort
Copy link
Copy Markdown
Member

agramfort commented Jun 6, 2019 via email

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 6, 2019

I could not replicate in my machine. so I restarted Travis to see if its just random.

~/code/mne-python bv_events*
(mne35) ❯ pytest mne/beamformer                                                    
Test session starts (platform: linux, Python 3.5.5, pytest 3.8.1, pytest-sugar 0.9.2)
rootdir: /home/sik/code/mne-python, inifile: setup.cfg
plugins: sugar-0.9.2, faulthandler-1.5.0, cov-2.6.0

 mne/beamformer/tests/test_dics.py s✓✓✓✓✓✓✓✓✓✓                                                                                19% █▉        
 mne/beamformer/tests/test_external.py ✓✓✓✓                                                                                   26% ██▋       
 mne/beamformer/tests/test_lcmv.py ✓ssss✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓                                                   95% █████████▌
 mne/beamformer/tests/test_rap_music.py ✓✓✓                                                                                  100% ██████████
------------------------------------- generated xml file: /home/sik/code/mne-python/junit-results.xml --------------------------------------


========================================================= short test summary info ==========================================================
SKIP [1] mne/beamformer/tests/test_dics.py:107: Test test_make_dics skipped, requires h5py. Got exception (No module named 'h5py')
SKIP [4] mne/beamformer/tests/test_lcmv.py:199: Test test_make_lcmv skipped, requires h5py. Got exception (No module named 'h5py')
======================================================== slowest 20 test durations =========================================================
3.00s call     mne/beamformer/tests/test_lcmv.py::test_lcmv_vector
2.58s call     mne/beamformer/tests/test_lcmv.py::test_lcmv_reg_proj[unit-noise-gain-True]
2.54s call     mne/beamformer/tests/test_lcmv.py::test_lcmv_reg_proj[unit-noise-gain-False]
2.46s call     mne/beamformer/tests/test_dics.py::test_tf_dics[testing_data]
2.21s call     mne/beamformer/tests/test_lcmv.py::test_lcmv_reg_proj[nai-True]
2.03s call     mne/beamformer/tests/test_lcmv.py::test_lcmv_reg_proj[None-False]
2.00s call     mne/beamformer/tests/test_lcmv.py::test_lcmv_reg_proj[nai-False]
1.97s call     mne/beamformer/tests/test_lcmv.py::test_tf_lcmv
1.96s call     mne/beamformer/tests/test_lcmv.py::test_lcmv_reg_proj[None-True]
1.55s call     mne/beamformer/tests/test_dics.py::test_apply_dics_timeseries[testing_data]
1.46s call     mne/beamformer/tests/test_rap_music.py::test_rap_music_simulated
1.44s call     mne/beamformer/tests/test_rap_music.py::test_rap_music_sphere
1.34s call     mne/beamformer/tests/test_lcmv.py::test_lcmv_source_power
1.25s setup    mne/beamformer/tests/test_external.py::test_lcmv_fieldtrip[testing_data-ug_vec-None-None-False]
1.08s setup    mne/beamformer/tests/test_external.py::test_lcmv_fieldtrip[testing_data-ung-unit-noise-gain-max-power-False]
1.02s call     mne/beamformer/tests/test_lcmv.py::test_depth_does_not_matter[testing_data-testing_data-max-power-nai]
0.99s call     mne/beamformer/tests/test_lcmv.py::test_depth_does_not_matter[testing_data-testing_data-max-power-unit-noise-gain]
0.97s setup    mne/beamformer/tests/test_external.py::test_lcmv_fieldtrip[testing_data-ug_scal-None-max-power-False]
0.96s call     mne/beamformer/tests/test_lcmv.py::test_lcmv_ctf_comp
0.94s call     mne/beamformer/tests/test_lcmv.py::test_depth_does_not_matter[testing_data-testing_data-vector-nai]

Results (73.02s):
      53 passed
       5 skipped

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 6, 2019

It passed here https://travis-ci.org/mne-tools/mne-python/jobs/542190809#L2724

Travis should be green this time.

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 6, 2019

@jona-sassenhagen does this solve your usecase?

@larsoner
Copy link
Copy Markdown
Member

larsoner commented Jun 6, 2019

@jona-sassenhagen feel free to merge if you're happy

return getattr(raw, '_get_auto_event_id', _DefaultEventParser)()
if isinstance(raw, RawBrainVision) or (
isinstance(raw, (RawFIF, RawArray)) and
_check_bv_annot(raw.annotations.description)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should log/warn in case the BV parser is called on a RawFIF/RawArray?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not warn because the save-load round-trip should be a reasonable use case. But a logger.info seems fine

markers_basename = set([dd.rstrip('0123456789 ') for dd in descriptions])
bv_markers = (set(_BV_EVENT_IO_OFFSETS.keys())
.union(set(_OTHER_ACCEPTED_MARKERS.keys())))
return len(markers_basename - bv_markers) == 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could be even stricter here if I understand correctly that the numeric code ending a BV code is always 3 characters (whitespace left padded plus an int 0-255), i.e.,

set([dd[:-3] for dd in descriptions])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cbrnr this is the case right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, at least I've only ever seen markers like that (S 2, R 1, ...).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started by set([dd[:-3] for dd in descriptions]) but I went for rstrip because I found it more readable. But either one is fine.

@jona-sassenhagen
Copy link
Copy Markdown
Contributor

Thanks for doing this @massich . If you notify when you've addressed my two short comments, I would merge.

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 11, 2019

I've also rebased. Feel free to merge.

@jona-sassenhagen
Copy link
Copy Markdown
Contributor

The Travis builds are failing?

@larsoner
Copy link
Copy Markdown
Member

Yes and the failure is real

https://travis-ci.org/mne-tools/mne-python/jobs/544263546

@massich
Copy link
Copy Markdown
Contributor Author

massich commented Jun 11, 2019

yes I think that sync or some other had some extra space. rstrip should work

@larsoner larsoner merged commit a420019 into mne-tools:master Jun 11, 2019
@larsoner
Copy link
Copy Markdown
Member

Thanks @massich

@jona-sassenhagen
Copy link
Copy Markdown
Contributor

Thanks @massich !

larsoner pushed a commit to larsoner/mne-python that referenced this pull request Jun 11, 2019
* TST: usecase

* FIX: check annotation description base names

* fix: remove pathlib

* RFC: Alternative without getattr.

* imports

* Fix: avoid adding 'barinvision' as event_id keyword

* remove _get_auto_event_id

* update whatsnew

* Update brainvision.py

* cosmit

* fix

* use rstrip
agramfort pushed a commit that referenced this pull request Jun 12, 2019
* FIX: allow raw_fif to parse brainvision events (#6417)

* TST: usecase

* FIX: check annotation description base names

* fix: remove pathlib

* RFC: Alternative without getattr.

* imports

* Fix: avoid adding 'barinvision' as event_id keyword

* remove _get_auto_event_id

* update whatsnew

* Update brainvision.py

* cosmit

* fix

* use rstrip

* MRG+1, DOC: footer logos (#6420)

* add individual inst. logos; add uni-graz

* add image titles (hover)

* force add missing logos

* codespell skip SVG files

* fix: make inria PNG; better logo wrapping

* fix footer overlap; slightly larger logos

* STY: prefer _DefaultEventParser to _Counter (#6355)

* Fix docstrings (#6385)

* docstring deduplication

* more n_jobs

* fix: import fill_doc

* fix crossrefs; deduplicate docdict def for random_state

* FIX: Conftest [circle front]

* FIX: Ref [circle front]
@jona-sassenhagen
Copy link
Copy Markdown
Contributor

Oh wow, I found a file that's become unreadable with this PR!

    826         pat = re.compile(r'^(\s*def\s)|(\s*async\s+def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)')
    827         while lnum > 0:
--> 828             if pat.match(lines[lnum]): break
    829             lnum = lnum - 1
    830         return lines, lnum

IndexError: list index out of range

I am investigating.

@massich massich deleted the bv_events branch August 7, 2019 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants