@@ -230,9 +230,19 @@ def _parse_header(self):
230230 self ._data_blocks [bl_type ][chan_id ] = data_block
231231
232232 # signals channels
233- sig_channels = []
234- all_sig_length = []
235233 source_id = []
234+
235+ # Scanning sources and populating signal channels at the same time. Sources have to have
236+ # same sampling rate and number of samples to belong to one stream.
237+ signal_channels = []
238+ channel_num_samples = []
239+
240+ # We will build the stream ids based on the channel prefixes
241+ # The channel prefixes are the first characters of the channel names which have the following format:
242+ # WB{number}, FPX{number}, SPKCX{number}, AI{number}, etc
243+ # We will extract the prefix and use it as stream id
244+ regex_prefix_pattern = r"^\D+" # Match any non-digit character at the beginning of the string
245+
236246 if self .progress_bar :
237247 chan_loop = trange (nb_sig_chan , desc = "Parsing signal channels" , leave = True )
238248 else :
@@ -245,7 +255,7 @@ def _parse_header(self):
245255 if length == 0 :
246256 continue # channel not added
247257 source_id .append (h ["SrcId" ])
248- all_sig_length .append (length )
258+ channel_num_samples .append (length )
249259 sampling_rate = float (h ["ADFreq" ])
250260 sig_dtype = "int16"
251261 units = "" # I don't know units
@@ -258,61 +268,57 @@ def _parse_header(self):
258268 0.5 * (2 ** global_header ["BitsPerSpikeSample" ]) * h ["Gain" ] * h ["PreampGain" ]
259269 )
260270 offset = 0.0
261- stream_id = "0" # This is overwritten later
262- sig_channels .append ((name , str (chan_id ), sampling_rate , sig_dtype , units , gain , offset , stream_id ))
271+ channel_prefix = re .match (regex_prefix_pattern , name ).group (0 )
272+ stream_id = channel_prefix
273+
274+ signal_channels .append ((name , str (chan_id ), sampling_rate , sig_dtype , units , gain , offset , stream_id ))
263275
264- sig_channels = np .array (sig_channels , dtype = _signal_channel_dtype )
276+ signal_channels = np .array (signal_channels , dtype = _signal_channel_dtype )
265277
266- if sig_channels .size == 0 :
278+ if signal_channels .size == 0 :
267279 signal_streams = np .array ([], dtype = _signal_stream_dtype )
268280
269281 else :
270282 # Detect streams
271- all_sig_length = np .asarray (all_sig_length )
272-
273- # names are WB{number}, FPX{number}, SPKCX{number}, AI{number}, etc
274- pattern = r"^\D+" # Match any non-digit character at the beginning of the string
275- channels_prefixes = np .asarray ([re .match (pattern , name ).group (0 ) for name in sig_channels ["name" ]])
276- buffer_stream_groups = set (zip (channels_prefixes , sig_channels ["sampling_rate" ], all_sig_length ))
277-
278- # There are explanations of the streams based on channel names
279- # provided by a Plexon Engineer, see here:
283+ channel_num_samples = np .asarray (channel_num_samples )
284+ # We are using channel prefixes as stream_ids
285+ # The meaning of the channel prefixes was provided by a Plexon Engineer, see here:
280286 # https://github.com/NeuralEnsemble/python-neo/pull/1495#issuecomment-2184256894
281- channel_prefix_to_stream_name = {
287+ stream_id_to_stream_name = {
282288 "WB" : "WB-Wideband" ,
283- "FP" : "FPl-Low Pass Filtered " ,
289+ "FP" : "FPl-Low Pass Filtered" ,
284290 "SP" : "SPKC-High Pass Filtered" ,
285291 "AI" : "AI-Auxiliary Input" ,
286292 }
287293
288- # Using a mapping to ensure consistent order of stream_index
289- channel_prefix_to_stream_id = {
290- "WB" : "0" ,
291- "FP" : "1" ,
292- "SP" : "2" ,
293- "AI" : "3" ,
294- }
295-
294+ unique_stream_ids = np .unique (signal_channels ["stream_id" ])
296295 signal_streams = []
297- self ._signal_length = {}
298- self ._sig_sampling_rate = {}
299-
300- for stream_index , (channel_prefix , sr , length ) in enumerate (buffer_stream_groups ):
301- # The users of plexon can modify the prefix of the channel names (e.g. `my_prefix` instead of `WB`). This is not common but in that case
302- # We assign the channel_prefix both as stream_name and stream_id
303- stream_name = channel_prefix_to_stream_name .get (channel_prefix , channel_prefix )
304- stream_id = channel_prefix_to_stream_id .get (channel_prefix , channel_prefix )
305-
306- mask = (sig_channels ["sampling_rate" ] == sr ) & (all_sig_length == length )
307- sig_channels ["stream_id" ][mask ] = stream_id
308-
309- self ._sig_sampling_rate [stream_index ] = sr
310- self ._signal_length [stream_index ] = length
311-
296+ for stream_id in unique_stream_ids :
297+ # We are using the channel prefixes as ids
298+ # The users of plexon can modify the prefix of the channel names (e.g. `my_prefix` instead of `WB`).
299+ # In that case we use the channel prefix both as stream id and name
300+ stream_name = stream_id_to_stream_name .get (stream_id , stream_id )
312301 signal_streams .append ((stream_name , stream_id ))
313302
314303 signal_streams = np .array (signal_streams , dtype = _signal_stream_dtype )
315304
305+ self .stream_id_samples = {}
306+ self .stream_id_sampling_frequency = {}
307+ self .stream_index_to_stream_id = {}
308+ for stream_index , stream_id in enumerate (signal_streams ["id" ]):
309+ # Keep a mapping from stream_index to stream_id
310+ self .stream_index_to_stream_id [stream_index ] = stream_id
311+
312+ # We extract the number of samples for each stream
313+ mask = signal_channels ["stream_id" ] == stream_id
314+ signal_num_samples = np .unique (channel_num_samples [mask ])
315+ assert signal_num_samples .size == 1 , "All channels in a stream must have the same number of samples"
316+ self .stream_id_samples [stream_id ] = signal_num_samples [0 ]
317+
318+ signal_sampling_frequency = np .unique (signal_channels [mask ]["sampling_rate" ])
319+ assert signal_sampling_frequency .size == 1 , "All channels in a stream must have the same sampling frequency"
320+ self .stream_id_sampling_frequency [stream_id ] = signal_sampling_frequency [0 ]
321+
316322 self ._global_ssampling_rate = global_header ["ADFrequency" ]
317323
318324 # Determine number of units per channels
@@ -374,7 +380,7 @@ def _parse_header(self):
374380 "nb_block" : 1 ,
375381 "nb_segment" : [1 ],
376382 "signal_streams" : signal_streams ,
377- "signal_channels" : sig_channels ,
383+ "signal_channels" : signal_channels ,
378384 "spike_channels" : spike_channels ,
379385 "event_channels" : event_channels ,
380386 }
@@ -392,14 +398,15 @@ def _segment_t_start(self, block_index, seg_index):
392398
393399 def _segment_t_stop (self , block_index , seg_index ):
394400 t_stop = float (self ._last_timestamps ) / self ._global_ssampling_rate
395- if hasattr (self , "_signal_length " ):
396- for stream_index in self ._signal_length .keys ():
397- t_stop_sig = self ._signal_length [ stream_index ] / self ._sig_sampling_rate [ stream_index ]
401+ if hasattr (self , "stream_id_samples " ):
402+ for stream_id in self .stream_id_samples .keys ():
403+ t_stop_sig = self .stream_id_samples [ stream_id ] / self .stream_id_sampling_frequency [ stream_id ]
398404 t_stop = max (t_stop , t_stop_sig )
399405 return t_stop
400406
401407 def _get_signal_size (self , block_index , seg_index , stream_index ):
402- return self ._signal_length [stream_index ]
408+ stream_id = self .stream_index_to_stream_id [stream_index ]
409+ return self .stream_id_samples [stream_id ]
403410
404411 def _get_signal_t_start (self , block_index , seg_index , stream_index ):
405412 return 0.0
0 commit comments