-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add Annotations to CNT + add code to deprecate stim_channel #6047
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
Add Annotations to CNT + add code to deprecate stim_channel #6047
Conversation
Codecov Report
@@ Coverage Diff @@
## master #6047 +/- ##
==========================================
+ Coverage 88.91% 88.95% +0.04%
==========================================
Files 401 402 +1
Lines 73013 73123 +110
Branches 12135 12151 +16
==========================================
+ Hits 64922 65050 +128
+ Misses 5201 5188 -13
+ Partials 2890 2885 -5 |
cd14d74 to
40fa0e3
Compare
|
@wmvanvliet you added the type 3 events in #3911, can you test that |
|
for the record, I've checked with files that have cnt and eeglab versions:
It would be good if we can get cnt files with events of type1 and type3 to be added to the test suit (I only have type2). |
TL;DRJust for the record, testing with some confidential files + some files on the web running the following import mne
import pytest
import os.path as op
from datetime import datetime
from mne import __file__ as _mne_file
from mne.tests.test_annotations import _assert_annotations_equal
regular_path = op.join(op.dirname(_mne_file), '..', 'sandbox', 'data')
confidential_path = op.join(regular_path, 'confidential', 'cnt')
flankers_path = op.join(regular_path, '914flankers.cnt')
cnt_files_with_eeglab_pair = [
op.join(confidential_path, 'BoyoAEpic1_16bit.cnt'),
op.join(confidential_path, 'cont_67chan_resp_32bit.cnt'),
op.join(confidential_path, 'SampleCNTFile_16bit.cnt')
]
def test_ensure_meas_date(recwarn):
raw = mne.io.read_raw_cnt(flankers_path, montage=None,
date_format='dd/mm/yy')
meas_date = (datetime
.fromtimestamp(raw.info['meas_date'][0])
.strftime('%d/%m/%y %H:%M:%S'))
assert meas_date == '23/09/07 12:22:15'
@pytest.mark.parametrize('fname', cnt_files_with_eeglab_pair, ids=op.basename)
def test_check_cnt_eeglab_pairs(fname, recwarn):
raw_eeglab = mne.io.read_raw_eeglab(fname.replace('.cnt', '.set'),
montage=None)
raw_cnt = mne.io.read_raw_cnt(fname, montage=None)
assert raw_cnt.info.keys() == raw_eeglab.info.keys()
xx = object_diff(raw_cnt.info, raw_eeglab.info)
print(xx)I get:
['chs'][63]['cal'] type mismatch (<class 'numpy.ndarray'>, <class 'float'>)
['chs'][63]['coord_frame'] value mismatch (4, 0)
['chs'][63]['loc'] array mismatch
['chs'][63]['unit_mul'] type mismatch (<class 'float'>, <class 'int'>)
['subject_info'] type mismatch (<class 'dict'>, <class 'NoneType'>) |
|
ok it's not a blocker the mismatch with eeglab for my point of view.
… |
|
@massich you have a flake8 error https://travis-ci.org/mne-tools/mne-python/jobs/505338464 |
mne/io/fieldtrip/tests/helpers.py
Outdated
| event_id = list(cfg_local['eventvalue'].astype('int')) | ||
| else: | ||
| event_id = [int(cfg_local['eventvalue'])] | ||
| if 'event_id' not in locals(): |
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.
calling locals() here seems dangerous makes the code harder to read. Can you see how to avoid it? thx
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.
forget it it's in tests. Not strongly necessary then but just nice to do.
mne/io/cnt/tests/test_cnt.py
Outdated
| def test_read_annotations(): | ||
| """Test reading for annotations from a .CNT file.""" | ||
| annot = read_annotations(fname) | ||
| assert len(annot) == 6 |
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.
I would merge this test with test_compare_events_and_annotations by replacing above the call to _read_annotations_cnt by read_annotations(fname). It will cover the same amount of code and will save time.
@jona-sassenhagen did the actual tests back then. I don't have any CNT files with type 3 events. |
|
@jona-sassenhagen can you test with your event-type-3 CNT file(s)? |
mne/io/cnt/_utils.py
Outdated
| teeg_parser = Struct('<Bll') | ||
|
|
||
| f.seek(teeg_offset) | ||
| return Teeg._make(teeg_parser.unpack(f.read(teeg_parser.size))) |
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.
Using private _make does not look clean / safe (using private method). Why not just return Teeg(*teeg_parser.unpack(f.read(teeg_parser.size)))
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.
'Cos I did not know how to write it! Thx.
mne/io/cnt/_utils.py
Outdated
| def parser(buffer): | ||
| struct = Struct(struct_pattern) | ||
| for chunk in struct.iter_unpack(buffer): | ||
| yield event_maker._make(chunk) |
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.
same
mne/io/cnt/cnt.py
Outdated
|
|
||
| with open(fname, 'rb') as fid: | ||
| fid.seek(SETUP_NCHANNELS_OFFSET) | ||
| (n_channels,) = unpack('<H', fid.read(calcsize('<H'))) |
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.
typically (and more compactly) we use and prefer np.frombuffer instead of fid.read + unpack
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.
if np.frombuffer took a file descriptor (or the file was lodaded in a buffer) this could be written like this which is really readable:
with open(fname, 'rb') as fid:
n_channels = np.frombuffer(
fid, dtype=np.uint16, offset=SETUP_NCHANNELS_OFFSET)
sfreq = np.frombuffer(
fid, dtype=np.uint16, offset=SETUP_RATE_OFFSET)
event_table_pos = np.frombuffer(
fid, dtype=np.int32, offset=SETUP_EVENTTABLEPOS_OFFSET)Otherwise np.fromfile but you can not get read of the seek.
with open(fname, 'rb') as fid:
fid.seek(SETUP_NCHANNELS_OFFSET)
n_channels = np.fromfile(fid, dtype='<u2', count=1)[0]
fid.seek(SETUP_RATE_OFFSET)
sfreq = np.fromfile(fid, dtype='<u2', count=1)[0]
fid.seek(SETUP_EVENTTABLEPOS_OFFSET)
event_table_pos = np.fromfile(fid, dtype='<i4', count=1)[0]But I've no idea which of the 3 is preferable.
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.
frombuffer is what we use in mne/io/tag.py and it works with a file descriptor, we changed it to that from np.fromfile a year or so ago so I think it's the preferred way
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.
(or rather I guess we do frombuffer(fid.read(...), ...) in tag.py)
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.
I would say do whichever works. Having a fid.seek in there is expected/fine
mne/io/cnt/cnt.py
Outdated
| def read_raw_cnt(input_fname, montage, eog=(), misc=(), ecg=(), emg=(), | ||
| data_format='auto', date_format='mm/dd/yy', preload=False, | ||
| verbose=None): | ||
| stim_channel=True, verbose=None): |
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.
Here it should be None
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.
Shouldn't default to True to not break people's code? At the beginning I just removed the whole stim. But talking to @agramfort it seems that we cannot get read of the stim in cnt until 0.20, unless we backport the addition of this stim_channel=True to 0.17.3 and then 0.18 to false/none and remove the stim channel.
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.
I would make None an alias for True, where None emits a warning but True does not. But maybe it's not necessary.
mne/io/cnt/cnt.py
Outdated
| stim_channel[event_time - 1] = event_id | ||
|
|
||
| else: # when stim_channel_toggle is False | ||
| pass |
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.
I don't see much value in else: pass because it's implied by just not having it there; and in this case the comment does not add anything because it's just the conditional checked above
mne/io/cnt/cnt.py
Outdated
| stim_channel=stim_channel, n_bytes=n_bytes) | ||
| else: | ||
| cnt_info.update(baselines=np.array(baselines), n_samples=n_samples, | ||
| n_bytes=n_bytes) |
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.
There is redundancy in several conditionals, including here, which could instead be (no need for else at all):
if stim_channel_toggle:
...
cnt_info.update(stim_channel=stim_channel)
cnt_info.update(baselines=np.array(baselines), n_samples=n_samples,
n_bytes=n_bytes)
Seems simpler (makes it clear no matter what, cnt_info is updated with these entries) and is more DRY, no?
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.
totally.
mne/io/cnt/cnt.py
Outdated
| _data_stop = start + sample_stop | ||
| data_[-1] = stim_ch[_data_start:_data_stop] | ||
| else: | ||
| pass |
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.
no need for else: pass
mne/io/cnt/cnt.py
Outdated
| description : array of str, shape (n_annotations,) | ||
| Array of strings containing description for each annotation. If a | ||
| string, all the annotations are given the same description. To reject | ||
| epochs, use description starting with keyword 'bad'. See example above. |
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.
Docstring is wrong. You return an annotations
mne/io/cnt/cnt.py
Outdated
| large amount of memory). If preload is a string, preload is the | ||
| file name of a memory-mapped file which is used to store the data | ||
| on the hard drive (slower, requires less memory). | ||
| stim_channel : bool (default True) |
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.
None not true
|
@larsoner merge if you are happy. The code synthesizing the stim channel from the events type 3 is still the same, and there's an upcoming PR fixing the overflow + the 32 bits annotations. |
In this PR: - [x] Fix CNT date parsing - [x] Add CNT support in `mne.read_annotations` - [x] Add `stim_channel` parameter to `mne.io.read_raw_cnt` (to later remove stim synthesis) - [x] Fix tests to use `stim_channel=False`
| raise IOError('Unexpected event size.') | ||
|
|
||
| # XXX long NumEvents is available, why are not we using it? | ||
| n_events = event_size // event_bytes |
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.
I guess that #6535 is why.
In this PR:
mne.read_annotationsstim_channelparameter tomne.io.read_raw_cnt(to later remove stim synthesis)stim_channel=FalseThis needs to go before #6025