Skip to content

Commit 7260675

Browse files
committed
fixes, refactoring, documentation, clean up
1 parent e452a1a commit 7260675

3 files changed

Lines changed: 87 additions & 58 deletions

File tree

mne/io/curry/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66

77
from .curry import read_raw_curry
88
from .curry import read_impedances_curry
9-
from .curry import read_montage_curry
9+
from .curry import read_dig_curry

mne/io/curry/curry.py

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ def _check_curry_filename(fname):
3737
# try suffixes
3838
if fname_in.suffix in CURRY_SUFFIX_DATA:
3939
fname_out = fname_in
40+
elif (
41+
fname_in.with_suffix("").exists()
42+
and fname_in.with_suffix("").suffix in CURRY_SUFFIX_DATA
43+
):
44+
fname_out = fname_in.with_suffix("")
4045
else:
4146
for data_suff in CURRY_SUFFIX_DATA:
4247
if fname_in.with_suffix(data_suff).exists():
@@ -116,10 +121,12 @@ def _get_curry_meas_info(fname):
116121
)
117122
is_ascii = byteorder == "ASCII"
118123

119-
# amp info
120-
# TODO - seems like there can be identifiable information (serial numbers, dates).
124+
# amplifier info
125+
# TODO - PRIVACY
126+
# seems like there can be identifiable information (serial numbers, dates).
121127
# MNE anonymization functions only overwrite "serial" and "site", though
122-
# TODO - there can be filter details, too
128+
# TODO - FUTURE ENHANCEMENT
129+
# # there can be filter details in AmplifierInfo, too
123130
amp_info = (
124131
re.compile(r"AmplifierInfo\s*=.*\n")
125132
.search(content_hdr)
@@ -182,9 +189,12 @@ def _get_curry_epoch_info(fname):
182189
event_id=event_id,
183190
tmin=0.0,
184191
tmax=(n_samples - 1) / sfreq,
185-
baseline=(0, 0),
192+
baseline=None,
193+
detrend=None,
194+
verbose=False,
186195
metadata=epochmetainfo,
187196
reject_by_annotation=False,
197+
reject=None,
188198
)
189199

190200

@@ -251,9 +261,18 @@ def _extract_curry_info(fname):
251261

252262
# events
253263
events = currydata["events"]
254-
# annotations = currydata[
255-
# "annotations"
256-
# ] # TODO - these dont really seem to correspond to events! what is it?
264+
# BUG in curryreader (v.0.1.1)! annotations read incorrectly (shifted by 1 line)
265+
annotations = currydata["annotations"]
266+
assert len(annotations) == len(events)
267+
if len(events) > 0:
268+
# quick fix: shift annotation down, last one is missing
269+
annotations = annotations[1:] + [""]
270+
event_desc = dict()
271+
for k, v in zip(events[:, 1], annotations):
272+
if int(k) not in event_desc.keys():
273+
event_desc[int(k)] = v.strip() if (v.strip() != "") else str(int(k))
274+
else:
275+
event_desc = None
257276

258277
# impedance measurements
259278
# moved to standalone def; see read_impedances_curry
@@ -324,8 +343,7 @@ def _extract_curry_info(fname):
324343
assert len(ch_pos) == ch_types.count("eeg") + ch_types.count("mag")
325344

326345
# finetune channel types (e.g. stim, eog etc might be identified by name)
327-
# TODO?
328-
346+
# TODO - FUTURE ENHANCEMENT
329347
# scale data to SI units
330348
orig_units = dict(zip(ch_names, units))
331349
cals = [
@@ -342,6 +360,7 @@ def _extract_curry_info(fname):
342360
landmarkslabels,
343361
hpimatrix,
344362
events,
363+
event_desc,
345364
orig_format,
346365
orig_units,
347366
cals,
@@ -366,7 +385,9 @@ def _read_annotations_curry(fname, sfreq="auto"):
366385
"""
367386
fname = _check_curry_filename(fname)
368387

369-
(sfreq_fromfile, _, _, _, _, _, _, _, events, _, _, _) = _extract_curry_info(fname)
388+
(sfreq_fromfile, _, _, _, _, _, _, _, events, event_desc, _, _, _) = (
389+
_extract_curry_info(fname)
390+
)
370391
if sfreq == "auto":
371392
sfreq = sfreq_fromfile
372393
elif np.isreal(sfreq):
@@ -381,7 +402,7 @@ def _read_annotations_curry(fname, sfreq="auto"):
381402
if isinstance(events, np.ndarray): # if there are events
382403
events = events.astype("int")
383404
events = np.insert(events, 1, np.diff(events[:, 2:]).flatten(), axis=1)[:, :3]
384-
return annotations_from_events(events, sfreq)
405+
return annotations_from_events(events, sfreq, event_desc=event_desc)
385406
else:
386407
warn("no event annotations found")
387408
return None
@@ -417,18 +438,18 @@ def _make_curry_montage(ch_names, ch_types, ch_pos, landmarks, landmarkslabels):
417438
hsp_pos = None
418439
# make dig montage for eeg
419440
mont = None
420-
if ch_pos.shape[1] in [3, 6]: # eeg xyz space
441+
if len(ch_pos_eeg) > 0:
421442
mont = make_dig_montage(
422443
ch_pos=ch_pos_eeg,
423444
nasion=landmark_dict["Nas"],
424445
lpa=landmark_dict["LPA"],
425446
rpa=landmark_dict["RPA"],
426447
hsp=hsp_pos,
427448
hpi=hpi_pos,
428-
coord_frame="unknown",
449+
coord_frame="head",
429450
)
430451
else: # not recorded?
431-
pass
452+
warn("No eeg sensor locations found in file.")
432453

433454
return mont
434455

@@ -532,21 +553,23 @@ def _set_chanloc_curry(inst, ch_types, ch_pos, landmarks, landmarkslabels):
532553
else:
533554
raise NotImplementedError
534555

535-
# _make_trans_dig(curry_paths, inst.info, curry_dev_dev_t) # TODO - necessary?!
556+
# TODO - REVIEW NEEDED
557+
# do we need further transpositions for MEG channel positions?
558+
# the testfiles i got look good to me..
559+
# _make_trans_dig(curry_paths, inst.info, curry_dev_dev_t)
536560

537561

538562
@verbose
539563
def read_raw_curry(
540-
fname, import_epochs_as_events=False, preload=False, verbose=None
564+
fname, import_epochs_as_annotations=False, preload=False, verbose=None
541565
) -> "RawCurry":
542566
"""Read raw data from Curry files.
543567
544568
Parameters
545569
----------
546570
fname : path-like
547-
Path to a curry file with extensions ``.dat``, ``.dap``, ``.rs3``,
548-
``.cdt``, ``.cdt.dpa``, ``.cdt.cef`` or ``.cef``.
549-
import_epochs_as_events : bool
571+
Path to a valid curry file.
572+
import_epochs_as_annotations : bool
550573
Set to ``True`` if you want to import epoched recordings as continuous ``raw``
551574
object with event annotations. Only do this if you know your data allows it.
552575
%(preload)s
@@ -568,19 +591,19 @@ def read_raw_curry(
568591
inst = RawCurry(fname, preload, verbose)
569592
if rectype in ["epochs", "evoked"]:
570593
curry_epoch_info = _get_curry_epoch_info(fname)
571-
if import_epochs_as_events:
594+
if import_epochs_as_annotations:
595+
# TODO - REVIEW NEEDED
596+
# give those annotations a specific name/type?
572597
epoch_annotations = annotations_from_events(
573598
events=curry_epoch_info["events"],
574599
event_desc={v: k for k, v in curry_epoch_info["event_id"].items()},
575600
sfreq=inst.info["sfreq"],
576601
)
577602
inst.set_annotations(inst.annotations + epoch_annotations)
578603
else:
579-
inst = Epochs(
580-
inst, **curry_epoch_info
581-
) # TODO - seems to reject flat channel
604+
inst = Epochs(inst, **curry_epoch_info)
582605
if rectype == "evoked":
583-
raise NotImplementedError
606+
raise NotImplementedError # not sure this is even supported format
584607
return inst
585608

586609

@@ -590,8 +613,7 @@ class RawCurry(BaseRaw):
590613
Parameters
591614
----------
592615
fname : path-like
593-
Path to a curry file with extensions ``.dat``, ``.dap``, ``.rs3``,
594-
``.cdt``, ``.cdt.dpa``, ``.cdt.cef`` or ``.cef``.
616+
Path to a valid curry file.
595617
%(preload)s
596618
%(verbose)s
597619
@@ -615,6 +637,7 @@ def __init__(self, fname, preload=False, verbose=None):
615637
landmarkslabels,
616638
hpimatrix,
617639
events,
640+
event_desc,
618641
orig_format,
619642
orig_units,
620643
cals,
@@ -655,11 +678,11 @@ def __init__(self, fname, preload=False, verbose=None):
655678
events = np.insert(events, 1, np.diff(events[:, 2:]).flatten(), axis=1)[
656679
:, :3
657680
]
658-
annot = annotations_from_events(events, sfreq)
681+
annot = annotations_from_events(events, sfreq, event_desc=event_desc)
659682
self.set_annotations(annot)
660683

661684
# add sensor locations
662-
# TODO - review wanted!
685+
# TODO - REVIEW NEEDED
663686
assert len(self.info["ch_names"]) == len(ch_types) >= len(ch_pos)
664687
_set_chanloc_curry(
665688
inst=self,
@@ -670,13 +693,15 @@ def __init__(self, fname, preload=False, verbose=None):
670693
)
671694

672695
# add HPI data (if present)
696+
# TODO - FUTURE ENHANCEMENT
673697
# from curryreader docstring:
674698
# "HPI-coil measurements matrix (Orion-MEG only) where every row is:
675699
# [measurementsample, dipolefitflag, x, y, z, deviation]"
676-
# that's incorrect, though. it seems to be:
700+
#
701+
# that's incorrect, though. it ratehr seems to be:
677702
# [sample, dipole_1, x_1,y_1, z_1, dev_1, ..., dipole_n, x_n, ...]
678703
# for all n coils.
679-
# TODO - do they actually store cHPI?
704+
# We are missing good example data or format specs! do not implement for now.
680705
if not isinstance(hpimatrix, list):
681706
warn("cHPI data found, but reader not implemented.")
682707
hpisamples = hpimatrix[:, 0]
@@ -707,8 +732,7 @@ def read_impedances_curry(fname, verbose=None):
707732
Parameters
708733
----------
709734
fname : path-like
710-
Path to a curry file with extensions ``.dat``, ``.dap``, ``.rs3``,
711-
``.cdt``, ``.cdt.dpa``, ``.cdt.cef`` or ``.cef``.
735+
Path to a valid curry file.
712736
%(verbose)s
713737
714738
Returns
@@ -730,15 +754,10 @@ def read_impedances_curry(fname, verbose=None):
730754
impedances = currydata["impedances"]
731755
ch_names = currydata["labels"]
732756

733-
# try get measurement times
734-
# TODO - is this even possible?
735-
annotations = currydata[
736-
"annotations"
737-
] # dont really seem to correspond to events!?!
738-
for anno in set(annotations):
739-
if "impedance" in anno.lower():
740-
print("FOUND IMPEDANCE ANNOTATION!")
741-
print(f"'{anno}' - N={len([a for a in annotations if a == anno])}")
757+
# get impedance measurement times
758+
# TODO - FUTURE ENHANCEMENT
759+
# info can be in the files (as events or IMPEDANCE_TIMES)
760+
# inconsistently, though, and low priority
742761

743762
# print impedances
744763
print("impedance measurements:")
@@ -749,22 +768,24 @@ def read_impedances_curry(fname, verbose=None):
749768

750769

751770
@verbose
752-
def read_montage_curry(fname, verbose=None):
753-
"""Read eeg montage from Curry files.
771+
def read_dig_curry(fname, verbose=None):
772+
"""Read electrode locations from Curry files.
754773
755774
Parameters
756775
----------
757776
fname : path-like
758-
The filename.
777+
A valid Curry file.
759778
%(verbose)s
760779
761780
Returns
762781
-------
763782
montage : instance of DigMontage | None
764783
The montage.
765784
"""
785+
# TODO - REVIEW NEEDED
786+
# API? do i need to add this in the docs somewhere?
766787
fname = _check_curry_filename(fname)
767-
(_, _, ch_names, ch_types, ch_pos, landmarks, landmarkslabels, _, _, _, _, _) = (
788+
(_, _, ch_names, ch_types, ch_pos, landmarks, landmarkslabels, _, _, _, _, _, _) = (
768789
_extract_curry_info(fname)
769790
)
770791
return _make_curry_montage(ch_names, ch_types, ch_pos, landmarks, landmarkslabels)

mne/io/curry/tests/test_curry.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from mne.datasets import testing
1414
from mne.epochs import Epochs
1515
from mne.event import find_events
16-
from mne.io.curry import read_impedances_curry, read_montage_curry, read_raw_curry
16+
from mne.io.curry import read_dig_curry, read_impedances_curry, read_raw_curry
1717
from mne.io.curry.curry import (
1818
RawCurry,
1919
_check_curry_filename,
@@ -87,10 +87,15 @@ def test_read_raw_curry_epoched():
8787
assert isinstance(ep, Epochs)
8888
assert len(ep.events) == 26
8989
assert len(ep.annotations) == 0
90-
raw = read_raw_curry(epoched_file, import_epochs_as_events=True)
90+
raw = read_raw_curry(epoched_file, import_epochs_as_annotations=True)
9191
assert isinstance(raw, RawCurry)
9292
assert len(raw.annotations) == 26
9393

94+
# check signal identity
95+
aa = raw.get_data()
96+
bb = ep.get_data()
97+
assert np.equal(aa, bb.transpose(1, 0, 2).reshape(aa.shape)).all()
98+
9499

95100
@testing.requires_testing_data
96101
@pytest.mark.parametrize(
@@ -277,7 +282,7 @@ def test_read_device_info():
277282
def test_read_impedances_curry(fname):
278283
"""Test reading impedances from CURRY files."""
279284
_, imp = read_impedances_curry(fname)
280-
actual_imp = empty(shape=(0, 3))
285+
actual_imp = empty(shape=(0, 3)) # TODO - need better testing data
281286
assert_allclose(
282287
imp,
283288
actual_imp,
@@ -286,16 +291,19 @@ def test_read_impedances_curry(fname):
286291

287292
@testing.requires_testing_data
288293
@pytest.mark.parametrize(
289-
"fname",
294+
"fname,mont_present",
290295
[
291-
pytest.param(curry7_bdf_file, id="curry 7"),
292-
pytest.param(curry8_bdf_file, id="curry 8"),
293-
pytest.param(curry7_bdf_ascii_file, id="curry 7 ascii"),
294-
pytest.param(curry8_bdf_ascii_file, id="curry 8 ascii"),
296+
pytest.param(curry7_bdf_file, True, id="curry 7"),
297+
pytest.param(curry8_bdf_file, True, id="curry 8"),
298+
pytest.param(curry7_bdf_ascii_file, True, id="curry 7 ascii"),
299+
pytest.param(curry8_bdf_ascii_file, True, id="curry 8 ascii"),
295300
],
296301
)
297-
def test_read_montage_curry(fname):
302+
def test_read_montage_curry(fname, mont_present):
298303
"""Test reading montage from CURRY files."""
299-
mont = read_montage_curry(fname)
300-
assert isinstance(mont, DigMontage)
301-
# TODO - not very specific, yet
304+
if mont_present:
305+
assert isinstance(read_dig_curry(fname), DigMontage)
306+
else:
307+
# TODO - not reached, yet. no test file without eeg chanlocs
308+
with pytest.warns(RuntimeWarning, match="No sensor locations found"):
309+
_ = read_dig_curry(fname)

0 commit comments

Comments
 (0)