diff --git a/pybv/_export.py b/pybv/_export.py deleted file mode 100644 index c843d8e..0000000 --- a/pybv/_export.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Use pybv to export data from different software packages to BrainVision.""" - -from pathlib import Path -from warnings import warn - -import numpy as np -from mne.channels.channels import _unit2human -from mne.io.constants import FIFF - -from pybv import write_brainvision - - -def _export_mne_raw(*, raw, fname, events=None, overwrite=False): - """Export raw data from MNE-Python. - - Parameters - ---------- - raw : mne.io.Raw - The raw data to export. - fname : str | pathlib.Path - The name of the file where raw data will be exported to. Must end with - ``".vhdr"``, and accompanying *.vmrk* and *.eeg* files will be written inside - the same directory. - events : np.ndarray | None - Events to be written to the marker file (*.vmrk*). If array, must be in - `MNE-Python format `_. If - ``None`` (default), events will be written based on ``raw.annotations``. - overwrite : bool - Whether or not to overwrite existing data. Defaults to ``False``. - - """ - # prepare file location - if not str(fname).endswith(".vhdr"): - raise ValueError("`fname` must have the '.vhdr' extension for BrainVision.") - fname = Path(fname) - folder_out = fname.parents[0] - fname_base = fname.stem - - # prepare data from raw - data = raw.get_data() # gets data starting from raw.first_samp - sfreq = raw.info["sfreq"] # in Hz - meas_date = raw.info["meas_date"] # datetime.datetime - ch_names = raw.ch_names - - # write voltage units as micro-volts and all other units without scaling - # write units that we don't know as n/a - unit = [] - for ch in raw.info["chs"]: - if ch["unit"] == FIFF.FIFF_UNIT_V: - unit.append("µV") - elif ch["unit"] == FIFF.FIFF_UNIT_CEL: - unit.append("°C") - else: - unit.append(_unit2human.get(ch["unit"], "n/a")) - unit = [u if u != "NA" else "n/a" for u in unit] - - # enforce conversion to float32 format - # XXX: Could add a feature that checks data and optimizes `unit`, `resolution`, and - # `format` so that raw.orig_format could be retained if reasonable. - if raw.orig_format != "single": - warn( - f"Encountered data in '{raw.orig_format}' format. " - "Converting to float32.", - RuntimeWarning, - ) - - fmt = "binary_float32" - resolution = 0.1 - - # handle events - # if we got an ndarray, this is in MNE-Python format - msg = "`events` must be None or array in MNE-Python format." - if events is not None: - # Subtract raw.first_samp because brainvision marks events starting from the - # first available data point and ignores the raw.first_samp - assert isinstance(events, np.ndarray), msg - assert events.ndim == 2, msg - assert events.shape[-1] == 3, msg - events[:, 0] -= raw.first_samp - events = events[:, [0, 2]] # reorder for pybv required order - - # else, prepare pybv style events from raw.annotations - else: - events = _mne_annots2pybv_events(raw) - - # no information about reference channels in mne currently - ref_ch_names = None - - # write to BrainVision - write_brainvision( - data=data, - sfreq=sfreq, - ch_names=ch_names, - ref_ch_names=ref_ch_names, - fname_base=fname_base, - folder_out=folder_out, - overwrite=overwrite, - events=events, - resolution=resolution, - unit=unit, - fmt=fmt, - meas_date=meas_date, - ) - - -def _mne_annots2pybv_events(raw): - """Convert mne Annotations to pybv events.""" - events = [] - for annot in raw.annotations: - # handle onset and duration: seconds to sample, relative to - # raw.first_samp / raw.first_time - onset = annot["onset"] - raw.first_time - onset = raw.time_as_index(onset).astype(int)[0] - duration = int(annot["duration"] * raw.info["sfreq"]) - - # triage type and description - # defaults to type="Comment" and the full description - etype = "Comment" - description = annot["description"] - for start in ["Stimulus/S", "Response/R", "Comment/"]: - if description.startswith(start): - etype = start.split("/")[0] - description = description.replace(start, "") - break - - if etype in ["Stimulus", "Response"] and description.strip().isdigit(): - description = int(description.strip()) - else: - # if cannot convert to int, we must use this as "Comment" - etype = "Comment" - - event_dict = dict( - onset=onset, # in samples - duration=duration, # in samples - description=description, - type=etype, - ) - - if "ch_names" in annot: - # handle channels - channels = list(annot["ch_names"]) - event_dict["channels"] = channels - - # add a "pybv" event - events += [event_dict] - - return events diff --git a/pybv/tests/test_export.py b/pybv/tests/test_export.py deleted file mode 100644 index f9b5ec0..0000000 --- a/pybv/tests/test_export.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Test export from software packages to BrainVision via pybv.""" - -import mne -import numpy as np -import pytest -from mne.io.constants import FIFF - -from pybv._export import _export_mne_raw - - -@pytest.mark.filterwarnings("ignore:.*non-voltage units.*n/a:UserWarning:pybv") -def test_export_mne_raw(tmpdir): - """Test mne export.""" - # Create a Raw object - sfreq = 250.0 - - ch_names = ["Fp1", "Fp2", "Fz", "Cz", "Pz", "O1", "O2", "analog", "temp"] - ch_types = ["eeg"] * (len(ch_names) - 2) + ["misc"] * 2 - - info = mne.create_info(ch_names, ch_types=ch_types, sfreq=sfreq) - - info.set_montage("standard_1020") - - data = np.random.randn(len(ch_names), int(sfreq * 100)) - - raw = mne.io.RawArray(data, info) - - # Make a fake channel in °C - raw.info["chs"][-1]["unit"] = FIFF.FIFF_UNIT_CEL - - annots = mne.Annotations( - onset=[3, 13, 30, 70, 90], # seconds - duration=[1, 1, 0.5, 0.25, 9], # seconds - description=[ - "Stimulus/S 1", - "Stimulus/S2.50", - "Response/R101", - "Look at this", - "Comment/And at this", - ], - ch_names=[(), (), (), ("Fp1",), ("Fp1", "Fp2")], - ) - raw.set_annotations(annots) - - # export to BrainVision - fname = tmpdir / "mne_export.vhdr" - with pytest.warns(RuntimeWarning, match="'double' .* Converting to float32"): - _export_mne_raw(raw=raw, fname=fname) - - with pytest.raises(ValueError, match="`fname` must have the '.vhdr'"): - _export_mne_raw(raw=raw, fname=str(fname).replace(".vhdr", ".eeg.tar.gz")) - - # test overwrite - with pytest.warns(): - _export_mne_raw(raw=raw, fname=fname, overwrite=True) - - # try once more with "single" data and mne events - fname = tmpdir / "mne_export_events.vhdr" - raw = mne.io.RawArray(data.astype(np.single), info) - raw.orig_format = "single" - events = np.vstack( - [ - np.linspace(0, sfreq * 00, 10).astype(int), - np.zeros(10).astype(int), - np.arange(1, 11).astype(int), - ] - ).T - _export_mne_raw(raw=raw, fname=fname, events=events) - raw_read = mne.io.read_raw_brainvision(fname) - np.testing.assert_allclose(raw_read.get_data(), raw.get_data()) diff --git a/pybv/tests/test_bv_writer.py b/pybv/tests/test_write_brainvision.py similarity index 100% rename from pybv/tests/test_bv_writer.py rename to pybv/tests/test_write_brainvision.py diff --git a/pyproject.toml b/pyproject.toml index 2e412a6..4d6e83a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,13 +38,9 @@ classifiers = [ ] [project.optional-dependencies] -full = [ - "mne >= 1.0", -] - dev = [ - "pybv[full]", "check-manifest", + "mne", "numpydoc", "pre-commit", "pytest",