Skip to content

Commit 3e42928

Browse files
committed
Shift times using writable copies of OOM arrays
Recordings loaded from disk may have time vectors that are out-of-memory (OOM) arrays: read-only mem-maps or zarr.Array. These can't be shifted in place, and need to be materialized as as numpy arrays before shifting.
1 parent 46a717b commit 3e42928

2 files changed

Lines changed: 32 additions & 6 deletions

File tree

src/spikeinterface/core/tests/test_time_handling.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,33 @@ def test_save_and_load_time_shift(self, request, fixture_name, tmp_path):
375375
times_recording.get_times(segment_index=idx), loaded_recording.get_times(segment_index=idx)
376376
)
377377

378+
@pytest.mark.parametrize("save_format", ["binary", "zarr"])
379+
def test_shift_times_after_load(self, request, save_format, tmp_path):
380+
"""
381+
Shift times on a recording loaded from disk as a read-only np.memmap
382+
(binary folder) and a lazy zarr.Array (zarr). Neither supports an in-place
383+
`+=`, so `shift_times` must shift a writable copy.
384+
"""
385+
_, times_recording, all_times = self._get_fixture_data(request, "time_vector_recording")
386+
387+
folder = tmp_path / "rec"
388+
times_recording.save(format=save_format, folder=folder)
389+
load_path = folder.with_suffix(".zarr") if save_format == "zarr" else folder
390+
loaded = si.load(load_path)
391+
392+
# Confirm we are actually exercising a non-writeable / non-ndarray path.
393+
for idx in range(loaded.get_num_segments()):
394+
tv = loaded.segments[idx].time_vector
395+
assert not (isinstance(tv, np.ndarray) and tv.flags.writeable)
396+
397+
shift = 123.456
398+
loaded.shift_times(shift)
399+
400+
for idx in range(loaded.get_num_segments()):
401+
assert np.allclose(
402+
loaded.get_times(segment_index=idx), all_times[idx] + shift, rtol=0, atol=1e-8
403+
)
404+
378405
def _store_all_times(self, recording):
379406
"""
380407
Convenience function to store original times of all segments to a dict.

src/spikeinterface/core/time_series.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,12 @@ def shift_times(self, shift: int | float, segment_index: int | None = None) -> N
273273
rs = self.segments[segment_index]
274274

275275
if self.has_time_vector(segment_index=segment_index):
276-
if rs.time_vector.flags.writeable:
277-
# If the time_vector is writeable, shift in-place to avoid a copy.
278-
rs.time_vector += shift
276+
if isinstance(rs.time_vector, np.ndarray) and rs.time_vector.flags.writeable:
277+
# If this is an in-memory numpy array
278+
rs.time_vector += shift # in-place, no copy
279279
else:
280-
# If the time_vector is a memmap from `np.load(..., mmap_mode='r')`,
281-
# in-place modification would error, so we shift a writable copy.
282-
rs.time_vector = rs.time_vector + shift
280+
# If this is a read-only memmap or zarr.Array
281+
rs.time_vector = np.asarray(rs.time_vector) + shift
283282
else:
284283
new_start_time = 0 + shift if rs.t_start is None else rs.t_start + shift
285284
rs.t_start = new_start_time

0 commit comments

Comments
 (0)