Skip to content

Commit d2d5942

Browse files
authored
Merge branch 'main' into fast-correlogram-merge
2 parents 9d95496 + 12a1276 commit d2d5942

5 files changed

Lines changed: 146 additions & 52 deletions

File tree

src/spikeinterface/core/node_pipeline.py

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def __init__(
191191
self._dtype = spike_peak_dtype
192192

193193
self.include_spikes_in_margin = include_spikes_in_margin
194-
if include_spikes_in_margin is not None:
194+
if include_spikes_in_margin:
195195
self._dtype = spike_peak_dtype + [("in_margin", "bool")]
196196

197197
self.peaks = sorting_to_peaks(sorting, extremum_channel_inds, self._dtype)
@@ -228,12 +228,6 @@ def compute(self, traces, start_frame, end_frame, segment_index, max_margin, pea
228228
# get local peaks
229229
sl = self.segment_slices[segment_index]
230230
peaks_in_segment = self.peaks[sl]
231-
# if self.include_spikes_in_margin:
232-
# i0, i1 = np.searchsorted(
233-
# peaks_in_segment["sample_index"], [start_frame - max_margin, end_frame + max_margin]
234-
# )
235-
# else:
236-
# i0, i1 = np.searchsorted(peaks_in_segment["sample_index"], [start_frame, end_frame])
237231
i0, i1 = peak_slice
238232

239233
local_peaks = peaks_in_segment[i0:i1]
@@ -435,21 +429,59 @@ def compute(self, traces, peaks):
435429
return sparse_wfs
436430

437431

438-
def find_parent_of_type(list_of_parents, parent_type, unique=True):
432+
def find_parent_of_type(list_of_parents, parent_type):
433+
"""
434+
Find a single parent of a given type(s) in a list of parents.
435+
If multiple parents of the given type are found, the first parent is returned.
436+
437+
Parameters
438+
----------
439+
list_of_parents : list of PipelineNode
440+
List of parents to search through.
441+
parent_type : type | tuple of types
442+
The type of parent to search for.
443+
444+
Returns
445+
-------
446+
parent : PipelineNode or None
447+
The parent of the given type. Returns None if no parent of the given type is found.
448+
"""
439449
if list_of_parents is None:
440450
return None
441451

452+
parents = find_parents_of_type(list_of_parents, parent_type)
453+
454+
if len(parents) > 0:
455+
return parents[0]
456+
else:
457+
return None
458+
459+
460+
def find_parents_of_type(list_of_parents, parent_type):
461+
"""
462+
Find all parents of a given type(s) in a list of parents.
463+
464+
Parameters
465+
----------
466+
list_of_parents : list of PipelineNode
467+
List of parents to search through.
468+
parent_type : type | tuple of types
469+
The type(s) of parents to search for.
470+
471+
Returns
472+
-------
473+
parents : list of PipelineNode
474+
List of parents of the given type(s). Returns an empty list if no parents of the given type(s) are found.
475+
"""
476+
if list_of_parents is None:
477+
return []
478+
442479
parents = []
443480
for parent in list_of_parents:
444481
if isinstance(parent, parent_type):
445482
parents.append(parent)
446483

447-
if unique and len(parents) == 1:
448-
return parents[0]
449-
elif not unique and len(parents) > 1:
450-
return parents[0]
451-
else:
452-
return None
484+
return parents
453485

454486

455487
def check_graph(nodes):
@@ -471,7 +503,7 @@ def check_graph(nodes):
471503
assert parent in nodes, f"Node {node} has parent {parent} that was not passed in nodes"
472504
assert (
473505
nodes.index(parent) < i
474-
), f"Node are ordered incorrectly: {node} before {parent} in the pipeline definition."
506+
), f"Nodes are ordered incorrectly: {node} before {parent} in the pipeline definition."
475507

476508
return nodes
477509

@@ -607,12 +639,16 @@ def _compute_peak_pipeline_chunk(segment_index, start_frame, end_frame, worker_c
607639
skip_after_n_peaks_per_worker = worker_ctx["skip_after_n_peaks_per_worker"]
608640

609641
recording_segment = recording._recording_segments[segment_index]
610-
node0 = nodes[0]
611-
612-
if isinstance(node0, (SpikeRetriever, PeakRetriever)):
613-
# in this case PeakSource could have no peaks and so no need to load traces just skip
614-
peak_slice = i0, i1 = node0.get_peak_slice(segment_index, start_frame, end_frame, max_margin)
615-
load_trace_and_compute = i0 < i1
642+
retrievers = find_parents_of_type(nodes, (SpikeRetriever, PeakRetriever))
643+
# get peak slices once for all retrievers
644+
peak_slice_by_retriever = {}
645+
for retriever in retrievers:
646+
peak_slice = i0, i1 = retriever.get_peak_slice(segment_index, start_frame, end_frame, max_margin)
647+
peak_slice_by_retriever[retriever] = peak_slice
648+
649+
if len(peak_slice_by_retriever) > 0:
650+
# in this case the retrievers could have no peaks, so we test if any spikes are in the chunk
651+
load_trace_and_compute = any(i0 < i1 for i0, i1 in peak_slice_by_retriever.values())
616652
else:
617653
# PeakDetector always need traces
618654
load_trace_and_compute = True
@@ -646,7 +682,8 @@ def _compute_peak_pipeline_chunk(segment_index, start_frame, end_frame, worker_c
646682
node_output = node.compute(trace_detection, start_frame, end_frame, segment_index, max_margin)
647683
# set sample index to local
648684
node_output[0]["sample_index"] += extra_margin
649-
elif isinstance(node, PeakSource):
685+
elif isinstance(node, (PeakRetriever, SpikeRetriever)):
686+
peak_slice = peak_slice_by_retriever[node]
650687
node_output = node.compute(traces_chunk, start_frame, end_frame, segment_index, max_margin, peak_slice)
651688
else:
652689
# TODO later when in master: change the signature of all nodes (or maybe not!)

src/spikeinterface/core/sortinganalyzer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,19 +2033,18 @@ def load(cls, sorting_analyzer):
20332033
return None
20342034

20352035
def load_run_info(self):
2036+
run_info = None
20362037
if self.format == "binary_folder":
20372038
extension_folder = self._get_binary_extension_folder()
20382039
run_info_file = extension_folder / "run_info.json"
20392040
if run_info_file.is_file():
20402041
with open(str(run_info_file), "r") as f:
20412042
run_info = json.load(f)
2042-
else:
2043-
warnings.warn(f"Found no run_info file for {self.extension_name}, extension should be re-computed.")
2044-
run_info = None
20452043

20462044
elif self.format == "zarr":
20472045
extension_group = self._get_zarr_extension_group(mode="r")
20482046
run_info = extension_group.attrs.get("run_info", None)
2047+
20492048
if run_info is None:
20502049
warnings.warn(f"Found no run_info file for {self.extension_name}, extension should be re-computed.")
20512050
self.run_info = run_info

src/spikeinterface/postprocessing/template_metrics.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,18 @@ def _run(self, verbose=False):
327327
)
328328

329329
existing_metrics = []
330-
tm_extension = self.sorting_analyzer.get_extension("template_metrics")
331-
if (
332-
delete_existing_metrics is False
333-
and tm_extension is not None
334-
and tm_extension.data.get("metrics") is not None
335-
):
336-
existing_metrics = tm_extension.params["metric_names"]
330+
331+
# Check if we need to propogate any old metrics. If so, we'll do that.
332+
# Otherwise, we'll avoid attempting to load an empty template_metrics.
333+
if set(self.params["metrics_to_compute"]) != set(self.params["metric_names"]):
334+
335+
tm_extension = self.sorting_analyzer.get_extension("template_metrics")
336+
if (
337+
delete_existing_metrics is False
338+
and tm_extension is not None
339+
and tm_extension.data.get("metrics") is not None
340+
):
341+
existing_metrics = tm_extension.params["metric_names"]
337342

338343
existing_metrics = []
339344
# here we get in the loaded via the dict only (to avoid full loading from disk after params reset)

src/spikeinterface/sortingcomponents/motion/motion_utils.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -587,10 +587,14 @@ def ensure_time_bins(time_bin_centers_s=None, time_bin_edges_s=None):
587587
Going from centers to edges is done by taking midpoints and padding with the
588588
left and rightmost centers.
589589
590+
To handle multi segment, this function is working both:
591+
* array/array input
592+
* list[array]/list[array] input
593+
590594
Parameters
591595
----------
592-
time_bin_centers_s : None or np.array
593-
time_bin_edges_s : None or np.array
596+
time_bin_centers_s : None or np.array or list[np.array]
597+
time_bin_edges_s : None or np.array or list[np.array]
594598
595599
Returns
596600
-------
@@ -600,17 +604,34 @@ def ensure_time_bins(time_bin_centers_s=None, time_bin_edges_s=None):
600604
raise ValueError("Need at least one of time_bin_centers_s or time_bin_edges_s.")
601605

602606
if time_bin_centers_s is None:
603-
assert time_bin_edges_s.ndim == 1 and time_bin_edges_s.size >= 2
604-
time_bin_centers_s = 0.5 * (time_bin_edges_s[1:] + time_bin_edges_s[:-1])
607+
if isinstance(time_bin_edges_s, list):
608+
# multi segment cas
609+
time_bin_centers_s = []
610+
for be in time_bin_edges_s:
611+
bc, _ = ensure_time_bins(time_bin_centers_s=None, time_bin_edges_s=be)
612+
time_bin_centers_s.append(bc)
613+
else:
614+
# simple segment
615+
assert time_bin_edges_s.ndim == 1 and time_bin_edges_s.size >= 2
616+
time_bin_centers_s = 0.5 * (time_bin_edges_s[1:] + time_bin_edges_s[:-1])
605617

606618
if time_bin_edges_s is None:
607-
time_bin_edges_s = np.empty(time_bin_centers_s.shape[0] + 1, dtype=time_bin_centers_s.dtype)
608-
time_bin_edges_s[[0, -1]] = time_bin_centers_s[[0, -1]]
609-
if time_bin_centers_s.size > 2:
610-
time_bin_edges_s[1:-1] = 0.5 * (time_bin_centers_s[1:] + time_bin_centers_s[:-1])
619+
if isinstance(time_bin_centers_s, list):
620+
# multi segment cas
621+
time_bin_edges_s = []
622+
for bc in time_bin_centers_s:
623+
_, be = ensure_time_bins(time_bin_centers_s=bc, time_bin_edges_s=None)
624+
time_bin_edges_s.append(be)
625+
else:
626+
# simple segment
627+
time_bin_edges_s = np.empty(time_bin_centers_s.shape[0] + 1, dtype=time_bin_centers_s.dtype)
628+
time_bin_edges_s[[0, -1]] = time_bin_centers_s[[0, -1]]
629+
if time_bin_centers_s.size > 2:
630+
time_bin_edges_s[1:-1] = 0.5 * (time_bin_centers_s[1:] + time_bin_centers_s[:-1])
611631

612632
return time_bin_centers_s, time_bin_edges_s
613633

614634

635+
615636
def ensure_time_bin_edges(time_bin_centers_s=None, time_bin_edges_s=None):
616637
return ensure_time_bins(time_bin_centers_s, time_bin_edges_s)[1]

src/spikeinterface/sortingcomponents/motion/tests/test_motion_interpolation.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,26 @@
1010
interpolate_motion_on_traces,
1111
)
1212
from spikeinterface.sortingcomponents.tests.common import make_dataset
13-
13+
from spikeinterface.core import generate_ground_truth_recording
1414

1515
def make_fake_motion(rec):
1616
# make a fake motion object
17-
duration = rec.get_total_duration()
17+
1818
locs = rec.get_channel_locations()
19-
temporal_bins = np.arange(0.5, duration - 0.49, 0.5)
2019
spatial_bins = np.arange(locs[:, 1].min(), locs[:, 1].max(), 100)
21-
displacement = np.zeros((temporal_bins.size, spatial_bins.size))
22-
displacement[:, :] = np.linspace(-30, 30, temporal_bins.size)[:, None]
2320

24-
motion = Motion([displacement], [temporal_bins], spatial_bins, direction="y")
21+
displacement = []
22+
temporal_bins = []
23+
for segment_index in range(rec.get_num_segments()):
24+
duration = rec.get_duration(segment_index=segment_index)
25+
seg_time_bins = np.arange(0.5, duration - 0.49, 0.5)
26+
seg_disp = np.zeros((seg_time_bins.size, spatial_bins.size))
27+
seg_disp[:, :] = np.linspace(-30, 30, seg_time_bins.size)[:, None]
28+
29+
temporal_bins.append(seg_time_bins)
30+
displacement.append(seg_disp)
31+
32+
motion = Motion(displacement, temporal_bins, spatial_bins, direction="y")
2533

2634
return motion
2735

@@ -176,7 +184,27 @@ def test_cross_band_interpolation():
176184

177185

178186
def test_InterpolateMotionRecording():
179-
rec, sorting = make_dataset()
187+
# rec, sorting = make_dataset()
188+
189+
# 2 segments
190+
rec, sorting = generate_ground_truth_recording(
191+
durations=[30.0],
192+
sampling_frequency=30000.0,
193+
num_channels=32,
194+
num_units=10,
195+
generate_probe_kwargs=dict(
196+
num_columns=2,
197+
xpitch=20,
198+
ypitch=20,
199+
contact_shapes="circle",
200+
contact_shape_params={"radius": 6},
201+
),
202+
generate_sorting_kwargs=dict(firing_rates=6.0, refractory_period_ms=4.0),
203+
noise_kwargs=dict(noise_levels=5.0, strategy="on_the_fly"),
204+
seed=2205,
205+
)
206+
207+
180208
motion = make_fake_motion(rec)
181209

182210
rec2 = InterpolateMotionRecording(rec, motion, border_mode="force_extrapolate")
@@ -187,15 +215,19 @@ def test_InterpolateMotionRecording():
187215

188216
rec2 = InterpolateMotionRecording(rec, motion, border_mode="remove_channels")
189217
assert rec2.channel_ids.size == 24
190-
for ch_id in (0, 1, 14, 15, 16, 17, 30, 31):
218+
for ch_id in ("0", "1", "14", "15", "16", "17", "30", "31"):
191219
assert ch_id not in rec2.channel_ids
192220

193221
traces = rec2.get_traces(segment_index=0, start_frame=0, end_frame=30000)
194222
assert traces.shape == (30000, 24)
195223

196-
traces = rec2.get_traces(segment_index=0, start_frame=0, end_frame=30000, channel_ids=[3, 4])
224+
traces = rec2.get_traces(segment_index=0, start_frame=0, end_frame=30000, channel_ids=["3", "4"])
197225
assert traces.shape == (30000, 2)
198226

227+
# test dump.load when multi segments
228+
rec2.dump("rec_motion_interp.pickle")
229+
rec3 = sc.load("rec_motion_interp.pickle")
230+
199231
# import matplotlib.pyplot as plt
200232
# import spikeinterface.widgets as sw
201233
# fig, ax = plt.subplots()
@@ -207,7 +239,7 @@ def test_InterpolateMotionRecording():
207239

208240
if __name__ == "__main__":
209241
# test_correct_motion_on_peaks()
210-
test_interpolate_motion_on_traces()
242+
# test_interpolate_motion_on_traces()
211243
# test_interpolation_simple()
212-
# test_InterpolateMotionRecording()
213-
test_cross_band_interpolation()
244+
test_InterpolateMotionRecording()
245+
# test_cross_band_interpolation()

0 commit comments

Comments
 (0)