88
99class XDFImport ():
1010 """
11- Read an XDF file and enable to export stream in a convenient format (e.g., an EEG stream into an mne.Raw instance).
11+ Read an XDF file and (optionally) create mne Raws and Annotations
1212
13- Arguments :
13+ Parameters :
1414 file_path: Path to XDF file (LSL data recorded with LabRecorder). Can be absolute or relative.
15- stream_type : Define which type of stream the user is looking to convert.
16- stream_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). Do not set to convert all of the request type
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)
1717 mne_type_map: Dict to map stream types to mne channel types
1818 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
1921 """
2022
2123 file_path : str
2224 mne_type_map : dict | None
2325 selected_stream_indices : List [int ]
24- map_id_to_idx : dict
26+ map_id_to_idx : dict # Stream identifier to the index in our list
2527 verbose : bool
2628
2729 def __init__ (self ,
@@ -32,7 +34,7 @@ def __init__(self,
3234 scale : float | str | None = None ,
3335 verbose : bool = False ,
3436 convert_to_mne : bool = True ,
35- ):
37+ ):
3638
3739 self .file_path = file_path
3840 self .scale = scale
@@ -47,13 +49,19 @@ def __init__(self,
4749
4850 self .available_streams = [XDFStream (stream , mne_type_map = mne_type_map ) for stream in streams ]
4951
50- self .map_streams ()
52+ self .init_map_streams ()
5153
5254 # Prepare the "selected_streams" list
5355 if select_matches is not None :
5456 self .select_streams_by_matches (select_matches )
55- else :
57+ elif select_type is not None :
5658 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 } " )
5765
5866 if verbose :
5967 print (self )
@@ -63,25 +71,52 @@ def __init__(self,
6371 self .convert_streams_to_mne ()
6472
6573 @property
66- def raws_dict (self ):
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"""
6791 ret = dict ()
68- for stream in self .selected_data_streams :
69- ret [stream .name ] = stream .raw
92+ for stream in self .selected_signal_streams :
93+ ret [stream .name ] = stream .mne_raw
7094 return ret
7195
7296 @property
73- def raws (self ):
74- return [stream .raw for stream in self .selected_data_streams ]
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 ]
75100
76101 @property
77102 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+ """
78108 ret = dict ()
79109 for stream in self .selected_markers_streams :
80110 ret [stream .name ] = stream .markers
81111 return ret
82112
83113 @property
84114 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+ """
85120 markers = None
86121 for stream in self .selected_markers_streams :
87122 if markers is None :
@@ -90,75 +125,56 @@ def markers(self) -> Markers:
90125 markers += stream .markers
91126 return markers
92127
93- @property
94- def selected_streams (self ):
95- return [self .available_streams [idx ] for idx in self .selected_stream_indices ]
96-
97- @property
98- def selected_data_streams (self ):
99- return [self .available_streams [idx ] for idx in self .selected_stream_indices if self .available_streams [idx ].is_mne_raw_compatible ]
100-
101- @property
102- def selected_markers_streams (self ):
103- return [self .available_streams [idx ] for idx in self .selected_stream_indices if self .available_streams [idx ].is_mne_annotations_compatible ]
104-
105- @property
106- def selected_stream_names (self ):
107- return [stream .name for stream in self .selected_streams ]
108-
109- @property
110- def selected_data_stream_names (self ):
111- return [stream .name for stream in self .selected_data_streams ]
112-
113- @property
114- def selected_markers_stream_names (self ):
115- return [stream .name for stream in self .selected_markers_streams ]
116-
117- def get_streams_for_type (self , stream_type : str ):
118- return [stream for stream in self .available_streams if stream .type == stream_type ]
119-
120128 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"""
121130 return [idx for idx , stream in enumerate (self .available_streams ) if stream .type == stream_type ]
122131
123132 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"""
124134 return [stream .id for stream in self .available_streams if stream .type == stream_type ]
125135
126- def map_streams (self ):
127- # create mapping between stream indentifiers and indices in our available_streams list
128- # id is the stream identifier
129- # idx is the index of the stream in our list "available_streams"
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+ """
130144 for idx , stream in enumerate (self .available_streams ):
131145 self .map_id_to_idx [stream .id ] = idx
132146
133147
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+
134157 def select_streams_by_type (self , stream_type : str ) -> list :
135158 """
136- Read the XDF file to find & store the XDF stream's indexes that match the `type` (e.g., "EEG").
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
137162
138- Arguments :
163+ Parameters :
139164 type: The string (e.g., "EEG", "video") that will be matched to XDF stream's `type` to find their indexes.
140165 """
141166
142167 if self .verbose :
143168 print (f"Looking for streams of type '{ stream_type } '" )
144169
145- if stream_type is not None :
146- self .selected_stream_indices = self .get_stream_indices_for_type (stream_type )
147- else :
148- # Use all the mne raw compatible
149- # TODO this if is complicated
150- self .selected_stream_indices = [idx for idx , stream in enumerate (self .available_streams ) if stream .is_mne_compatible ]
151-
152- # Assert that we have found at least one real stream of selected type
153- if len (self .selected_stream_indices ) == 0 :
154- raise ValueError (f"No stream of type '{ stream_type } ' were found in this XDF file" )
170+ self .selected_stream_indices = self .get_stream_indices_for_type (stream_type )
155171
156172 def select_streams_by_matches (self , keyword_matches : list ):
157173 """
158174 Interpret the query made by the user (a list of indexes, or `str` that matches
159175 streams' name) into a list containing the indexes within the XDF file.
160176
161- Arguments :
177+ Parameters :
162178 idx: List containing the index that the user is trying to convert.
163179 """
164180 for keyword_match in keyword_matches :
@@ -181,59 +197,74 @@ def select_streams_by_matches(self, keyword_matches: list):
181197
182198 def convert_streams_to_mne (self ):
183199 """
184- A function that centralizes the pipeline for creating a dictionary containing converted
185- XDF stream into `mne.Raw`.
200+ Create mne.io.RawArray objects from every signal streams, and add all markers as mne.Annotations to the mne.io.RawArray objects
186201
187- Note:
188- The returned dictionary has the name of the stream as a key and the `mne.Raw` object as the value.
202+ mne.io.RawArray objects are then available using obj.mne_raws or obj.mne_raws_dict
189203 """
190204
191205 # Find if all the stream have unique names (true if any stream name is duplicated)
192- names = self .selected_stream_names
206+ names = [ stream . name for stream in self .selected_streams ]
193207
194208 for stream in self .selected_streams :
195209 if self .verbose :
196- print (f'Converting { stream .name } ' )
210+ print (f'Converting { stream .name } to MNE ' )
197211
198212 has_duplicate_names = names .count (stream .name ) > 1
199213 if has_duplicate_names :
200214 warnings .warn ("Multiple streams have the same name. Adding original stream_id as suffixes to the generated raws" )
201215
202- stream .convert_to_mne (self .scale , append_stream_id = has_duplicate_names )
216+ stream .convert_to_mne_raw (self .scale , append_stream_id = has_duplicate_names )
203217
204218 # add annotations from markers streams to data streams
205- for data_stream in self .selected_data_streams :
219+ for data_stream in self .selected_signal_streams :
206220 if self .markers is not None :
207- data_stream .raw .set_annotations (self .markers .as_mne_annotations_for_stream (data_stream .stream ))
221+ data_stream .mne_raw .set_annotations (self .markers .as_mne_annotations (data_stream .reference_time ))
208222
209223 if self .verbose :
210- print ("Convertion done." )
224+ print ("All convertion to MNE done." )
211225
212- def save_to_fif_files (self , dir_path ):
213- return [stream .save_to_fif_file (dir_path ) for stream in self .selected_data_streams ]
214-
215226 def rename_channels (self , new_names ):
216- return [stream .rename_channels (new_names ) for stream in self .selected_data_streams ]
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 )
217235
218236 def set_montage (self , montage ):
219237 """
220238 Set the montage of the raw(s) using a custom mne montage label, or the path to a dig.montage file.
221239
222- Arguments :
240+ Parameters :
223241 self: The instance of the class.
224242 montage: A path to a local Dig montage or a mne standard montage.
225243 """
226244 if self .verbose or True :
227- print (f"Setting '{ montage } ' as the montage for streams: { ',' .join (self .selected_data_stream_names )} " )
245+ names = [stream .name for stream in self .selected_signal_streams ]
246+ print (f"Setting '{ montage } ' as the montage for streams: { ',' .join (names )} " )
228247
229248
230- for stream in self .selected_data_streams :
249+ for stream in self .selected_signal_streams :
231250 try :
232251 stream .set_montage (montage )
233252 except ValueError as e :
234253 warnings .warn (f"Invalid montage given to mne.set_montage(): { montage } " )
235254 raise e
236255
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+
237268 def __str__ (self ):
238269 available_streams_str = "\n " .join ([str (stream ) for stream in self .available_streams ])
239270 selected_streams_str = "," .join ([stream .name for stream in self .selected_streams ])
0 commit comments