11from typing import List
22import os
3+ import warnings
4+ from dataclasses import dataclass
35
46import mne
57import numpy as np
68from pyxdf .pyxdf import StreamData
79
10+ @dataclass
11+ class Markers ():
12+ timestamps : np .array
13+ durations : np .array
14+ descriptions : List [str ]
15+
16+ def as_mne_annotations (self , reference_time : float = 0.0 ):
17+ onset_times = np .array (self .timestamps ) - reference_time
18+ empty = mne .Annotations (onset = [], duration = [], description = [])
19+ if len (onset_times ) == 0 :
20+ return empty
21+
22+ i = 0
23+ while onset_times [i ] < 0 :
24+ i += 1
25+ if i == len (onset_times ):
26+ return empty
27+
28+ return mne .Annotations (onset = onset_times [i :], duration = self .durations [i :], description = self .descriptions [i :])
29+
30+ def as_mne_annotations_for_stream (self , stream : StreamData ):
31+ reference_time = stream ['time_stamps' ][0 ]
32+ return self .as_mne_annotations (reference_time )
33+
34+ def __add__ (self , other ):
35+ if not isinstance (other , Markers ):
36+ return NotImplemented
37+ timestamps = np .concatenate ([self .timestamps , self .timestamps ])
38+ durations = np .concatenate ([self .durations , self .durations ])
39+ descriptions = self .descriptions + other .descriptions
40+ return Markers (timestamps , durations , descriptions )
41+
842class XDFStream ():
943 stream : StreamData
1044 metadata_desc : dict
1145 raw : mne .io .RawArray | None
12- annotations : mne .Annotations | None
13- time_offset : float
46+ markers : mne .Annotations | None
1447
1548 @staticmethod
1649 def stream_type_to_mne_ch_type (stream_type : str , type_map : dict = None ):
@@ -64,12 +97,12 @@ def __init__(self, stream, mne_type_map: dict = None):
6497 self .mne_type_map = mne_type_map
6598 self .metadata_desc = {}
6699 self .raw = None
67- self . annotations = None
100+
68101 if self .stream ["info" ]["desc" ] and self .stream ["info" ]["desc" ][0 ]:
69102 self .metadata_desc = self .stream ["info" ]["desc" ][0 ]
70103
71- self . time_offset = float ( self .stream [ 'info' ][ 'created_at' ][ 0 ])
72-
104+ if self .is_markers_compatible :
105+ self . init_markers ()
73106
74107 @property
75108 def name (self ):
@@ -87,6 +120,10 @@ def id(self):
87120 def srate (self ):
88121 return float (self .stream ["info" ]["nominal_srate" ][0 ])
89122
123+ @property
124+ def is_markers_compatible (self ):
125+ return self .srate == 0.0
126+
90127 @property
91128 def is_mne_compatible (self ):
92129 return self .is_mne_raw_compatible or self .is_mne_annotations_compatible
@@ -99,7 +136,7 @@ def is_mne_raw_compatible(self):
99136
100137 @property
101138 def is_mne_annotations_compatible (self ):
102- return self .srate == 0.0
139+ return self .is_markers_compatible
103140
104141 @property
105142 def time_series (self ):
@@ -143,27 +180,16 @@ def ch_names(self):
143180 def ch_types (self ):
144181 return XDFStream .get_mne_ch_types (self .type , self .ch_names , self .mne_type_map )
145182
146- def create_mne_annotations (self ):
147- if self .type == 'Markers' :
148- timestamps = self .stream ['time_stamps' ] # Time stamps of the markers
149- descriptions = self .stream ['time_series' ] # Marker descriptions (the event labels)
150-
151- # Ensure timestamps are in seconds (if they aren't already)
152- timestamps = np .array (timestamps ) - self .time_offset
153- onset_times = timestamps
154- duration = np .zeros_like (onset_times )
183+ def init_markers (self ):
184+ assert self .is_markers_compatible
155185
156- # Create the description for each marker (can be customized, here we use the marker text)
157- description = [str (desc [0 ]) for desc in descriptions ] # Flatten description if necessary
186+ timestamps = np .array (self .stream ['time_stamps' ])
187+ descriptions = np .array (self .stream ['time_series' ])
188+ durations = np .zeros_like (timestamps )
158189
159- # Create the annotations object
160- self .annotations = mne .Annotations (onset = onset_times , duration = duration , description = description )
161- # TODO debug time stamps
162- # print("AAAAAAAAAAAAAAAAAAAAAAAAAaaa")
163- # print(onset_times)
190+ descriptions = [str (desc [0 ]) for desc in descriptions ] # Flatten description
164191
165- else :
166- self .markers = None
192+ self .markers = Markers (timestamps , durations , descriptions )
167193
168194 def get_unique_name (self , append_stream_id : bool = False ):
169195 unique_stream_name = self .name
@@ -172,15 +198,17 @@ def get_unique_name(self, append_stream_id: bool = False):
172198 return unique_stream_name
173199
174200
175- def convert_to_mne (self , scale : float | str | None , append_stream_id : bool = False , verbose : bool = False ):
201+ def convert_to_mne (
202+ self ,
203+ scale : float | str | None ,
204+ append_stream_id : bool = False ,
205+ verbose : bool = False
206+ ):
176207 self .unique_name = self .get_unique_name (append_stream_id = append_stream_id )
177208 if self .is_mne_raw_compatible :
178209 mne_info = self .create_mne_info (verbose = verbose )
179210 self .raw = self .create_mne_raw (mne_info , scale , verbose = verbose )
180211
181- if self .is_mne_annotations_compatible :
182- self .create_mne_annotations ()
183-
184212 def create_mne_info (self , verbose : bool = False ):
185213 """
186214 Create a mne.info object from the XDF's EEG stream metadata.
0 commit comments