Skip to content

Commit 775ad34

Browse files
committed
fix CI issues
1 parent 05cf038 commit 775ad34

5 files changed

Lines changed: 297 additions & 30 deletions

File tree

experanto/interpolators.py

Lines changed: 176 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,52 @@
2020
logger = logging.getLogger(__name__)
2121

2222

23+
def resolve_neuron_indices(neuron_ids, neuron_indices, unit_ids, n_signals):
24+
if neuron_ids is None and neuron_indices is None:
25+
return None
26+
27+
if neuron_ids is not None:
28+
ids_to_indexes = []
29+
30+
for nid in neuron_ids:
31+
match = np.where(unit_ids == nid)[0]
32+
if len(match) == 0:
33+
raise ValueError(f"Neuron id {nid} not found")
34+
ids_to_indexes.append(int(match[0]))
35+
36+
if neuron_indices is None:
37+
return ids_to_indexes
38+
39+
if set(ids_to_indexes) != set(neuron_indices):
40+
raise ValueError("neuron_ids and neuron_indices refer to different neurons")
41+
42+
warnings.warn(
43+
"Both neuron_ids and neuron_indices provided; using neuron_indices",
44+
stacklevel=2,
45+
)
46+
47+
return validate_neuron_indices(neuron_indices, n_signals)
48+
49+
50+
def validate_neuron_indices(neuron_indices, n_signals):
51+
try:
52+
indexes_seq = list(neuron_indices)
53+
except TypeError as exc:
54+
raise TypeError("neuron_indices must be iterable") from exc
55+
56+
if not all(isinstance(i, (int, np.integer)) for i in indexes_seq):
57+
raise TypeError("neuron_indices must contain integers")
58+
59+
if indexes_seq:
60+
if min(indexes_seq) < 0 or max(indexes_seq) >= n_signals:
61+
raise ValueError("neuron_indices out of bounds")
62+
63+
if len(set(indexes_seq)) != len(indexes_seq):
64+
raise ValueError("neuron_indices contain duplicates")
65+
66+
return indexes_seq
67+
68+
2369
class Interpolator:
2470
"""Abstract base class for time series interpolation.
2571
@@ -55,8 +101,10 @@ class Interpolator:
55101
Experiment : High-level interface that manages multiple interpolators.
56102
"""
57103

58-
def __init__(self, root_folder: str) -> None:
104+
def __init__(self, root_folder: str | Path) -> None:
59105
self.root_folder = Path(root_folder)
106+
self.n_signals: int = 0
107+
self.interpolation_mode: str | None = None
60108
self.start_time = None
61109
self.end_time = None
62110
# Valid interval can be different to start time and end time.
@@ -177,6 +225,14 @@ class SequenceInterpolator(Interpolator):
177225
If True, subtracts mean during normalization.
178226
normalize_std_threshold : float, optional
179227
Minimum std threshold to prevent division by near-zero values.
228+
neuron_ids : list, optional
229+
Biological neuron IDs to include. Converted to indexes using meta/unit_ids.npy.
230+
neuron_indices : list, optional
231+
Column indexes of neurons to include.
232+
neuron_ids : list, optional
233+
Biological neuron IDs to include. Converted to indexes using meta/unit_ids.npy.
234+
neuron_indices : list, optional
235+
Column indexes of neurons to include.
180236
**kwargs
181237
Additional keyword arguments (ignored).
182238
@@ -202,10 +258,12 @@ class SequenceInterpolator(Interpolator):
202258

203259
def __init__(
204260
self,
205-
root_folder: str,
261+
root_folder: str | Path,
206262
cache_data: bool = False, # already cached, put it here for consistency
207263
keep_nans: bool = False,
208264
interpolation_mode: str = "nearest_neighbor",
265+
neuron_ids: list[int] | None = None,
266+
neuron_indices: list[int] | None = None,
209267
normalize: bool = False,
210268
normalize_subtract_mean: bool = False,
211269
normalize_std_threshold: float | None = None, # or 0.01
@@ -215,18 +273,30 @@ def __init__(
215273
meta = self.load_meta()
216274
self.keep_nans = keep_nans
217275
self.interpolation_mode = interpolation_mode
276+
self.neuron_ids = neuron_ids
218277
self.normalize = normalize
219278
self.normalize_subtract_mean = normalize_subtract_mean
220279
self.normalize_std_threshold = normalize_std_threshold
221280
self.sampling_rate = meta["sampling_rate"]
222281
self.time_delta = 1.0 / self.sampling_rate
223282
self.start_time = meta["start_time"]
224283
self.end_time = meta["end_time"]
225-
self.is_mem_mapped = meta["is_mem_mapped"] if "is_mem_mapped" in meta else False
284+
self.is_mem_mapped = meta.get("is_mem_mapped", False)
226285
# Valid interval can be different to start time and end time.
227286
self.valid_interval = TimeInterval(self.start_time, self.end_time)
228287

229288
self.n_signals = meta["n_signals"]
289+
unit_ids = None
290+
if neuron_ids is not None:
291+
unit_ids = np.load(self.root_folder / "meta/unit_ids.npy")
292+
293+
self.neuron_indices = resolve_neuron_indices(
294+
neuron_ids,
295+
neuron_indices,
296+
unit_ids,
297+
self.n_signals,
298+
)
299+
230300
# read .mem (memmap) or .npy file
231301
if self.is_mem_mapped:
232302
self._data = np.memmap(
@@ -235,28 +305,47 @@ def __init__(
235305
mode="r",
236306
shape=(meta["n_timestamps"], meta["n_signals"]),
237307
)
238-
239-
if cache_data:
240-
self._data = np.array(self._data).astype(
241-
np.float32
242-
) # Convert memmap to ndarray
243308
else:
244309
self._data = np.load(self.root_folder / "data.npy")
245310

311+
if self.neuron_indices is not None:
312+
self.n_signals = len(self.neuron_indices)
313+
314+
# Cache only selected data
315+
if self.is_mem_mapped and cache_data:
316+
if self.neuron_indices is None:
317+
self._data = np.array(self._data, dtype=np.float32)
318+
else:
319+
self._data = np.stack(
320+
[self._data[:, i] for i in self.neuron_indices],
321+
axis=1,
322+
).astype(np.float32)
323+
324+
self.is_mem_mapped = False
325+
246326
if self.normalize:
247327
self.normalize_init()
248328

249329
def normalize_init(self):
250-
self.mean = np.load(self.root_folder / "meta/means.npy")
251-
self.std = np.load(self.root_folder / "meta/stds.npy")
330+
mean = np.load(self.root_folder / "meta/means.npy") # shape: (n_total_signals,)
331+
std = np.load(self.root_folder / "meta/stds.npy")
332+
333+
# Filter to selected neurons, before assertion
334+
if self.neuron_indices is not None:
335+
mean = mean[self.neuron_indices]
336+
std = std[self.neuron_indices]
337+
338+
self.mean = mean.T
339+
self.std = std.T
340+
341+
# Now n_signals and shape are guaranteed to match
252342
assert (
253343
self.mean.shape[0] == self.n_signals
254-
), f"mean shape does not match: {self.mean.shape} vs {self._data.shape}"
344+
), f"mean shape does not match: {self.mean.shape[0]} vs {self.n_signals}"
255345
assert (
256346
self.std.shape[0] == self.n_signals
257-
), f"std shape does not match: {self.std.shape} vs {self._data.shape}"
258-
self.mean = self.mean.T
259-
self.std = self.std.T
347+
), f"std shape does not match: {self.std.shape[0]} vs {self.n_signals}"
348+
260349
if self.normalize_std_threshold:
261350
threshold = self.normalize_std_threshold * np.nanmean(self.std)
262351
idx = self.std > threshold
@@ -294,7 +383,16 @@ def interpolate(
294383
)
295384

296385
if self.interpolation_mode == "nearest_neighbor":
297-
data = self._data[idx_lower]
386+
if self.neuron_indices is None:
387+
data = self._data[idx_lower]
388+
else:
389+
if self.is_mem_mapped:
390+
data = np.stack(
391+
[self._data[idx_lower, i] for i in self.neuron_indices],
392+
axis=1,
393+
)
394+
else:
395+
data = self._data[idx_lower]
298396

299397
return (data, valid) if return_valid else data
300398

@@ -324,8 +422,22 @@ def interpolate(
324422
lower_signal_ratio = ((times_upper - times_valid) / denom)[:, None]
325423
upper_signal_ratio = ((times_valid - times_lower) / denom)[:, None]
326424

327-
data_lower = self._data[idx_lower]
328-
data_upper = self._data[idx_upper]
425+
if self.neuron_indices is None:
426+
data_lower = self._data[idx_lower]
427+
data_upper = self._data[idx_upper]
428+
else:
429+
if self.is_mem_mapped:
430+
data_lower = np.stack(
431+
[self._data[idx_lower, i] for i in self.neuron_indices],
432+
axis=1,
433+
)
434+
data_upper = np.stack(
435+
[self._data[idx_upper, i] for i in self.neuron_indices],
436+
axis=1,
437+
)
438+
else:
439+
data_lower = self._data[idx_lower]
440+
data_upper = self._data[idx_upper]
329441

330442
interpolated = (
331443
lower_signal_ratio * data_lower + upper_signal_ratio * data_upper
@@ -376,7 +488,7 @@ class PhaseShiftedSequenceInterpolator(SequenceInterpolator):
376488

377489
def __init__(
378490
self,
379-
root_folder: str,
491+
root_folder: str | Path,
380492
cache_data: bool = False, # already cached, put it here for consistency
381493
keep_nans: bool = False,
382494
interpolation_mode: str = "nearest_neighbor",
@@ -397,6 +509,10 @@ def __init__(
397509
)
398510

399511
self._phase_shifts = np.load(self.root_folder / "meta/phase_shifts.npy")
512+
# Forward the required indexes
513+
if self.neuron_indices is not None:
514+
self._phase_shifts = self._phase_shifts[self.neuron_indices]
515+
400516
self.valid_interval = TimeInterval(
401517
self.start_time
402518
+ (np.max(self._phase_shifts) if len(self._phase_shifts) > 0 else 0),
@@ -523,8 +639,8 @@ class ScreenInterpolator(Interpolator):
523639

524640
def __init__(
525641
self,
526-
root_folder: str,
527-
cache_data: bool = False,
642+
root_folder: str | Path,
643+
cache_data: bool = False, # New parameter
528644
rescale: bool = False,
529645
rescale_size: tuple[int, int] | None = None,
530646
normalize: bool = False,
@@ -752,7 +868,7 @@ class TimeIntervalInterpolator(Interpolator):
752868
*i*-th valid time falls within any interval for the *j*-th label.
753869
"""
754870

755-
def __init__(self, root_folder: str, cache_data: bool = False, **kwargs):
871+
def __init__(self, root_folder: str | Path, cache_data: bool = False, **kwargs):
756872
super().__init__(root_folder)
757873
self.cache_data = cache_data
758874

@@ -1046,11 +1162,13 @@ class SpikeInterpolator(Interpolator):
10461162

10471163
def __init__(
10481164
self,
1049-
root_folder: str,
1165+
root_folder: str | Path,
10501166
cache_data: bool = False,
10511167
interpolation_window: float = 0.3,
10521168
interpolation_align: str = "center",
10531169
smoothing_sigma: float = 0.0,
1170+
neuron_ids: list[int] | None = None,
1171+
neuron_indices: list[int] | None = None,
10541172
):
10551173
super().__init__(root_folder)
10561174

@@ -1100,6 +1218,42 @@ def __init__(
11001218
else:
11011219
self.spikes = np.load(self.dat_path)
11021220

1221+
unit_ids = None
1222+
if neuron_ids is not None:
1223+
unit_ids = np.load(self.root_folder / "meta/unit_ids.npy")
1224+
1225+
neuron_indices = resolve_neuron_indices(
1226+
neuron_ids,
1227+
neuron_indices,
1228+
unit_ids,
1229+
self.n_signals,
1230+
)
1231+
1232+
# If specific neuron indexes are requested, rebuild the spike array so that it
1233+
# only contains spikes from the selected neurons. We also rebuild the indices
1234+
# array so that it matches the new compacted spike array.
1235+
if neuron_indices is not None:
1236+
if len(neuron_indices) == 0:
1237+
# No neurons selected: represent this as an empty spike train
1238+
self.spikes = np.empty((0,), dtype=self.spikes.dtype)
1239+
self.indices = np.array([0], dtype=np.int64)
1240+
self.n_signals = 0
1241+
else:
1242+
new_indices = [0]
1243+
new_spikes = []
1244+
1245+
for i in neuron_indices:
1246+
start = self.indices[i]
1247+
end = self.indices[i + 1]
1248+
neuron_spikes = self.spikes[start:end]
1249+
1250+
new_spikes.append(neuron_spikes)
1251+
new_indices.append(new_indices[-1] + len(neuron_spikes))
1252+
1253+
self.spikes = np.concatenate(new_spikes)
1254+
self.indices = np.array(new_indices, dtype=np.int64)
1255+
self.n_signals = len(neuron_indices)
1256+
11031257
def interpolate(
11041258
self, times: np.ndarray, return_valid: bool = False
11051259
) -> tuple[np.ndarray, np.ndarray] | np.ndarray:

tests/create_screen_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def create_screen_data(
5050
chunk = video_frames[start:end]
5151
if len(chunk) == 0:
5252
continue
53-
video_array = np.stack(chunk, axis=0)
53+
video_array = np.stack(list(chunk), axis=0)
5454
np.save(data_dir / f"{vid_idx+image_frame_count:05d}.npy", video_array)
5555
with open(meta_dir / f"{vid_idx+image_frame_count:05d}.yml", "w") as f:
5656
yaml.safe_dump(

0 commit comments

Comments
 (0)