Skip to content

Commit 06a624c

Browse files
committed
Refactor continuous data handling to use a dictionary structure for improved access by index and stream name
1 parent a1cb07e commit 06a624c

File tree

5 files changed

+67
-103
lines changed

5 files changed

+67
-103
lines changed

src/open_ephys/analysis/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ Recording Index: 0
6565

6666
## Loading continuous data
6767

68-
Continuous data for each recording is accessed via the `.continuous` property of each `Recording` object. This returns a list of continuous data, grouped by processor/sub-processor. For example, if you have two data streams merged into a single Record Node, each data stream will be associated with a different processor ID. If you're recording Neuropixels data, each probe's data stream will be stored in a separate sub-processor, which must be loaded individually.
68+
Continuous data for each recording is accessed via the `.continuous` property of each `Recording` object. This now returns a dictionary of continuous data grouped by processor/sub-processor. Each stream is stored twice in the dictionary: once under its zero-based index and once under its stream name. For example, if you have two data streams merged into a single Record Node, each data stream will be associated with a different processor ID. If you're recording Neuropixels data, each probe's data stream will be stored in a separate sub-processor, which must be loaded individually.
6969

70-
Continuous data for individual data streams can be accessed by index (e.g., `continuous[0]`), or by stream name (e.g., `continuous["example_data"]`). If there are multiple streams with the same name, the source processor ID will be appended to the stream name so they can be distinguished (e.g., `continuous["example_data_100"]`).
70+
Continuous data for individual data streams can be accessed by index (e.g., `continuous[0]`), or by stream name (e.g., `continuous["example_data"]`). If there are multiple streams with the same name, the source processor ID will be appended to the stream name so they can be distinguished (e.g., `continuous["example_data_100"]`). Iterating over the dictionary yields the continuous objects in index order, and `continuous.keys()` lists both the integer indices and stream names that can be used for lookup.
7171

7272
Each `continuous` object has four properties:
7373

src/open_ephys/analysis/formats/BinaryRecording.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
RecordingFormat,
3838
Spikes,
3939
SpikeMetadata,
40-
create_continuous_named_tuple
40+
create_continuous_dict
4141
)
4242
from open_ephys.analysis.utils import alphanum_key
4343

@@ -295,7 +295,7 @@ def load_continuous(self):
295295
names[idx2] = name2 + "_" + source_processor_ids[idx2]
296296
break
297297

298-
self._continuous = create_continuous_named_tuple(names, values)
298+
self._continuous = create_continuous_dict(names, values)
299299

300300
def load_spikes(self):
301301
self._spikes = []

src/open_ephys/analysis/formats/NwbRecording.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
SpikeMetadata,
3737
RecordingFormat,
3838
Recording,
39-
create_continuous_named_tuple
39+
create_continuous_dict
4040
)
4141

4242

@@ -178,7 +178,7 @@ def load_continuous(self):
178178
names[idx2] = name2 + "_" + source_processor_ids[idx2]
179179
break
180180

181-
self._continuous = create_continuous_named_tuple(names, values)
181+
self._continuous = create_continuous_dict(names, values)
182182

183183
def load_spikes(self):
184184

src/open_ephys/analysis/formats/OpenEphysRecording.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
RecordingFormat,
4141
Spikes,
4242
SpikeMetadata,
43-
create_continuous_named_tuple
43+
create_continuous_dict
4444
)
4545

4646

@@ -286,7 +286,8 @@ def load_continuous(self):
286286
continuous_files, stream_indexes, unique_stream_indexes, stream_info = (
287287
self.find_continuous_files()
288288
)
289-
self._continuous = []
289+
values = []
290+
names = []
290291

291292
for stream_index in unique_stream_indexes:
292293

@@ -295,11 +296,15 @@ def load_continuous(self):
295296
if stream_indexes[ind] == stream_index:
296297
files_for_stream.append(os.path.join(self.directory, filename))
297298

298-
self._continuous.append(
299+
names.append(stream_info[stream_index]["stream_name"])
300+
301+
values.append(
299302
OpenEphysContinuous(
300303
stream_info[stream_index], files_for_stream, self.recording_index
301304
)
302305
)
306+
307+
self._continuous = create_continuous_dict(names, values)
303308

304309
def load_spikes(self):
305310

src/open_ephys/analysis/recording.py

Lines changed: 53 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from abc import ABC, abstractmethod
2727
from dataclasses import dataclass
28-
from collections import namedtuple
28+
from collections.abc import Sequence
2929
from enum import StrEnum
3030
import warnings
3131
import numpy as np
@@ -51,51 +51,54 @@ class ContinuousMetadata:
5151
bit_volts: list[float]
5252

5353

54-
def create_continuous_named_tuple(names, values):
55-
"""
56-
Create a named tuple from the given names and values.
57-
"""
58-
NT = namedtuple("DynamicTuple", names)
59-
60-
class ContinuousWrapper:
61-
"""
62-
Allow the .continuous attribute to be accessed as a dictionary.
63-
"""
64-
def __init__(self, nt, names):
65-
self._nt = nt
66-
self._names = names
67-
self._index = {name: i for i, name in enumerate(names)}
68-
69-
def __getitem__(self, key):
70-
if isinstance(key, str):
71-
return getattr(self._nt, key)
72-
return self._nt[key]
73-
74-
def __getattr__(self, attr):
75-
return getattr(self._nt, attr)
76-
77-
def __len__(self):
78-
return len(self._nt)
79-
80-
def __iter__(self):
81-
return iter(self._nt)
82-
83-
def keys(self):
84-
"""Return available field names (like dict.keys())."""
85-
return list(self._names)
86-
87-
def items(self):
88-
"""Return (name, value) pairs."""
89-
return [(name, getattr(self._nt, name)) for name in self._names]
90-
91-
def values(self):
92-
"""Return values (like dict.values())."""
93-
return list(self._nt)
94-
95-
def __repr__(self):
96-
return repr(self._nt)
97-
98-
return ContinuousWrapper(NT(*values), names)
54+
class ContinuousDict(dict):
55+
"""Dictionary access to continuous streams by numeric index or string name."""
56+
57+
def __init__(self, names: Sequence[str], values: Sequence["Continuous"]):
58+
if len(names) != len(values):
59+
raise ValueError("`names` and `values` must have the same length.")
60+
61+
super().__init__()
62+
self._names = list(names)
63+
self._values = list(values)
64+
65+
for idx, (name, value) in enumerate(zip(self._names, self._values)):
66+
super().__setitem__(idx, value)
67+
super().__setitem__(name, value)
68+
69+
def __getitem__(self, key):
70+
if isinstance(key, (int, np.integer)):
71+
key = int(key)
72+
return super().__getitem__(key)
73+
74+
def __iter__(self):
75+
return iter(self._values)
76+
77+
def __len__(self):
78+
return len(self._values)
79+
80+
def keys(self):
81+
return list(range(len(self._values))) + list(self._names)
82+
83+
def items(self):
84+
return [(idx, value) for idx, value in enumerate(self._values)] + [
85+
(name, value) for name, value in zip(self._names, self._values)
86+
]
87+
88+
def values(self):
89+
return list(self._values)
90+
91+
def __repr__(self):
92+
entries = ", ".join(
93+
f"{name!r}: {value!r}" for name, value in zip(self._names, self._values)
94+
)
95+
return f"ContinuousDict({{{entries}}})"
96+
97+
98+
def create_continuous_dict(names, values):
99+
"""Return continuous data as a dictionary keyed by index and stream name."""
100+
101+
return ContinuousDict(names, values)
99102
class Continuous(ABC):
100103
metadata: ContinuousMetadata
101104
samples: np.ndarray
@@ -132,52 +135,6 @@ def get_samples(
132135
"""
133136
pass
134137

135-
def create_continuous_named_tuple(names, values):
136-
"""
137-
Create a named tuple from the given names and values.
138-
"""
139-
NT = namedtuple("DynamicTuple", names)
140-
141-
class ContinuousWrapper:
142-
"""
143-
Allow the .continuous attribute to be accessed as a dictionary.
144-
"""
145-
def __init__(self, nt, names):
146-
self._nt = nt
147-
self._names = names
148-
self._index = {name: i for i, name in enumerate(names)}
149-
150-
def __getitem__(self, key):
151-
if isinstance(key, str):
152-
return getattr(self._nt, key)
153-
return self._nt[key]
154-
155-
def __getattr__(self, attr):
156-
return getattr(self._nt, attr)
157-
158-
def __len__(self):
159-
return len(self._nt)
160-
161-
def __iter__(self):
162-
return iter(self._nt)
163-
164-
def keys(self):
165-
"""Return available field names (like dict.keys())."""
166-
return list(self._names)
167-
168-
def items(self):
169-
"""Return (name, value) pairs."""
170-
return [(name, getattr(self._nt, name)) for name in self._names]
171-
172-
def values(self):
173-
"""Return values (like dict.values())."""
174-
return list(self._nt)
175-
176-
def __repr__(self):
177-
return repr(self._nt)
178-
179-
return ContinuousWrapper(NT(*values), names)
180-
181138
class Spikes(ABC):
182139
metadata: SpikeMetadata
183140
waveforms: np.ndarray | None
@@ -242,8 +199,10 @@ class Recording(ABC):
242199
"""
243200

244201
@property
245-
def continuous(self) -> list[Continuous] | None:
246-
"""Returns a list of Continuous objects"""
202+
def continuous(self) -> ContinuousDict | None:
203+
"""Returns a ContinuousDict containing Continuous objects
204+
which can be accessed by index or stream name.
205+
"""
247206
if self._continuous is None:
248207
self.load_continuous()
249208
return self._continuous

0 commit comments

Comments
 (0)