11"""
2- Pipeline on spikes/peaks/detected peaks
3-
4- Functions that can be chained:
5- * after peak detection
6- * already detected peaks
7- * spikes (labeled peaks)
8- to compute some additional features on-the-fly:
9- * peak localization
10- * peak-to-peak
11- * pca
12- * amplitude
13- * amplitude scaling
14- * ...
15-
16- There are two ways for using theses "plugin nodes":
17- * during `peak_detect()`
18- * when peaks are already detected and reduced with `select_peaks()`
19- * on a sorting object
2+
3+
204"""
215
226from __future__ import annotations
@@ -103,6 +87,15 @@ def get_trace_margin(self):
10387 def get_dtype (self ):
10488 return base_peak_dtype
10589
90+ def get_peak_slice (
91+ self ,
92+ segment_index ,
93+ start_frame ,
94+ end_frame ,
95+ ):
96+ # not needed for PeakDetector
97+ raise NotImplementedError
98+
10699
107100# this is used in sorting components
108101class PeakDetector (PeakSource ):
@@ -127,11 +120,18 @@ def get_trace_margin(self):
127120 def get_dtype (self ):
128121 return base_peak_dtype
129122
130- def compute (self , traces , start_frame , end_frame , segment_index , max_margin ):
131- # get local peaks
123+ def get_peak_slice (self , segment_index , start_frame , end_frame , max_margin ):
132124 sl = self .segment_slices [segment_index ]
133125 peaks_in_segment = self .peaks [sl ]
134126 i0 , i1 = np .searchsorted (peaks_in_segment ["sample_index" ], [start_frame , end_frame ])
127+ return i0 , i1
128+
129+ def compute (self , traces , start_frame , end_frame , segment_index , max_margin , peak_slice ):
130+ # get local peaks
131+ sl = self .segment_slices [segment_index ]
132+ peaks_in_segment = self .peaks [sl ]
133+ # i0, i1 = np.searchsorted(peaks_in_segment["sample_index"], [start_frame, end_frame])
134+ i0 , i1 = peak_slice
135135 local_peaks = peaks_in_segment [i0 :i1 ]
136136
137137 # make sample index local to traces
@@ -212,8 +212,7 @@ def get_trace_margin(self):
212212 def get_dtype (self ):
213213 return self ._dtype
214214
215- def compute (self , traces , start_frame , end_frame , segment_index , max_margin ):
216- # get local peaks
215+ def get_peak_slice (self , segment_index , start_frame , end_frame , max_margin ):
217216 sl = self .segment_slices [segment_index ]
218217 peaks_in_segment = self .peaks [sl ]
219218 if self .include_spikes_in_margin :
@@ -222,6 +221,20 @@ def compute(self, traces, start_frame, end_frame, segment_index, max_margin):
222221 )
223222 else :
224223 i0 , i1 = np .searchsorted (peaks_in_segment ["sample_index" ], [start_frame , end_frame ])
224+ return i0 , i1
225+
226+ def compute (self , traces , start_frame , end_frame , segment_index , max_margin , peak_slice ):
227+ # get local peaks
228+ sl = self .segment_slices [segment_index ]
229+ peaks_in_segment = self .peaks [sl ]
230+ # if self.include_spikes_in_margin:
231+ # i0, i1 = np.searchsorted(
232+ # peaks_in_segment["sample_index"], [start_frame - max_margin, end_frame + max_margin]
233+ # )
234+ # else:
235+ # i0, i1 = np.searchsorted(peaks_in_segment["sample_index"], [start_frame, end_frame])
236+ i0 , i1 = peak_slice
237+
225238 local_peaks = peaks_in_segment [i0 :i1 ]
226239
227240 # make sample index local to traces
@@ -467,31 +480,91 @@ def run_node_pipeline(
467480 nodes ,
468481 job_kwargs ,
469482 job_name = "pipeline" ,
470- mp_context = None ,
483+ # mp_context=None,
471484 gather_mode = "memory" ,
472485 gather_kwargs = {},
473486 squeeze_output = True ,
474487 folder = None ,
475488 names = None ,
476489 verbose = False ,
490+ skip_after_n_peaks = None ,
477491):
478492 """
479- Common function to run pipeline with peak detector or already detected peak.
493+ Machinery to compute in parallel operations on peaks and traces.
494+
495+ This useful in several use cases:
496+ * in sortingcomponents : detect peaks and make some computation on then (localize, pca, ...)
497+ * in sortingcomponents : replay some peaks and make some computation on then (localize, pca, ...)
498+ * postprocessing : replay some spikes and make some computation on then (localize, pca, ...)
499+
500+ Here a "peak" is a spike without any labels just a "detected".
501+ Here a "spike" is a spike with any a label so already sorted.
502+
503+ The main idea is to have a graph of nodes.
504+ Every node is doing a computaion of some peaks and related traces.
505+ The first node is PeakSource so either a peak detector PeakDetector or peak/spike replay (PeakRetriever/SpikeRetriever)
506+
507+ Every node can have one or several output that can be directed to other nodes (aka nodes have parents).
508+
509+ Every node can optionally have a global output that will be gathered by the main process.
510+ This is controlled by return_output = True.
511+
512+ The gather consists of concatenating features related to peaks (localization, pca, scaling, ...) into a single big vector.
513+ These vectors can be in "memory" or in files ("npy")
514+
515+
516+ Parameters
517+ ----------
518+
519+ recording: Recording
520+
521+ nodes: a list of PipelineNode
522+
523+ job_kwargs: dict
524+ The classical job_kwargs
525+ job_name : str
526+ The name of the pipeline used for the progress_bar
527+ gather_mode : "memory" | "npz"
528+
529+ gather_kwargs : dict
530+ OPtions to control the "gather engine". See GatherToMemory or GatherToNpy.
531+ squeeze_output : bool, default True
532+ If only one output node then squeeze the tuple
533+ folder : str | Path | None
534+ Used for gather_mode="npz"
535+ names : list of str
536+ Names of outputs.
537+ verbose : bool, default False
538+ Verbosity.
539+ skip_after_n_peaks : None | int
540+ Skip the computation after n_peaks.
541+ This is not an exact because internally this skip is done per worker in average.
542+
543+ Returns
544+ -------
545+ outputs: tuple of np.array | np.array
546+ a tuple of vector for the output of nodes having return_output=True.
547+ If squeeze_output=True and only one output then directly np.array.
480548 """
481549
482550 check_graph (nodes )
483551
484552 job_kwargs = fix_job_kwargs (job_kwargs )
485553 assert all (isinstance (node , PipelineNode ) for node in nodes )
486554
555+ if skip_after_n_peaks is not None :
556+ skip_after_n_peaks_per_worker = skip_after_n_peaks / job_kwargs ["n_jobs" ]
557+ else :
558+ skip_after_n_peaks_per_worker = None
559+
487560 if gather_mode == "memory" :
488561 gather_func = GatherToMemory ()
489562 elif gather_mode == "npy" :
490563 gather_func = GatherToNpy (folder , names , ** gather_kwargs )
491564 else :
492565 raise ValueError (f"wrong gather_mode : { gather_mode } " )
493566
494- init_args = (recording , nodes )
567+ init_args = (recording , nodes , skip_after_n_peaks_per_worker )
495568
496569 processor = ChunkRecordingExecutor (
497570 recording ,
@@ -510,79 +583,103 @@ def run_node_pipeline(
510583 return outs
511584
512585
513- def _init_peak_pipeline (recording , nodes ):
586+ def _init_peak_pipeline (recording , nodes , skip_after_n_peaks_per_worker ):
514587 # create a local dict per worker
515588 worker_ctx = {}
516589 worker_ctx ["recording" ] = recording
517590 worker_ctx ["nodes" ] = nodes
518591 worker_ctx ["max_margin" ] = max (node .get_trace_margin () for node in nodes )
592+ worker_ctx ["skip_after_n_peaks_per_worker" ] = skip_after_n_peaks_per_worker
593+ worker_ctx ["num_peaks" ] = 0
519594 return worker_ctx
520595
521596
522597def _compute_peak_pipeline_chunk (segment_index , start_frame , end_frame , worker_ctx ):
523598 recording = worker_ctx ["recording" ]
524599 max_margin = worker_ctx ["max_margin" ]
525600 nodes = worker_ctx ["nodes" ]
601+ skip_after_n_peaks_per_worker = worker_ctx ["skip_after_n_peaks_per_worker" ]
526602
527603 recording_segment = recording ._recording_segments [segment_index ]
528- traces_chunk , left_margin , right_margin = get_chunk_with_margin (
529- recording_segment , start_frame , end_frame , None , max_margin , add_zeros = True
530- )
604+ node0 = nodes [0 ]
531605
532- # compute the graph
533- pipeline_outputs = {}
534- for node in nodes :
535- node_parents = node .parents if node .parents else list ()
536- node_input_args = tuple ()
537- for parent in node_parents :
538- parent_output = pipeline_outputs [parent ]
539- parent_outputs_tuple = parent_output if isinstance (parent_output , tuple ) else (parent_output ,)
540- node_input_args += parent_outputs_tuple
541- if isinstance (node , PeakDetector ):
542- # to handle compatibility peak detector is a special case
543- # with specific margin
544- # TODO later when in master: change this later
545- extra_margin = max_margin - node .get_trace_margin ()
546- if extra_margin :
547- trace_detection = traces_chunk [extra_margin :- extra_margin ]
606+ if isinstance (node0 , (SpikeRetriever , PeakRetriever )):
607+ # in this case PeakSource could have no peaks and so no need to load traces just skip
608+ peak_slice = i0 , i1 = node0 .get_peak_slice (segment_index , start_frame , end_frame , max_margin )
609+ load_trace_and_compute = i0 < i1
610+ else :
611+ # PeakDetector always need traces
612+ load_trace_and_compute = True
613+
614+ if skip_after_n_peaks_per_worker is not None :
615+ if worker_ctx ["num_peaks" ] > skip_after_n_peaks_per_worker :
616+ load_trace_and_compute = False
617+
618+ if load_trace_and_compute :
619+ traces_chunk , left_margin , right_margin = get_chunk_with_margin (
620+ recording_segment , start_frame , end_frame , None , max_margin , add_zeros = True
621+ )
622+ # compute the graph
623+ pipeline_outputs = {}
624+ for node in nodes :
625+ node_parents = node .parents if node .parents else list ()
626+ node_input_args = tuple ()
627+ for parent in node_parents :
628+ parent_output = pipeline_outputs [parent ]
629+ parent_outputs_tuple = parent_output if isinstance (parent_output , tuple ) else (parent_output ,)
630+ node_input_args += parent_outputs_tuple
631+ if isinstance (node , PeakDetector ):
632+ # to handle compatibility peak detector is a special case
633+ # with specific margin
634+ # TODO later when in master: change this later
635+ extra_margin = max_margin - node .get_trace_margin ()
636+ if extra_margin :
637+ trace_detection = traces_chunk [extra_margin :- extra_margin ]
638+ else :
639+ trace_detection = traces_chunk
640+ node_output = node .compute (trace_detection , start_frame , end_frame , segment_index , max_margin )
641+ # set sample index to local
642+ node_output [0 ]["sample_index" ] += extra_margin
643+ elif isinstance (node , PeakSource ):
644+ node_output = node .compute (traces_chunk , start_frame , end_frame , segment_index , max_margin , peak_slice )
548645 else :
549- trace_detection = traces_chunk
550- node_output = node .compute (trace_detection , start_frame , end_frame , segment_index , max_margin )
551- # set sample index to local
552- node_output [0 ]["sample_index" ] += extra_margin
553- elif isinstance (node , PeakSource ):
554- node_output = node .compute (traces_chunk , start_frame , end_frame , segment_index , max_margin )
555- else :
556- # TODO later when in master: change the signature of all nodes (or maybe not!)
557- node_output = node .compute (traces_chunk , * node_input_args )
558- pipeline_outputs [node ] = node_output
559-
560- # propagate the output
561- pipeline_outputs_tuple = tuple ()
562- for node in nodes :
563- # handle which buffer are given to the output
564- # this is controlled by node.return_output being a bool or tuple of bool
565- out = pipeline_outputs [node ]
566- if isinstance (out , tuple ):
567- if isinstance (node .return_output , bool ) and node .return_output :
568- pipeline_outputs_tuple += out
569- elif isinstance (node .return_output , tuple ):
570- for flag , e in zip (node .return_output , out ):
571- if flag :
572- pipeline_outputs_tuple += (e ,)
573- else :
574- if isinstance (node .return_output , bool ) and node .return_output :
575- pipeline_outputs_tuple += (out ,)
576- elif isinstance (node .return_output , tuple ):
577- # this should not apppend : maybe a checker somewhere before ?
578- pass
646+ # TODO later when in master: change the signature of all nodes (or maybe not!)
647+ node_output = node .compute (traces_chunk , * node_input_args )
648+ pipeline_outputs [node ] = node_output
649+
650+ if skip_after_n_peaks_per_worker is not None and isinstance (node , PeakSource ):
651+ worker_ctx ["num_peaks" ] += node_output [0 ].size
652+
653+ # propagate the output
654+ pipeline_outputs_tuple = tuple ()
655+ for node in nodes :
656+ # handle which buffer are given to the output
657+ # this is controlled by node.return_output being a bool or tuple of bool
658+ out = pipeline_outputs [node ]
659+ if isinstance (out , tuple ):
660+ if isinstance (node .return_output , bool ) and node .return_output :
661+ pipeline_outputs_tuple += out
662+ elif isinstance (node .return_output , tuple ):
663+ for flag , e in zip (node .return_output , out ):
664+ if flag :
665+ pipeline_outputs_tuple += (e ,)
666+ else :
667+ if isinstance (node .return_output , bool ) and node .return_output :
668+ pipeline_outputs_tuple += (out ,)
669+ elif isinstance (node .return_output , tuple ):
670+ # this should not apppend : maybe a checker somewhere before ?
671+ pass
672+
673+ if isinstance (nodes [0 ], PeakDetector ):
674+ # the first out element is the peak vector
675+ # we need to go back to absolut sample index
676+ pipeline_outputs_tuple [0 ]["sample_index" ] += start_frame - left_margin
579677
580- if isinstance (nodes [0 ], PeakDetector ):
581- # the first out element is the peak vector
582- # we need to go back to absolut sample index
583- pipeline_outputs_tuple [0 ]["sample_index" ] += start_frame - left_margin
678+ return pipeline_outputs_tuple
584679
585- return pipeline_outputs_tuple
680+ else :
681+ # the gather will skip this output and not concatenate it
682+ return
586683
587684
588685class GatherToMemory :
@@ -595,6 +692,9 @@ def __init__(self):
595692 self .tuple_mode = None
596693
597694 def __call__ (self , res ):
695+ if res is None :
696+ return
697+
598698 if self .tuple_mode is None :
599699 # first loop only
600700 self .tuple_mode = isinstance (res , tuple )
@@ -655,6 +755,9 @@ def __init__(self, folder, names, npy_header_size=1024, exist_ok=False):
655755 self .final_shapes .append (None )
656756
657757 def __call__ (self , res ):
758+ if res is None :
759+ return
760+
658761 if self .tuple_mode is None :
659762 # first loop only
660763 self .tuple_mode = isinstance (res , tuple )
0 commit comments