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