@@ -134,6 +134,20 @@ class NeuralynxRawIO(BaseRawIO):
134134 ("samples" , "int16" , NcsSection ._RECORD_SIZE ),
135135 ]
136136
137+ # Filter parameter keys used for stream differentiation
138+ _filter_keys = [
139+ "DSPLowCutFilterEnabled" ,
140+ "DspLowCutFrequency" ,
141+ "DspLowCutFilterType" ,
142+ "DspLowCutNumTaps" ,
143+ "DSPHighCutFilterEnabled" ,
144+ "DspHighCutFrequency" ,
145+ "DspHighCutFilterType" ,
146+ "DspHighCutNumTaps" ,
147+ "DspDelayCompensation" ,
148+ "DspFilterDelay_µs" ,
149+ ]
150+
137151 def __init__ (
138152 self ,
139153 dirname = "" ,
@@ -189,6 +203,61 @@ def __init__(
189203 def _source_name (self ):
190204 return self .dirname
191205
206+ def _build_stream_key (self , header_info , chan_index , gain ):
207+ """
208+ Build stream key based on acquisition parameters only.
209+
210+ Stream keys are used to group channels that share the same acquisition
211+ configuration. Channels with the same stream key will be placed in the
212+ same stream and can be read together.
213+
214+ Parameters
215+ ----------
216+ header_info : dict
217+ Header information from NlxHeader
218+ chan_index : int
219+ Channel index for multi-channel parameters
220+ gain : float
221+ Channel gain value (bit_to_microVolt)
222+
223+ Returns
224+ -------
225+ tuple
226+ Hashable stream key containing acquisition parameters:
227+ (sampling_rate, input_range, gain, input_inverted, filter_params_tuple)
228+ """
229+ # Core acquisition parameters (already normalized by NlxHeader)
230+ sampling_rate = float (header_info ["sampling_rate" ])
231+
232+ # Get InputRange - could be int (single-channel) or list (multi-channel)
233+ input_range = header_info .get ("InputRange" )
234+ if isinstance (input_range , list ):
235+ # Multi-channel file: get value for this channel
236+ input_range = input_range [chan_index ] if chan_index < len (input_range ) else input_range [0 ]
237+ # Already converted to int by NlxHeader._normalize_types()
238+
239+ gain = float (gain )
240+
241+ input_inverted = header_info .get ("input_inverted" , False )
242+
243+ # Filter parameters (already normalized by NlxHeader)
244+ filter_params = []
245+ for key in self ._filter_keys :
246+ value = header_info .get (key )
247+ if value is not None :
248+ filter_params .append ((key , value ))
249+
250+ # Create hashable stream key
251+ stream_key = (
252+ sampling_rate ,
253+ input_range ,
254+ gain ,
255+ input_inverted ,
256+ tuple (sorted (filter_params )),
257+ )
258+
259+ return stream_key
260+
192261 def _parse_header (self ):
193262
194263 stream_channels = []
@@ -268,26 +337,30 @@ def _parse_header(self):
268337
269338 chan_uid = (chan_name , str (chan_id ))
270339 if ext == "ncs" :
271- file_mmap = self ._get_file_map (filename )
272- n_packets = copy .copy (file_mmap .shape [0 ])
273- if n_packets :
274- t_start = copy .copy (file_mmap [0 ][0 ])
275- else : # empty file
276- t_start = 0
277- stream_prop = (float (info ["sampling_rate" ]), int (n_packets ), float (t_start ))
278- if stream_prop not in stream_props :
279- stream_props [stream_prop ] = {"stream_id" : len (stream_props ), "filenames" : [filename ]}
340+ # Calculate gain for this channel
341+ gain = info ["bit_to_microVolt" ][idx ]
342+ if info .get ("input_inverted" , False ):
343+ gain *= - 1
344+
345+ # Build stream key from acquisition parameters only
346+ stream_key = self ._build_stream_key (info , idx , gain )
347+
348+ if stream_key not in stream_props :
349+ stream_props [stream_key ] = {
350+ "stream_id" : len (stream_props ),
351+ "filenames" : [filename ],
352+ "channels" : set (),
353+ }
280354 else :
281- stream_props [stream_prop ]["filenames" ].append (filename )
282- stream_id = stream_props [stream_prop ]["stream_id" ]
355+ stream_props [stream_key ]["filenames" ].append (filename )
356+
357+ stream_id = stream_props [stream_key ]["stream_id" ]
358+ stream_props [stream_key ]["channels" ].add ((chan_name , str (chan_id )))
283359 # @zach @ramon : we need to discuss this split by channel buffer
284360 buffer_id = ""
285361
286362 # a sampled signal channel
287363 units = "uV"
288- gain = info ["bit_to_microVolt" ][idx ]
289- if info .get ("input_inverted" , False ):
290- gain *= - 1
291364 offset = 0.0
292365 signal_channels .append (
293366 (
@@ -392,14 +465,50 @@ def _parse_header(self):
392465 event_channels = np .array (event_channels , dtype = _event_channel_dtype )
393466
394467 if signal_channels .size > 0 :
395- # ordering streams according from high to low sampling rates
396- stream_props = {k : stream_props [k ] for k in sorted (stream_props , reverse = True )}
397- stream_names = [f"Stream (rate,#packet,t0): { sp } " for sp in stream_props ]
398- stream_ids = [stream_prop ["stream_id" ] for stream_prop in stream_props .values ()]
399- buffer_ids = ["" for sp in stream_props ]
468+ # Build filter configuration registry
469+ filter_configs = {} # filter_params_tuple -> filter_id
470+ _filter_configurations = {} # filter_id -> filter parameters dict
471+
472+ for stream_key , stream_info in stream_props .items ():
473+ # Extract filter parameters from stream_key
474+ # stream_key = (sampling_rate, input_range, gain, input_inverted, filter_params_tuple)
475+ sampling_rate , input_range , gain , input_inverted , filter_params_tuple = stream_key
476+
477+ # Assign filter ID (deduplicated by filter_params_tuple)
478+ if filter_params_tuple not in filter_configs :
479+ filter_id = len (filter_configs )
480+ filter_configs [filter_params_tuple ] = filter_id
481+ # Convert filter_params_tuple to dict for storage
482+ _filter_configurations [filter_id ] = dict (filter_params_tuple )
483+
484+ # Store filter configurations as private instance attribute
485+ self ._filter_configurations = _filter_configurations
486+
487+ # Order streams by sampling rate (high to low)
488+ ordered_stream_keys = sorted (stream_props .keys (), reverse = True , key = lambda x : x [0 ])
489+
490+ stream_names = []
491+ stream_ids = []
492+ buffer_ids = []
493+
494+ for stream_key in ordered_stream_keys :
495+ stream_info = stream_props [stream_key ]
496+ stream_id = stream_info ["stream_id" ]
497+
498+ # Unpack stream_key and format stream name
499+ sampling_rate , input_range , gain , input_inverted , filter_params_tuple = stream_key
500+ filter_id = filter_configs [filter_params_tuple ]
501+ voltage_mv = int (input_range / 1000 ) if input_range is not None else 0
502+ stream_name = f"stream{ stream_id } _{ int (sampling_rate )} Hz_{ voltage_mv } mVRange_f{ filter_id } "
503+
504+ stream_names .append (stream_name )
505+ stream_ids .append (stream_id )
506+ buffer_ids .append ("" )
507+
400508 signal_streams = list (zip (stream_names , stream_ids , buffer_ids ))
401509 else :
402510 signal_streams = []
511+ self ._filter_configurations = {}
403512 signal_buffers = np .array ([], dtype = _signal_buffer_dtype )
404513 signal_streams = np .array (signal_streams , dtype = _signal_stream_dtype )
405514
0 commit comments