bci_essentials.io.xdf_sources
1import pyxdf 2 3from .sources import EegSource, MarkerSource 4from ..utils.logger import Logger # Logger wrapper 5 6# Instantiate a logger for the module at the default level of logging.INFO 7# Logs to bci_essentials.__module__) where __module__ is the name of the module 8logger = Logger(name=__name__) 9 10__all__ = ["XdfMarkerSource", "XdfEegSource"] 11 12 13class XdfMarkerSource(MarkerSource): 14 def __init__(self, filename: str): 15 """Create a MarkerSource object that obtains markers from an XDF file. 16 17 Parameters 18 ---------- 19 filename : str 20 The full name of file, including path. If file isn't found, an Exception is raised. 21 """ 22 samples, timestamps, info = load_xdf_stream(filename, "LSL_Marker_Strings") 23 self.__samples = samples 24 self.__timestamps = timestamps 25 self.__info = info 26 27 @property 28 def name(self) -> str: 29 return self.__info["name"][0] 30 31 def get_markers(self) -> tuple[list[list], list]: 32 """Read markers and related timestamps from the XDF file. 33 34 Returns 35 ------- 36 markers_from_xdf : tuple[markers, timestamps] 37 A tuple of (markers, timestamps). The contents of the file on the first call to get_markers(). 38 - Returns empty lists [[],[]] thereafter. 39 """ 40 # return all data on first get 41 samples = self.__samples 42 timestamps = self.__timestamps 43 44 # reset to empty for next get 45 self.__samples = [[]] 46 self.__timestamps = [] 47 48 return [samples, timestamps] 49 50 def time_correction(self) -> float: 51 return 0.0 52 53 54class XdfEegSource(EegSource): 55 """Create a MarkerSource object that obtains EEG from an XDF file 56 57 Parameters 58 ---------- 59 filename : str 60 The full name of file, including path. If file isn't found, an Exception is raised. 61 """ 62 63 def __init__(self, filename: str): 64 samples, timestamps, info = load_xdf_stream(filename, "EEG") 65 self.__samples = samples 66 self.__timestamps = timestamps 67 self.__info = info 68 69 @property 70 def name(self) -> str: 71 return self.__info["name"][0] 72 73 @property 74 def fsample(self) -> float: 75 return float(self.__info["nominal_srate"][0]) 76 77 @property 78 def n_channels(self) -> int: 79 return int(self.__info["channel_count"][0]) 80 81 @property 82 def channel_types(self) -> list[str]: 83 types = [] 84 try: 85 description = self.__info["desc"][0]["channels"][0]["channel"] 86 types = [channel["type"][0] for channel in description] 87 types = [str.lower(type) for type in types] 88 except Exception: 89 pass 90 91 return types 92 93 @property 94 def channel_units(self) -> list[str]: 95 units = [] 96 try: 97 description = self.__info["desc"][0]["channels"][0]["channel"] 98 units = [channel["unit"][0] for channel in description] 99 except Exception: 100 pass 101 return units 102 103 @property 104 def channel_labels(self) -> list[str]: 105 labels = [] 106 try: 107 description = self.__info["desc"][0]["channels"][0]["channel"] 108 labels = [channel["label"][0] for channel in description] 109 except Exception: 110 pass 111 return labels 112 113 def get_samples(self) -> tuple[list[list], list]: 114 """Read markers and related timestamps from the XDF file. Returns the contents of the file 115 on the first call to get_markers(), returns empty lists thereafter. 116 """ 117 # return all data on first get 118 samples = self.__samples 119 timestamps = self.__timestamps 120 121 # reset to empty for next get 122 self.__samples = [[]] 123 self.__timestamps = [] 124 125 return [samples, timestamps] 126 127 def time_correction(self) -> float: 128 return 0.0 129 130 131def load_xdf_stream(filepath: str, streamtype: str) -> tuple[list, list, list]: 132 """A helper function to load the contents of the XDF file and return stream data, timestamps and info""" 133 # Don't need the header returned by load_xdf() 134 streams, _ = pyxdf.load_xdf(filepath) 135 136 samples = [] 137 timestamps = [] 138 info = [] 139 140 for i in range(len(streams)): 141 stream = streams[i] 142 info = stream["info"] 143 type = info["type"][0] 144 if type == streamtype: 145 try: 146 # Check if the stream is empty 147 if len(stream["time_series"]) == 0: 148 logger.warning( 149 "%s stream is empty, there may be multiple streams including ghost streams so maybe this is okay", 150 streamtype, 151 ) 152 continue 153 154 samples = stream["time_series"] 155 timestamps = stream["time_stamps"] 156 157 except Exception: 158 logger.error("%s data not available", streamtype) 159 160 break 161 162 return (samples, timestamps, info)
14class XdfMarkerSource(MarkerSource): 15 def __init__(self, filename: str): 16 """Create a MarkerSource object that obtains markers from an XDF file. 17 18 Parameters 19 ---------- 20 filename : str 21 The full name of file, including path. If file isn't found, an Exception is raised. 22 """ 23 samples, timestamps, info = load_xdf_stream(filename, "LSL_Marker_Strings") 24 self.__samples = samples 25 self.__timestamps = timestamps 26 self.__info = info 27 28 @property 29 def name(self) -> str: 30 return self.__info["name"][0] 31 32 def get_markers(self) -> tuple[list[list], list]: 33 """Read markers and related timestamps from the XDF file. 34 35 Returns 36 ------- 37 markers_from_xdf : tuple[markers, timestamps] 38 A tuple of (markers, timestamps). The contents of the file on the first call to get_markers(). 39 - Returns empty lists [[],[]] thereafter. 40 """ 41 # return all data on first get 42 samples = self.__samples 43 timestamps = self.__timestamps 44 45 # reset to empty for next get 46 self.__samples = [[]] 47 self.__timestamps = [] 48 49 return [samples, timestamps] 50 51 def time_correction(self) -> float: 52 return 0.0
MarkerSource objects send time synchronized markers/commands to bci_controller.
XdfMarkerSource(filename: str)
15 def __init__(self, filename: str): 16 """Create a MarkerSource object that obtains markers from an XDF file. 17 18 Parameters 19 ---------- 20 filename : str 21 The full name of file, including path. If file isn't found, an Exception is raised. 22 """ 23 samples, timestamps, info = load_xdf_stream(filename, "LSL_Marker_Strings") 24 self.__samples = samples 25 self.__timestamps = timestamps 26 self.__info = info
Create a MarkerSource object that obtains markers from an XDF file.
Parameters
- filename (str): The full name of file, including path. If file isn't found, an Exception is raised.
def
get_markers(self) -> tuple[list[list], list]:
32 def get_markers(self) -> tuple[list[list], list]: 33 """Read markers and related timestamps from the XDF file. 34 35 Returns 36 ------- 37 markers_from_xdf : tuple[markers, timestamps] 38 A tuple of (markers, timestamps). The contents of the file on the first call to get_markers(). 39 - Returns empty lists [[],[]] thereafter. 40 """ 41 # return all data on first get 42 samples = self.__samples 43 timestamps = self.__timestamps 44 45 # reset to empty for next get 46 self.__samples = [[]] 47 self.__timestamps = [] 48 49 return [samples, timestamps]
Read markers and related timestamps from the XDF file.
Returns
- markers_from_xdf (tuple[markers, timestamps]): A tuple of (markers, timestamps). The contents of the file on the first call to get_markers(). - Returns empty lists [[],[]] thereafter.
def
time_correction(self) -> float:
Get the current time correction for timestamps.
Returns
- time_correction (float):
The current time correction estimate (seconds).
- This is the number that needs to be added to a time tamp that was remotely generated via local_clock() to map it into the local clock domain of the machine.
55class XdfEegSource(EegSource): 56 """Create a MarkerSource object that obtains EEG from an XDF file 57 58 Parameters 59 ---------- 60 filename : str 61 The full name of file, including path. If file isn't found, an Exception is raised. 62 """ 63 64 def __init__(self, filename: str): 65 samples, timestamps, info = load_xdf_stream(filename, "EEG") 66 self.__samples = samples 67 self.__timestamps = timestamps 68 self.__info = info 69 70 @property 71 def name(self) -> str: 72 return self.__info["name"][0] 73 74 @property 75 def fsample(self) -> float: 76 return float(self.__info["nominal_srate"][0]) 77 78 @property 79 def n_channels(self) -> int: 80 return int(self.__info["channel_count"][0]) 81 82 @property 83 def channel_types(self) -> list[str]: 84 types = [] 85 try: 86 description = self.__info["desc"][0]["channels"][0]["channel"] 87 types = [channel["type"][0] for channel in description] 88 types = [str.lower(type) for type in types] 89 except Exception: 90 pass 91 92 return types 93 94 @property 95 def channel_units(self) -> list[str]: 96 units = [] 97 try: 98 description = self.__info["desc"][0]["channels"][0]["channel"] 99 units = [channel["unit"][0] for channel in description] 100 except Exception: 101 pass 102 return units 103 104 @property 105 def channel_labels(self) -> list[str]: 106 labels = [] 107 try: 108 description = self.__info["desc"][0]["channels"][0]["channel"] 109 labels = [channel["label"][0] for channel in description] 110 except Exception: 111 pass 112 return labels 113 114 def get_samples(self) -> tuple[list[list], list]: 115 """Read markers and related timestamps from the XDF file. Returns the contents of the file 116 on the first call to get_markers(), returns empty lists thereafter. 117 """ 118 # return all data on first get 119 samples = self.__samples 120 timestamps = self.__timestamps 121 122 # reset to empty for next get 123 self.__samples = [[]] 124 self.__timestamps = [] 125 126 return [samples, timestamps] 127 128 def time_correction(self) -> float: 129 return 0.0
Create a MarkerSource object that obtains EEG from an XDF file
Parameters
- filename (str): The full name of file, including path. If file isn't found, an Exception is raised.
channel_types: list[str]
82 @property 83 def channel_types(self) -> list[str]: 84 types = [] 85 try: 86 description = self.__info["desc"][0]["channels"][0]["channel"] 87 types = [channel["type"][0] for channel in description] 88 types = [str.lower(type) for type in types] 89 except Exception: 90 pass 91 92 return types
The type of each channel, ex: eeg, or stim
channel_units: list[str]
94 @property 95 def channel_units(self) -> list[str]: 96 units = [] 97 try: 98 description = self.__info["desc"][0]["channels"][0]["channel"] 99 units = [channel["unit"][0] for channel in description] 100 except Exception: 101 pass 102 return units
The unit of each channel, ex: microvolts
channel_labels: list[str]
104 @property 105 def channel_labels(self) -> list[str]: 106 labels = [] 107 try: 108 description = self.__info["desc"][0]["channels"][0]["channel"] 109 labels = [channel["label"][0] for channel in description] 110 except Exception: 111 pass 112 return labels
The label for each channel, ex: FC3, C5
def
get_samples(self) -> tuple[list[list], list]:
114 def get_samples(self) -> tuple[list[list], list]: 115 """Read markers and related timestamps from the XDF file. Returns the contents of the file 116 on the first call to get_markers(), returns empty lists thereafter. 117 """ 118 # return all data on first get 119 samples = self.__samples 120 timestamps = self.__timestamps 121 122 # reset to empty for next get 123 self.__samples = [[]] 124 self.__timestamps = [] 125 126 return [samples, timestamps]
Read markers and related timestamps from the XDF file. Returns the contents of the file on the first call to get_markers(), returns empty lists thereafter.
def
time_correction(self) -> float:
Get the current time correction for timestamps.
Returns
- time_correction (float):
The current time correction estimate (seconds).
- This is the number that needs to be added to a time tamp that was remotely generated via local_clock() to map it into the local clock domain of the machine.