Skip to content
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

Correctly load marker streams from .xdf files #464

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
### 🗑️ Removed
- Remove Python 3.9 support ([#457](https://github.com/cbrnr/mnelab/pull/457) by [Clemens Brunner](https://github.com/cbrnr))

### 🔧 Fixed
### Fixed
- Fix an issue where some .xdf files were not loaded correctly (wrong dtype or multi-channel marker) ([#464](https://github.com/cbrnr/mnelab/pull/464) by [Benedikt Klöckl](https://github.com/bkloeckl))
- Fix a bug where appending data would not be correctly displayed in the history ([#446](https://github.com/cbrnr/mnelab/pull/446) by [Benedikt Klöckl](https://github.com/bkloeckl))
- Fix resetting the settings to default values ([#456](https://github.com/cbrnr/mnelab/pull/456) by [Clemens Brunner](https://github.com/cbrnr))
- Fix an issue where the channel montage figure could not be closed on macOS ([#459](https://github.com/cbrnr/mnelab/pull/459) by [Benedikt Klöckl](https://github.com/bkloeckl))
Expand Down
10 changes: 7 additions & 3 deletions src/mnelab/dialogs/xdf_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def selected_streams(self):
for row in self.view.selectionModel().selectedRows():
type_ = self.view.item(row.row(), 2).text()
fs = self.view.item(row.row(), 5).value()
if type_ != "Markers" and fs != 0:
if not _is_marker(type_) and fs != 0:
streams.append(self.view.item(row.row(), 0).value())
return streams

Expand All @@ -160,7 +160,11 @@ def selected_markers(self):
markers = []
for row in self.view.selectionModel().selectedRows():
type_ = self.view.item(row.row(), 2).text()
fs = self.view.item(row.row(), 5).value()
if type_ == "Markers" and fs == 0:
if _is_marker(type_):
markers.append(self.view.item(row.row(), 0).value())
return markers


def _is_marker(type):
marker_types = ["Markers", "Marker", "StringMarker"]
return type in marker_types
52 changes: 35 additions & 17 deletions src/mnelab/io/xdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(
Resampling target frequency in Hz. If only one stream_id is given, this can
be `None`, in which case no resampling is performed.
"""

if len(stream_ids) > 1 and fs_new is None:
raise ValueError(
"Argument `fs_new` is required when reading multiple streams."
Expand All @@ -42,7 +43,7 @@ def __init__(
streams, _ = load_xdf(fname)
streams = {stream["info"]["stream_id"]: stream for stream in streams}

if all(_is_markerstream(streams[stream_id]) for stream_id in stream_ids):
if (not stream_ids) and marker_ids:
raise RuntimeError(
"Loading only marker streams is not supported, at least one stream must"
" be a regular stream."
Expand All @@ -53,6 +54,23 @@ def __init__(
for stream_id in stream_ids:
stream = streams[stream_id]

# check if the dtype is valid, try convertion otherwise
dtype = stream["time_series"].dtype
if dtype not in [np.float64, np.complex128]:
try:
stream["time_series"] = stream["time_series"].astype(np.float64)
except ValueError:
try:
stream["time_series"] = stream["time_series"].astype(
np.complex128
)
except ValueError as e:
raise RuntimeError(
f"Stream {stream['info']['name']} has unsupported"
" dtype {dtype}. "
f"Conversion to float64 and complex128 failed."
) from e

n_chans = int(stream["info"]["channel_count"][0])
labels, types, units = [], [], []
try:
Expand Down Expand Up @@ -92,17 +110,23 @@ def __init__(
super().__init__(preload=data, info=info, filenames=[fname])

# convert marker streams to annotations
for stream_id, stream in streams.items():
if marker_ids is not None and stream_id not in marker_ids:
continue
if not _is_markerstream(stream):
continue
for marker_id in marker_ids:
stream = streams[marker_id]
channel_count = int(stream["info"]["channel_count"][0])
onsets = stream["time_stamps"] - first_time
prefix = f"{stream_id}-" if prefix_markers else ""
descriptions = [
f"{prefix}{item}" for sub in stream["time_series"] for item in sub
]
self.annotations.append(onsets, [0] * len(onsets), descriptions)
prefix = f"{marker_id}-" if prefix_markers else ""

if channel_count == 1:
# handle single-channel markers
descriptions = [f"{prefix}{item[0]}" for item in stream["time_series"]]
self.annotations.append(onsets, [0] * len(onsets), descriptions)
else:
# handle multi-channel markers
for sample in stream["time_series"]:
for _, description in enumerate(sample):
self.annotations.append(
onsets, [0] * len(onsets), str(description)
)


def _resample_streams(streams, stream_ids, fs_new):
Expand Down Expand Up @@ -196,12 +220,6 @@ def read_raw_xdf(
return RawXDF(fname, stream_ids, marker_ids, prefix_markers, fs_new)


def _is_markerstream(stream):
srate = float(stream["info"]["nominal_srate"][0])
n_chans = int(stream["info"]["channel_count"][0])
return srate == 0 and n_chans == 1


def get_xml(fname):
"""Get XML stream headers and footers from all streams.

Expand Down