Skip to content

Commit 93412ce

Browse files
authored
Merge branch 'master' into feature/wavelets
2 parents 4eec60c + 71b4aa8 commit 93412ce

File tree

10 files changed

+2632
-962
lines changed

10 files changed

+2632
-962
lines changed

data/XDF/dyad-example-noise.xdf

1.33 MB
Binary file not shown.
222 KB
Binary file not shown.

hypyp/xdf/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .xdf_import import XDFImport
2+
from .xdf_stream import XDFStream
3+
4+
__all__ = ["XDFImport", "XDFStream"]
5+

hypyp/xdf/xdf_import.py

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
from typing import List, Dict
2+
import warnings
3+
4+
import pyxdf
5+
import numpy as np
6+
7+
from .xdf_stream import XDFStream, Markers
8+
9+
class XDFImport():
10+
"""
11+
Read an XDF file and (optionally) create mne Raws and Annotations
12+
13+
Parameters:
14+
file_path: Path to XDF file (LSL data recorded with LabRecorder). Can be absolute or relative.
15+
select_type: Define which type of stream the user is looking to convert. Should match the stream type in LSL
16+
select_matches: List of the stream index(es) in the XDF the user wishes to convert (can be `str` which the class will try to match to the name of an existing stream or an `int` which will be interpreted as such)
17+
mne_type_map: Dict to map stream types to mne channel types
18+
scale: Scaling factor or 'auto' for automatic scaling, None for no scaling.
19+
convert_to_mne: Flag to disable the automatic conversion to mne Raws and Annotations
20+
verbose: Verbose flag
21+
"""
22+
23+
file_path: str
24+
mne_type_map: dict | None
25+
selected_stream_indices: List[int]
26+
map_id_to_idx: dict # Stream identifier to the index in our list
27+
verbose: bool
28+
29+
def __init__(self,
30+
file_path: str,
31+
select_type: str = None,
32+
select_matches: list = None,
33+
mne_type_map: dict = None,
34+
scale: float | str | None = None,
35+
verbose: bool = False,
36+
convert_to_mne: bool = True,
37+
):
38+
39+
self.file_path = file_path
40+
self.scale = scale
41+
42+
self.selected_stream_indices = []
43+
self.map_id_to_idx = {}
44+
45+
self.verbose = verbose
46+
47+
# Load file
48+
streams, self.header = pyxdf.load_xdf(file_path, verbose=(None if verbose == False else verbose))
49+
50+
self.available_streams = [XDFStream(stream, mne_type_map=mne_type_map) for stream in streams]
51+
52+
self.init_map_streams()
53+
54+
# Prepare the "selected_streams" list
55+
if select_matches is not None:
56+
self.select_streams_by_matches(select_matches)
57+
elif select_type is not None:
58+
self.select_streams_by_type(select_type)
59+
else:
60+
self.select_all_streams()
61+
62+
# Assert that we have found at least one real stream of selected type
63+
if len(self.selected_stream_indices) == 0:
64+
raise ValueError(f"No stream selected. select_type: {select_type}, select_matches: {select_matches}")
65+
66+
if verbose:
67+
print(self)
68+
69+
# Create MNE objects
70+
if convert_to_mne:
71+
self.convert_streams_to_mne()
72+
73+
@property
74+
def selected_streams(self):
75+
"""List of selected streams, given the select_match or select_type"""
76+
return [self.available_streams[idx] for idx in self.selected_stream_indices]
77+
78+
@property
79+
def selected_signal_streams(self):
80+
"""List of selected streams that are signal streams (srate>0), given the select_match or select_type"""
81+
return [self.available_streams[idx] for idx in self.selected_stream_indices if self.available_streams[idx].is_mne_raw_compatible]
82+
83+
@property
84+
def selected_markers_streams(self):
85+
"""List of selected streams that are markers streams (srate==0), given the select_match or select_type"""
86+
return [self.available_streams[idx] for idx in self.selected_stream_indices if self.available_streams[idx].is_mne_annotations_compatible]
87+
88+
@property
89+
def mne_raws_dict(self):
90+
"""Dictionary of mne.io.RawArray object for selected streams, by stream name"""
91+
ret = dict()
92+
for stream in self.selected_signal_streams:
93+
ret[stream.name] = stream.mne_raw
94+
return ret
95+
96+
@property
97+
def mne_raws(self):
98+
"""List of mne.io.RawArray object for selected streams"""
99+
return [stream.mne_raw for stream in self.selected_signal_streams]
100+
101+
@property
102+
def markers_dict(self) -> Dict[str, Markers]:
103+
"""
104+
Dictionary of Markers object from selected streams that are not signals (srate==0), by stream name
105+
106+
Markers can be converted to mne.Annotations for signal streams
107+
"""
108+
ret = dict()
109+
for stream in self.selected_markers_streams:
110+
ret[stream.name] = stream.markers
111+
return ret
112+
113+
@property
114+
def markers(self) -> Markers:
115+
"""
116+
Merge list of all Markers from selected streams that are not signals (srate==0)
117+
118+
Markers can be converted to mne.Annotations for signal streams
119+
"""
120+
markers = None
121+
for stream in self.selected_markers_streams:
122+
if markers is None:
123+
markers = stream.markers
124+
else:
125+
markers += stream.markers
126+
return markers
127+
128+
def get_stream_indices_for_type(self, stream_type: str):
129+
"""For a given type of stream, get the list of index in the available streams which are of this type"""
130+
return [idx for idx, stream in enumerate(self.available_streams) if stream.type == stream_type]
131+
132+
def get_stream_ids_for_type(self, stream_type: str):
133+
"""For a given type of stream, get the XDF identifiers in the available streams which are of this type"""
134+
return [stream.id for stream in self.available_streams if stream.type == stream_type]
135+
136+
def init_map_streams(self):
137+
"""
138+
Create mapping between stream indentifiers and indices in our available_streams list
139+
140+
'id' is the stream identifier
141+
142+
'idx' is the index of the stream in our list "available_streams"
143+
"""
144+
for idx, stream in enumerate(self.available_streams):
145+
self.map_id_to_idx[stream.id] = idx
146+
147+
148+
def select_all_streams(self) -> list:
149+
"""
150+
Find in the available streams loaded from the XDF file all the streams that can be converted to MNE
151+
152+
Subsequent calls to class methods will only apply to the selected streams
153+
"""
154+
self.selected_stream_indices = [idx for idx, stream in enumerate(self.available_streams) if stream.is_mne_compatible]
155+
156+
157+
def select_streams_by_type(self, stream_type: str) -> list:
158+
"""
159+
Find in the available streams loaded from the XDF file the streams that are of a specific type.
160+
161+
Subsequent calls to class methods will only apply to the selected streams
162+
163+
Parameters:
164+
type: The string (e.g., "EEG", "video") that will be matched to XDF stream's `type` to find their indexes.
165+
"""
166+
167+
if self.verbose:
168+
print(f"Looking for streams of type '{stream_type}'")
169+
170+
self.selected_stream_indices = self.get_stream_indices_for_type(stream_type)
171+
172+
def select_streams_by_matches(self, keyword_matches: list):
173+
"""
174+
Interpret the query made by the user (a list of indexes, or `str` that matches
175+
streams' name) into a list containing the indexes within the XDF file.
176+
177+
Parameters:
178+
idx: List containing the index that the user is trying to convert.
179+
"""
180+
for keyword_match in keyword_matches:
181+
# match stream_id
182+
found_idx = None
183+
if type(keyword_match) == int:
184+
stream_id = keyword_match
185+
found_idx = self.map_id_to_idx[stream_id]
186+
187+
# match stream name
188+
else:
189+
for idx, stream in enumerate(self.available_streams):
190+
if stream.name == keyword_match:
191+
found_idx = idx
192+
193+
if found_idx is None:
194+
raise ValueError(f"No stream matching keyword '{keyword_match}'")
195+
196+
self.selected_stream_indices.append(found_idx)
197+
198+
def convert_streams_to_mne(self):
199+
"""
200+
Create mne.io.RawArray objects from every signal streams, and add all markers as mne.Annotations to the mne.io.RawArray objects
201+
202+
mne.io.RawArray objects are then available using obj.mne_raws or obj.mne_raws_dict
203+
"""
204+
205+
# Find if all the stream have unique names (true if any stream name is duplicated)
206+
names = [stream.name for stream in self.selected_streams]
207+
208+
for stream in self.selected_streams:
209+
if self.verbose:
210+
print(f'Converting {stream.name} to MNE')
211+
212+
has_duplicate_names = names.count(stream.name) > 1
213+
if has_duplicate_names:
214+
warnings.warn("Multiple streams have the same name. Adding original stream_id as suffixes to the generated raws")
215+
216+
stream.convert_to_mne_raw(self.scale, append_stream_id=has_duplicate_names)
217+
218+
# add annotations from markers streams to data streams
219+
for data_stream in self.selected_signal_streams:
220+
if self.markers is not None:
221+
data_stream.mne_raw.set_annotations(self.markers.as_mne_annotations(data_stream.reference_time))
222+
223+
if self.verbose:
224+
print("All convertion to MNE done.")
225+
226+
def rename_channels(self, new_names):
227+
"""
228+
Set the name of all the channels for every signal streams. Useful when they were not correctly loaded (or not present) from the XDF file
229+
230+
Parameters:
231+
new_names (List[str]): The list of new names to set
232+
"""
233+
for stream in self.selected_signal_streams:
234+
stream.rename_channels(new_names)
235+
236+
def set_montage(self, montage):
237+
"""
238+
Set the montage of the raw(s) using a custom mne montage label, or the path to a dig.montage file.
239+
240+
Parameters:
241+
self: The instance of the class.
242+
montage: A path to a local Dig montage or a mne standard montage.
243+
"""
244+
if self.verbose or True:
245+
names = [stream.name for stream in self.selected_signal_streams]
246+
print(f"Setting '{montage}' as the montage for streams: {','.join(names)}")
247+
248+
249+
for stream in self.selected_signal_streams:
250+
try:
251+
stream.set_montage(montage)
252+
except ValueError as e:
253+
warnings.warn(f"Invalid montage given to mne.set_montage(): {montage}")
254+
raise e
255+
256+
def save_to_fif_files(self, dir_path):
257+
"""
258+
Save all the mne.io.RawArray objects as .fif files
259+
260+
Parameters:
261+
dir_path (str): Relative or absolute path of the folder where to save the .fif files
262+
263+
Returns:
264+
List[str]: The list of file names that have been created
265+
"""
266+
return [stream.save_to_fif_file(dir_path) for stream in self.selected_signal_streams]
267+
268+
def __str__(self):
269+
available_streams_str = "\n ".join([str(stream) for stream in self.available_streams])
270+
selected_streams_str = ",".join([stream.name for stream in self.selected_streams])
271+
return f"""XDFImport with {len(self.available_streams)} available streams and {len(self.selected_streams)} selected streams (loaded from {self.file_path})
272+
Available streams:
273+
{available_streams_str}
274+
Selected streams: [{selected_streams_str}]
275+
"""
276+

0 commit comments

Comments
 (0)