|
63 | 63 | from neo.rawio.neuralynxrawio.ncssections import NcsSection, NcsSectionsFactory |
64 | 64 | from neo.rawio.neuralynxrawio.nlxheader import NlxHeader |
65 | 65 |
|
| 66 | +# Named tuple for stream identification keys |
| 67 | +StreamKey = namedtuple('StreamKey', ['sampling_rate', 'input_range', 'filter_params']) |
| 68 | + |
66 | 69 |
|
67 | 70 | class NeuralynxRawIO(BaseRawIO): |
68 | 71 | """ |
@@ -134,6 +137,20 @@ class NeuralynxRawIO(BaseRawIO): |
134 | 137 | ("samples", "int16", NcsSection._RECORD_SIZE), |
135 | 138 | ] |
136 | 139 |
|
| 140 | + # Filter parameter keys used for stream differentiation |
| 141 | + _filter_keys = [ |
| 142 | + "DSPLowCutFilterEnabled", |
| 143 | + "DspLowCutFrequency", |
| 144 | + "DspLowCutFilterType", |
| 145 | + "DspLowCutNumTaps", |
| 146 | + "DSPHighCutFilterEnabled", |
| 147 | + "DspHighCutFrequency", |
| 148 | + "DspHighCutFilterType", |
| 149 | + "DspHighCutNumTaps", |
| 150 | + "DspDelayCompensation", |
| 151 | + "DspFilterDelay_µs", |
| 152 | + ] |
| 153 | + |
137 | 154 | def __init__( |
138 | 155 | self, |
139 | 156 | dirname="", |
@@ -268,26 +285,49 @@ def _parse_header(self): |
268 | 285 |
|
269 | 286 | chan_uid = (chan_name, str(chan_id)) |
270 | 287 | 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]} |
| 288 | + # Calculate gain for this channel |
| 289 | + gain = info["bit_to_microVolt"][idx] |
| 290 | + if info.get("input_inverted", False): |
| 291 | + gain *= -1 |
| 292 | + |
| 293 | + # Build stream key from acquisition parameters |
| 294 | + sampling_rate = float(info["sampling_rate"]) |
| 295 | + |
| 296 | + # Get InputRange for this specific channel |
| 297 | + # Normalized by NlxHeader to always be a list |
| 298 | + input_range = info.get("InputRange") |
| 299 | + if isinstance(input_range, list): |
| 300 | + input_range = input_range[idx] if idx < len(input_range) else input_range[0] |
| 301 | + |
| 302 | + # Build filter parameters tuple |
| 303 | + filter_params = [] |
| 304 | + for key in self._filter_keys: |
| 305 | + if key in info: |
| 306 | + filter_params.append((key, info[key])) |
| 307 | + |
| 308 | + # Create stream key (channels with same key go in same stream) |
| 309 | + stream_key = StreamKey( |
| 310 | + sampling_rate=sampling_rate, |
| 311 | + input_range=input_range, |
| 312 | + filter_params=tuple(sorted(filter_params)), |
| 313 | + ) |
| 314 | + |
| 315 | + if stream_key not in stream_props: |
| 316 | + stream_props[stream_key] = { |
| 317 | + "stream_id": len(stream_props), |
| 318 | + "filenames": [filename], |
| 319 | + "channels": set(), |
| 320 | + } |
280 | 321 | else: |
281 | | - stream_props[stream_prop]["filenames"].append(filename) |
282 | | - stream_id = stream_props[stream_prop]["stream_id"] |
| 322 | + stream_props[stream_key]["filenames"].append(filename) |
| 323 | + |
| 324 | + stream_id = stream_props[stream_key]["stream_id"] |
| 325 | + stream_props[stream_key]["channels"].add((chan_name, str(chan_id))) |
283 | 326 | # @zach @ramon : we need to discuss this split by channel buffer |
284 | 327 | buffer_id = "" |
285 | 328 |
|
286 | 329 | # a sampled signal channel |
287 | 330 | units = "uV" |
288 | | - gain = info["bit_to_microVolt"][idx] |
289 | | - if info.get("input_inverted", False): |
290 | | - gain *= -1 |
291 | 331 | offset = 0.0 |
292 | 332 | signal_channels.append( |
293 | 333 | ( |
@@ -392,14 +432,40 @@ def _parse_header(self): |
392 | 432 | event_channels = np.array(event_channels, dtype=_event_channel_dtype) |
393 | 433 |
|
394 | 434 | 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] |
| 435 | + # Build DSP filter configuration registry: filter_id -> filter parameters dict |
| 436 | + # Extract unique filter configurations from stream keys |
| 437 | + unique_filter_tuples = {stream_key.filter_params for stream_key in stream_props.keys()} |
| 438 | + _dsp_filter_configurations = {i: dict(filt) for i, filt in enumerate(sorted(unique_filter_tuples))} |
| 439 | + |
| 440 | + # Create reverse mapping for looking up filter IDs during stream name construction |
| 441 | + seen_filters = {filt: i for i, filt in enumerate(sorted(unique_filter_tuples))} |
| 442 | + |
| 443 | + # Store DSP filter configurations as private instance attribute |
| 444 | + # Keeping private for now - may expose via annotations or public API in future |
| 445 | + self._dsp_filter_configurations = _dsp_filter_configurations |
| 446 | + |
| 447 | + # Order streams by sampling rate (high to low) |
| 448 | + # This is to keep some semblance of stability |
| 449 | + ordered_stream_keys = sorted(stream_props.keys(), reverse=True, key=lambda x: x.sampling_rate) |
| 450 | + |
| 451 | + stream_names = [] |
| 452 | + stream_ids = [] |
| 453 | + buffer_ids = [] |
| 454 | + |
| 455 | + for stream_id, stream_key in enumerate(ordered_stream_keys): |
| 456 | + # Format stream name using namedtuple fields |
| 457 | + dsp_filter_id = seen_filters[stream_key.filter_params] |
| 458 | + voltage_mv = int(stream_key.input_range / 1000) if stream_key.input_range is not None else 0 |
| 459 | + stream_name = f"stream{stream_id}_{int(stream_key.sampling_rate)}Hz_{voltage_mv}mVRange_DSPFilter{dsp_filter_id}" |
| 460 | + |
| 461 | + stream_names.append(stream_name) |
| 462 | + stream_ids.append(str(stream_id)) |
| 463 | + buffer_ids.append("") |
| 464 | + |
400 | 465 | signal_streams = list(zip(stream_names, stream_ids, buffer_ids)) |
401 | 466 | else: |
402 | 467 | signal_streams = [] |
| 468 | + self._filter_configurations = {} |
403 | 469 | signal_buffers = np.array([], dtype=_signal_buffer_dtype) |
404 | 470 | signal_streams = np.array(signal_streams, dtype=_signal_stream_dtype) |
405 | 471 |
|
|
0 commit comments