22
33import importlib .util
44import warnings
5+ import platform
56from copy import deepcopy
7+ from tqdm .auto import tqdm
8+
9+ from concurrent .futures import ProcessPoolExecutor , ThreadPoolExecutor
10+ import multiprocessing as mp
11+ from threadpoolctl import threadpool_limits
612
713import numpy as np
8- from joblib import Parallel , delayed
914
1015from spikeinterface .core import BaseSorting
16+ from spikeinterface .core .job_tools import fix_job_kwargs
1117from spikeinterface .core .sortinganalyzer import (
1218 AnalyzerExtension ,
1319 SortingAnalyzer ,
@@ -630,9 +636,7 @@ class ComputeACG3D(AnalyzerExtension):
630636 - smoothing_factor (float): width of the boxcar filter for smoothing (in milliseconds).
631637 Default=250ms. Set to None to disable smoothing.
632638 - firing_rate_bins (array-like): Optional predefined firing rate bin edges.
633- If provided, num_firing_rate_bins is ignored.
634- - n_jobs (int): The number of parallel jobs spawned to compute the acgs across units.
635- Defaults to -1 (one job per cpu).
639+ If provided, num_firing_rate_bins is ignored.
636640
637641 Returns
638642 -------
@@ -660,7 +664,7 @@ class ComputeACG3D(AnalyzerExtension):
660664 depend_on = []
661665 need_recording = False
662666 use_nodepipeline = False
663- need_job_kwargs = False
667+ need_job_kwargs = True
664668
665669 def __init__ (self , sorting_analyzer ):
666670 AnalyzerExtension .__init__ (self , sorting_analyzer )
@@ -671,14 +675,12 @@ def _set_params(
671675 bin_ms : float = 1.0 ,
672676 num_firing_rate_quantiles : int = 10 ,
673677 smoothing_factor : int = 250 ,
674- n_jobs : int = - 1 ,
675678 ):
676679 params = dict (
677680 window_ms = window_ms ,
678681 bin_ms = bin_ms ,
679682 num_firing_rate_quantiles = num_firing_rate_quantiles ,
680683 smoothing_factor = smoothing_factor ,
681- n_jobs = n_jobs ,
682684 )
683685
684686 return params
@@ -704,6 +706,7 @@ def _merge_extension_data(
704706 bin_ms = self .params ["bin_ms" ],
705707 num_firing_rate_quantiles = self .params ["num_firing_rate_quantiles" ],
706708 smoothing_factor = self .params ["smoothing_factor" ],
709+ ** job_kwargs ,
707710 )
708711
709712 new_unit_ids_indices = new_sorting .ids_to_indices (new_unit_ids )
@@ -726,8 +729,8 @@ def _merge_extension_data(
726729 )
727730 return new_data
728731
729- def _run (self , verbose = False ):
730- acgs_3d , firing_quantiles , bins = _compute_acgs_3d (self .sorting_analyzer .sorting , ** self .params )
732+ def _run (self , verbose = False , ** job_kwargs ):
733+ acgs_3d , firing_quantiles , bins = _compute_acgs_3d (self .sorting_analyzer .sorting , ** self .params , ** job_kwargs )
731734 self .data ["firing_quantiles" ] = firing_quantiles
732735 self .data ["acgs_3d" ] = acgs_3d
733736 self .data ["bins" ] = bins
@@ -743,7 +746,7 @@ def _compute_acgs_3d(
743746 bin_ms : float = 1.0 ,
744747 num_firing_rate_quantiles : int = 10 ,
745748 smoothing_factor : int = 250 ,
746- n_jobs : int = - 1 ,
749+ ** job_kwargs ,
747750):
748751 """
749752 Computes the 3D autocorrelogram for a single unit.
@@ -784,6 +787,25 @@ def _compute_acgs_3d(
784787 if unit_ids is None :
785788 unit_ids = sorting .unit_ids
786789
790+ job_kwargs = fix_job_kwargs (job_kwargs )
791+
792+ n_jobs = job_kwargs ["n_jobs" ]
793+ progress_bar = job_kwargs ["progress_bar" ]
794+ max_threads_per_worker = job_kwargs ["max_threads_per_worker" ]
795+ mp_context = job_kwargs ["mp_context" ]
796+ pool_engine = job_kwargs ["pool_engine" ]
797+ if mp_context is not None and platform .system () == "Windows" :
798+ assert mp_context != "fork" , "'fork' mp_context not supported on Windows!"
799+ elif mp_context == "fork" and platform .system () == "Darwin" :
800+ warnings .warn ('As of Python 3.8 "fork" is no longer considered safe on macOS' )
801+
802+ num_segments = sorting .get_num_segments ()
803+ if num_segments > 1 :
804+ warnings .warn (
805+ "Multiple segments detected. Firing rate quantiles will be automatically computed on the first segment. "
806+ "Manually define global firing_rate_quantiles if needed." ,
807+ )
808+
787809 # pre-compute time bins
788810 winsize_bins = 2 * int (0.5 * window_ms * 1.0 / bin_ms ) + 1
789811 bin_times_ms = np .linspace (- window_ms / 2 , window_ms / 2 , num = winsize_bins )
@@ -794,23 +816,45 @@ def _compute_acgs_3d(
794816
795817 time_bins_ms = np .repeat (bin_times_ms , num_units , axis = 0 )
796818
797- # Process units in parallel
798- results = Parallel (n_jobs = n_jobs )(
799- delayed (_compute_3d_acg_one_unit )(
800- sorting ,
801- unit_id ,
802- window_ms ,
803- bin_ms ,
804- num_firing_rate_quantiles = num_firing_rate_quantiles ,
805- smoothing_factor = smoothing_factor ,
806- )
819+ items = [
820+ (sorting , unit_id , window_ms , bin_ms , num_firing_rate_quantiles , smoothing_factor , max_threads_per_worker )
807821 for unit_id in unit_ids
808- )
809-
810- # Unpack results
811- for unit_index , (acg_3d , firing_quantile ) in enumerate (results ):
812- acgs_3d [unit_index , :, :] = acg_3d
813- firing_quantiles [unit_index , :] = firing_quantile
822+ ]
823+
824+ if n_jobs > 1 :
825+ job_name = "calculate_acgs_3d"
826+ if pool_engine == "process" :
827+ parallel_pool_class = ProcessPoolExecutor
828+ pool_kwargs = dict (mp_context = mp .get_context (mp_context ))
829+ desc = f"{ job_name } (workers: { n_jobs } processes)"
830+ else :
831+ parallel_pool_class = ThreadPoolExecutor
832+ pool_kwargs = dict ()
833+ desc = f"{ job_name } (workers: { n_jobs } threads)"
834+
835+ # Process units in parallel
836+ with parallel_pool_class (max_workers = n_jobs , ** pool_kwargs ) as executor :
837+ results = executor .map (_compute_3d_acg_one_unit_star , items )
838+ if progress_bar :
839+ results = tqdm (results , total = len (unit_ids ), desc = desc )
840+ for unit_index , (acg_3d , firing_quantile ) in enumerate (results ):
841+ acgs_3d [unit_index , :, :] = acg_3d
842+ firing_quantiles [unit_index , :] = firing_quantile
843+ else :
844+ # Process units in serial
845+ for unit_index , (sorting , unit_id , window_ms , bin_ms , num_firing_rate_quantiles , smoothing_factor ) in enumerate (
846+ items
847+ ):
848+ acg_3d , firing_quantile = _compute_3d_acg_one_unit (
849+ sorting ,
850+ unit_id ,
851+ window_ms ,
852+ bin_ms ,
853+ num_firing_rate_quantiles ,
854+ smoothing_factor ,
855+ )
856+ acgs_3d [unit_index , :, :] = acg_3d
857+ firing_quantiles [unit_index , :] = firing_quantile
814858
815859 return acgs_3d , firing_quantiles , time_bins_ms
816860
@@ -848,17 +892,9 @@ def _compute_3d_acg_one_unit(
848892 # Samples per bin
849893 samples_per_bin = int (np .ceil (fs / (1000 / bin_size )))
850894
851- segments = sorting .get_num_segments ()
852- if segments > 1 and firing_rate_quantiles is None :
853- warnings .warn (
854- "Multiple segments detected. Firing rate quantiles will be automatically computed on the first segment."
855- " Manually define global firing_rate_quantiles if needed." ,
856- UserWarning ,
857- 2 ,
858- )
859-
895+ num_segments = sorting .get_num_segments ()
860896 # Convert times_1 and times_2 (which are in units of fs to units of bin_size)
861- for segment_index in range (segments ):
897+ for segment_index in range (num_segments ):
862898 spike_times = sorting .get_unit_spike_train (unit_id , segment_index = segment_index )
863899
864900 # Convert to bin indices
@@ -927,6 +963,20 @@ def _compute_3d_acg_one_unit(
927963 return acg_3d , firing_rate_quantiles
928964
929965
966+ def _compute_3d_acg_one_unit_star (args ):
967+ """
968+ Helper function to compute the 3D ACG for a single unit.
969+ This is used to parallelize the computation.
970+ """
971+ max_threads_per_worker = args [- 1 ]
972+ new_args = args [:- 1 ]
973+ if max_threads_per_worker is None :
974+ return _compute_3d_acg_one_unit (* new_args )
975+ else :
976+ with threadpool_limits (limits = int (max_threads_per_worker )):
977+ return _compute_3d_acg_one_unit (* new_args )
978+
979+
930980def compute_acgs_3d (
931981 sorting_analyzer_or_sorting : SortingAnalyzer | BaseSorting ,
932982 window_ms : float = 50.0 ,
0 commit comments