Skip to content

Commit 5bb0460

Browse files
committed
Use SI parallel machinery
1 parent b29c1c5 commit 5bb0460

1 file changed

Lines changed: 86 additions & 36 deletions

File tree

src/spikeinterface/postprocessing/correlograms.py

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22

33
import importlib.util
44
import warnings
5+
import platform
56
from 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

713
import numpy as np
8-
from joblib import Parallel, delayed
914

1015
from spikeinterface.core import BaseSorting
16+
from spikeinterface.core.job_tools import fix_job_kwargs
1117
from 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+
930980
def compute_acgs_3d(
931981
sorting_analyzer_or_sorting: SortingAnalyzer | BaseSorting,
932982
window_ms: float = 50.0,

0 commit comments

Comments
 (0)