2525import pathlib
2626import warnings
2727import platform
28+ import re
2829
29- from collections import namedtuple
3030from urllib .request import urlopen
3131from datetime import datetime
3232
@@ -54,6 +54,10 @@ class Plexon2RawIO(BaseRawIO):
5454 pl2_dll_file_path: str | Path | None, default: None
5555 The path to the necessary dll for loading pl2 files
5656 If None will find correct dll for architecture and if it does not exist will download it
57+ reading_attempts: int, default: 15
58+ Number of attempts to read the file before raising an error
59+ This opening process is somewhat unreliable and might fail occasionally. Adjust this higher
60+ if you encounter problems in opening the file.
5761
5862 Notes
5963 -----
@@ -89,7 +93,7 @@ class Plexon2RawIO(BaseRawIO):
8993 extensions = ["pl2" ]
9094 rawmode = "one-file"
9195
92- def __init__ (self , filename , pl2_dll_file_path = None ):
96+ def __init__ (self , filename , pl2_dll_file_path = None , reading_attempts = 15 ):
9397
9498 # signals, event and spiking data will be cached
9599 # cached signal data can be cleared using `clear_analogsignal_cache()()`
@@ -129,7 +133,6 @@ def __init__(self, filename, pl2_dll_file_path=None):
129133
130134 self .pl2reader = PyPL2FileReader (pl2_dll_file_path = pl2_dll_file_path )
131135
132- reading_attempts = 10
133136 for attempt in range (reading_attempts ):
134137 self .pl2reader .pl2_open_file (self .filename )
135138
@@ -153,50 +156,73 @@ def _parse_header(self):
153156 # Scanning sources and populating signal channels at the same time. Sources have to have
154157 # same sampling rate and number of samples to belong to one stream.
155158 signal_channels = []
156- source_characteristics = {}
157- Source = namedtuple ("Source" , "id name sampling_rate n_samples" )
158- for c in range (self .pl2reader .pl2_file_info .m_TotalNumberOfAnalogChannels ):
159- achannel_info = self .pl2reader .pl2_get_analog_channel_info (c )
159+ channel_num_samples = []
160+
161+ # We will build the stream ids based on the channel prefixes
162+ # The channel prefixes are the first characters of the channel names which have the following format:
163+ # WB{number}, FPX{number}, SPKCX{number}, AI{number}, etc
164+ # We will extract the prefix and use it as stream id
165+ regex_prefix_pattern = r"^\D+" # Match any non-digit character at the beginning of the string
166+
167+ for channel_index in range (self .pl2reader .pl2_file_info .m_TotalNumberOfAnalogChannels ):
168+ achannel_info = self .pl2reader .pl2_get_analog_channel_info (channel_index )
160169 # only consider active channels
161- if not achannel_info .m_ChannelEnabled :
170+ if not ( achannel_info .m_ChannelEnabled and achannel_info . m_ChannelRecordingEnabled ) :
162171 continue
163172
164173 # assign to matching stream or create new stream based on signal characteristics
165174 rate = achannel_info .m_SamplesPerSecond
166- n_samples = achannel_info .m_NumberOfValues
167- source_id = str (achannel_info .m_Source )
168-
169- channel_source = Source (source_id , f"stream@{ rate } Hz" , rate , n_samples )
170- existing_source = source_characteristics .setdefault (source_id , channel_source )
171-
172- # ensure that stream of this channel and existing stream have same properties
173- if channel_source != existing_source :
174- raise ValueError (
175- f"The channel source { channel_source } must be the same as the existing source { existing_source } "
176- )
175+ num_samples = achannel_info .m_NumberOfValues
176+ channel_num_samples .append (num_samples )
177177
178178 ch_name = achannel_info .m_Name .decode ()
179179 chan_id = f"source{ achannel_info .m_Source } .{ achannel_info .m_Channel } "
180180 dtype = "int16"
181181 units = achannel_info .m_Units .decode ()
182182 gain = achannel_info .m_CoeffToConvertToUnits
183183 offset = 0.0 # PL2 files don't contain information on signal offset
184- stream_id = source_id
184+
185+ channel_prefix = re .match (regex_prefix_pattern , ch_name ).group (0 )
186+ stream_id = channel_prefix
185187 buffer_id = ""
186188 signal_channels .append ((ch_name , chan_id , rate , dtype , units , gain , offset , stream_id , buffer_id ))
187189
188190 signal_channels = np .array (signal_channels , dtype = _signal_channel_dtype )
189- self .signal_stream_characteristics = source_characteristics
190-
191- # create signal streams from source information
192- # we consider stream = source but buffer is unkown
191+ channel_num_samples = np .array (channel_num_samples )
192+
193+ # We are using channel prefixes as stream_ids
194+ # The meaning of the channel prefixes was provided by a Plexon Engineer, see here:
195+ # https://github.com/NeuralEnsemble/python-neo/pull/1495#issuecomment-2184256894
196+ stream_id_to_stream_name = {
197+ "WB" : "WB-Wideband" ,
198+ "FP" : "FPl-Low Pass Filtered" ,
199+ "SP" : "SPKC-High Pass Filtered" ,
200+ "AI" : "AI-Auxiliary Input" ,
201+ }
202+
203+ unique_stream_ids = np .unique (signal_channels ["stream_id" ])
193204 signal_streams = []
194- for stream_idx , source in source_characteristics .items ():
195- stream_id = source_id = str (source .id )
196- buffer_id = ""
197- signal_streams .append ((source .name , stream_id , buffer_id ))
198- signal_buffers = np .array ([], dtype = _signal_buffer_dtype )
205+ for stream_id in unique_stream_ids :
206+ # We are using the channel prefixes as ids
207+ # The users of plexon can modify the prefix of the channel names (e.g. `my_prefix` instead of `WB`).
208+ # In that case we use the channel prefix both as stream id and name
209+ stream_name = stream_id_to_stream_name .get (stream_id , stream_id )
210+ signal_streams .append ((stream_name , stream_id ))
199211 signal_streams = np .array (signal_streams , dtype = _signal_stream_dtype )
212+ # In plexon buffer is unkown
213+ signal_buffers = np .array ([], dtype = _signal_buffer_dtype )
214+
215+ self .stream_id_samples = {}
216+ self .stream_index_to_stream_id = {}
217+ for stream_index , stream_id in enumerate (signal_streams ["id" ]):
218+ # Keep a mapping from stream_index to stream_id
219+ self .stream_index_to_stream_id [stream_index ] = stream_id
220+
221+ # We extract the number of samples for each stream
222+ mask = signal_channels ["stream_id" ] == stream_id
223+ signal_num_samples = np .unique (channel_num_samples [mask ])
224+ assert signal_num_samples .size == 1 , "All channels in a stream must have the same number of samples"
225+ self .stream_id_samples [stream_id ] = signal_num_samples [0 ]
200226
201227 # pre-loading spike channel_data for later usage
202228 self ._spike_channel_cache = {}
@@ -205,7 +231,7 @@ def _parse_header(self):
205231 schannel_info = self .pl2reader .pl2_get_spike_channel_info (c )
206232
207233 # only consider active channels
208- if not schannel_info .m_ChannelEnabled :
234+ if not ( schannel_info .m_ChannelEnabled and schannel_info . m_ChannelRecordingEnabled ) :
209235 continue
210236
211237 for channel_unit_id in range (schannel_info .m_NumberOfUnits ):
@@ -229,7 +255,7 @@ def _parse_header(self):
229255 echannel_info = self .pl2reader .pl2_get_digital_channel_info (i )
230256
231257 # only consider active channels
232- if not echannel_info .m_ChannelEnabled :
258+ if not ( echannel_info .m_ChannelEnabled and echannel_info . m_ChannelRecordingEnabled ) :
233259 continue
234260
235261 # event channels are characterized by (name, id, type), with type in ['event', 'epoch']
@@ -361,16 +387,12 @@ def _segment_t_stop(self, block_index, seg_index):
361387 end_time = (
362388 self .pl2reader .pl2_file_info .m_StartRecordingTime + self .pl2reader .pl2_file_info .m_DurationOfRecording
363389 )
364- return end_time / self .pl2reader .pl2_file_info .m_TimestampFrequency
390+ return float ( end_time / self .pl2reader .pl2_file_info .m_TimestampFrequency )
365391
366392 def _get_signal_size (self , block_index , seg_index , stream_index ):
367- # this must return an integer value (the number of samples)
368-
369- stream_id = self .header ["signal_streams" ][stream_index ]["id" ]
370- stream_characteristic = list (self .signal_stream_characteristics .values ())[stream_index ]
371- if stream_id != stream_characteristic .id :
372- raise ValueError (f"The `stream_id` must be { stream_characteristic .id } " )
373- return int (stream_characteristic .n_samples ) # Avoids returning a numpy.int64 scalar
393+ stream_id = self .stream_index_to_stream_id [stream_index ]
394+ num_samples = int (self .stream_id_samples [stream_id ])
395+ return num_samples
374396
375397 def _get_signal_t_start (self , block_index , seg_index , stream_index ):
376398 # This returns the t_start of signals as a float value in seconds
0 commit comments