From 4f73dd1cd5f7990ace9f6f8d962b218f406e4692 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 11 Sep 2023 16:37:59 +0200 Subject: [PATCH 01/44] WIP: firing_range and amplitude_spread --- doc/modules/qualitymetrics/firing_range.rst | 48 ++++++++++ .../qualitymetrics/misc_metrics.py | 94 ++++++++++++++++++- .../qualitymetrics/quality_metric_list.py | 2 + 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 doc/modules/qualitymetrics/firing_range.rst diff --git a/doc/modules/qualitymetrics/firing_range.rst b/doc/modules/qualitymetrics/firing_range.rst new file mode 100644 index 0000000000..fd8f79682c --- /dev/null +++ b/doc/modules/qualitymetrics/firing_range.rst @@ -0,0 +1,48 @@ +Firing range (:code:`firing_range`) +=================================== + + +Calculation +----------- + +The firing range indicates the spread of the firing range of a unit across the recording. It is computed by +taking the difference between the 95-th and 5th percentiles firing rates computed over short time bins (e.g. 10 s). + + + +Expectation and use +------------------- + +Both very high and very low firing rates can indicate errors. +Highly contaminated units (type I error) may have high firing rates as a result of the inclusion of other neurons' spikes. +Low firing rate units are likely to be incomplete (type II error), although this is not always the case (some neurons have highly selective firing patterns). +The firing rate is expected to be approximately log-normally distributed [Buzsáki]_. + +Example code +------------ + +.. code-block:: python + + import spikeinterface.qualitymetrics as qm + + # Make recording, sorting and wvf_extractor object for your data. + firing_rate = qm.compute_firing_ranges(wvf_extractor) + # firing_rate is a dict containing the units' IDs as keys, + # and their firing rates across segments as values (in Hz). + +References +---------- + +.. autofunction:: spikeinterface.qualitymetrics.misc_metrics.compute_firing_rates + + +Links to original implementations +--------------------------------- + +* From the `AllenSDK `_ + +Literature +---------- + +Unknown origin. +Widely discussed eg: [Buzsáki]_. diff --git a/src/spikeinterface/qualitymetrics/misc_metrics.py b/src/spikeinterface/qualitymetrics/misc_metrics.py index ee28485983..9be9a32ff6 100644 --- a/src/spikeinterface/qualitymetrics/misc_metrics.py +++ b/src/spikeinterface/qualitymetrics/misc_metrics.py @@ -563,7 +563,99 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), **k return synchrony_metrics -_default_params["synchrony_metrics"] = dict(synchrony_sizes=(0, 2, 4)) +_default_params["synchrony"] = dict(synchrony_sizes=(0, 2, 4)) + + +def compute_firing_ranges(waveform_extractor, bin_size_s=5, quantiles=(0.05, 0.95), unit_ids=None): + """ + Compute firing range, the range between the 5th and 95th quantiles of the firing rates distribution + computed in non-overlapping time bins. + + Parameters + ---------- + waveform_extractor : WaveformExtractor + The waveform extractor object. + bin_size_s : float, default: 5 + The size of the bin in seconds. + quantiles : tuple, default: (0.05, 0.95) + The quantiles to compute. + + Returns + ------- + firing_ranges : dict + The firing range for each unit. + """ + sampling_frequency = waveform_extractor.sampling_frequency + bin_size_samples = int(bin_size_s * sampling_frequency) + sorting = waveform_extractor.sorting + if unit_ids is None: + unit_ids = sorting.unit_ids + + # for each segment, we compute the firing rate histogram and we concatenate them + firing_rate_histograms = {unit_id: np.array([], dtype=float) for unit_id in sorting.unit_ids} + for segment_index in range(waveform_extractor.get_num_segments()): + num_samples = waveform_extractor.get_num_samples(segment_index) + edges = np.arange(0, num_samples + 1, bin_size_samples) + + for unit_id in unit_ids: + spike_times = sorting.get_unit_spike_train(unit_id=unit_id, segment_index=segment_index) + spike_counts, _ = np.histogram(spike_times, bins=edges) + firing_rates = spike_counts / bin_size_s + firing_rate_histograms[unit_id] = np.concatentate((firing_rate_histograms[unit_id], firing_rates)) + + # finally we compute the percentiles + firing_ranges = {} + for unit_id in unit_ids: + firing_ranges[unit_id] = np.percentile(firing_rate_histograms[unit_id], quantiles[1]) - np.percentile( + firing_rate_histograms[unit_id], quantiles[0] + ) + + return firing_ranges + + +_default_params["firing_range"] = dict(bin_size_s=5, quantiles=(0.05, 0.95)) + + +# TODO: docs +def compute_amplitude_spreads( + waveform_extractor, spikes_bin_size=50, amplitude_extension="spike_amplitudes", unit_ids=None +): + """Calculate mean spread of spike amplitudes within defined bins of AP events + + S Musall 2023 + + Input: + ------ + amplitudes : numpy.ndarray + Array of amplitudes (don't need to be in physical units) + + """ + sorting = waveform_extractor.sorting + spikes = sorting.to_spike_vector() + num_spikes = sorting.count_num_spikes_per_unit() + if unit_ids is None: + unit_ids = sorting.unit_ids + + if waveform_extractor.is_extension(amplitude_extension): + sac = waveform_extractor.load_extension(amplitude_extension) + amps = sac.get_data(outputs="concatenated") + else: + warnings.warn("") + empty_dict = {unit_id: np.nan for unit_id in unit_ids} + return empty_dict + + all_unit_ids = list(sorting.unit_ids) + for unit_id in unit_ids: + amps_unit = amps[spikes["unit_index"] == all_unit_ids.index(unit_id)] + if num_spikes[unit_id] < spikes_bin_size: + amp_spread = np.var(amps_unit) + else: + amp_spread = [] + for i in range(0, num_spikes[unit_id], spikes_bin_size): + amp_spread.append(np.var(amps_unit[i : i + spikes_bin_size])) + amp_spread = np.median(amp_spread) + + return amp_spread def compute_amplitude_cutoffs( diff --git a/src/spikeinterface/qualitymetrics/quality_metric_list.py b/src/spikeinterface/qualitymetrics/quality_metric_list.py index 90dbb47a3a..917927f44a 100644 --- a/src/spikeinterface/qualitymetrics/quality_metric_list.py +++ b/src/spikeinterface/qualitymetrics/quality_metric_list.py @@ -12,6 +12,7 @@ compute_amplitude_medians, compute_drift_metrics, compute_synchrony_metrics, + compute_firing_ranges, ) from .pca_metrics import ( @@ -41,5 +42,6 @@ "amplitude_cutoff": compute_amplitude_cutoffs, "amplitude_median": compute_amplitude_medians, "synchrony": compute_synchrony_metrics, + "firing_range": compute_firing_ranges, "drift": compute_drift_metrics, } From b91ff2e774de0b2ee04f1ed6e075962e1c30d468 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 12 Sep 2023 15:24:44 +0200 Subject: [PATCH 02/44] Add amplitude_spread --- .../qualitymetrics/amplitude_spread.rst | 48 ++++++++++++++ doc/modules/qualitymetrics/drift.rst | 1 + doc/modules/qualitymetrics/firing_range.rst | 24 +++---- .../qualitymetrics/misc_metrics.py | 66 ++++++++++++++----- .../qualitymetrics/quality_metric_list.py | 2 + .../tests/test_metrics_functions.py | 26 +++++++- 6 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 doc/modules/qualitymetrics/amplitude_spread.rst diff --git a/doc/modules/qualitymetrics/amplitude_spread.rst b/doc/modules/qualitymetrics/amplitude_spread.rst new file mode 100644 index 0000000000..0ae0761265 --- /dev/null +++ b/doc/modules/qualitymetrics/amplitude_spread.rst @@ -0,0 +1,48 @@ +Amplitude spread (:code:`amplitude_spread`) +=========================================== + + +Calculation +----------- + +The amplitude spread is a measure of the amplitude variability. +It is computed the ratio between the standard deviation and the amplitude mean (aka coefficient of variation). +To obtain a better estimate of this measure, it is first computed separately for several bins of a prefixed number of spikes +(e.g 100) and then the median of these values is taken. + +The computation requires either spike amplitudes (see :py:func:`~spikeinterface.postprocessing.compute_spike_amplitudes()`) +or amplitude scalings (see :py:func:`~spikeinterface.postprocessing.compute_amplitude_scalings()`) to be pre-computed. + + +Expectation and use +------------------- + +Very high levels of amplitude_spread ranges, outside of a physiolocigal range, might indicate noise contamination. + + +Example code +------------ + +.. code-block:: python + + import spikeinterface.qualitymetrics as qm + + # Make recording, sorting and wvf_extractor object for your data. + # It is required to run `compute_spike_amplitudes(wvf_extractor)` or + # `compute_amplitude_scalings(wvf_extractor)` (if missing, values will be NaN) + amplitude_spread = qm.compute_firing_ranges(wvf_extractor, amplitude_extension='spike_amplitudes') + # amplitude_spread is a dict containing the units' IDs as keys, + # and their amplitude_spread (in units of standard deviation). + + + +References +---------- + +.. autofunction:: spikeinterface.qualitymetrics.misc_metrics.compute_amplitude_spreads + + +Literature +---------- + +Designed by Simon Musall and adapted to SpikeInterface by Alessio Buccino. diff --git a/doc/modules/qualitymetrics/drift.rst b/doc/modules/qualitymetrics/drift.rst index 0a852f80af..4e78150ba7 100644 --- a/doc/modules/qualitymetrics/drift.rst +++ b/doc/modules/qualitymetrics/drift.rst @@ -42,6 +42,7 @@ Example code import spikeinterface.qualitymetrics as qm + # Make recording, sorting and wvf_extractor object for your data. # It is required to run `compute_spike_locations(wvf_extractor)` # (if missing, values will be NaN) drift_ptps, drift_stds, drift_mads = qm.compute_drift_metrics(wvf_extractor, peak_sign="neg") diff --git a/doc/modules/qualitymetrics/firing_range.rst b/doc/modules/qualitymetrics/firing_range.rst index fd8f79682c..0d17eedc13 100644 --- a/doc/modules/qualitymetrics/firing_range.rst +++ b/doc/modules/qualitymetrics/firing_range.rst @@ -5,7 +5,7 @@ Firing range (:code:`firing_range`) Calculation ----------- -The firing range indicates the spread of the firing range of a unit across the recording. It is computed by +The firing range indicates the dispersion of the firing rate of a unit across the recording. It is computed by taking the difference between the 95-th and 5th percentiles firing rates computed over short time bins (e.g. 10 s). @@ -13,10 +13,8 @@ taking the difference between the 95-th and 5th percentiles firing rates compute Expectation and use ------------------- -Both very high and very low firing rates can indicate errors. -Highly contaminated units (type I error) may have high firing rates as a result of the inclusion of other neurons' spikes. -Low firing rate units are likely to be incomplete (type II error), although this is not always the case (some neurons have highly selective firing patterns). -The firing rate is expected to be approximately log-normally distributed [Buzsáki]_. +Very high levels of firing ranges, outside of a physiolocigal range, might indicate noise contamination. + Example code ------------ @@ -26,23 +24,17 @@ Example code import spikeinterface.qualitymetrics as qm # Make recording, sorting and wvf_extractor object for your data. - firing_rate = qm.compute_firing_ranges(wvf_extractor) - # firing_rate is a dict containing the units' IDs as keys, - # and their firing rates across segments as values (in Hz). + firing_range = qm.compute_firing_ranges(wvf_extractor) + # firing_range is a dict containing the units' IDs as keys, + # and their firing firing_range as values (in Hz). References ---------- -.. autofunction:: spikeinterface.qualitymetrics.misc_metrics.compute_firing_rates - - -Links to original implementations ---------------------------------- +.. autofunction:: spikeinterface.qualitymetrics.misc_metrics.compute_firing_ranges -* From the `AllenSDK `_ Literature ---------- -Unknown origin. -Widely discussed eg: [Buzsáki]_. +Designed by Simon Musall and adapted to SpikeInterface by Alessio Buccino. diff --git a/src/spikeinterface/qualitymetrics/misc_metrics.py b/src/spikeinterface/qualitymetrics/misc_metrics.py index 9be9a32ff6..6c237ee720 100644 --- a/src/spikeinterface/qualitymetrics/misc_metrics.py +++ b/src/spikeinterface/qualitymetrics/misc_metrics.py @@ -567,8 +567,7 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), **k def compute_firing_ranges(waveform_extractor, bin_size_s=5, quantiles=(0.05, 0.95), unit_ids=None): - """ - Compute firing range, the range between the 5th and 95th quantiles of the firing rates distribution + """Calculate firing range, the range between the 5th and 95th quantiles of the firing rates distribution computed in non-overlapping time bins. Parameters @@ -579,11 +578,17 @@ def compute_firing_ranges(waveform_extractor, bin_size_s=5, quantiles=(0.05, 0.9 The size of the bin in seconds. quantiles : tuple, default: (0.05, 0.95) The quantiles to compute. + unit_ids : list or None + List of unit ids to compute the firing range. If None, all units are used. Returns ------- firing_ranges : dict The firing range for each unit. + + Notes + ----- + Designed by Simon Musall and ported to SpikeInterface by Alessio Buccino. """ sampling_frequency = waveform_extractor.sampling_frequency bin_size_samples = int(bin_size_s * sampling_frequency) @@ -601,7 +606,7 @@ def compute_firing_ranges(waveform_extractor, bin_size_s=5, quantiles=(0.05, 0.9 spike_times = sorting.get_unit_spike_train(unit_id=unit_id, segment_index=segment_index) spike_counts, _ = np.histogram(spike_times, bins=edges) firing_rates = spike_counts / bin_size_s - firing_rate_histograms[unit_id] = np.concatentate((firing_rate_histograms[unit_id], firing_rates)) + firing_rate_histograms[unit_id] = np.concatenate((firing_rate_histograms[unit_id], firing_rates)) # finally we compute the percentiles firing_ranges = {} @@ -616,20 +621,37 @@ def compute_firing_ranges(waveform_extractor, bin_size_s=5, quantiles=(0.05, 0.9 _default_params["firing_range"] = dict(bin_size_s=5, quantiles=(0.05, 0.95)) -# TODO: docs def compute_amplitude_spreads( - waveform_extractor, spikes_bin_size=50, amplitude_extension="spike_amplitudes", unit_ids=None + waveform_extractor, num_spikes_per_bin=100, amplitude_extension="spike_amplitudes", unit_ids=None ): - """Calculate mean spread of spike amplitudes within defined bins of AP events + """Calculate spread of spike amplitudes within defined bins of spike events. + The spread is the median relative variance (variance divided by the overall amplitude mean) + computed over bins of `num_spikes_per_bin` spikes. - S Musall 2023 + Parameters + ---------- + waveform_extractor : WaveformExtractor + The waveform extractor object. + num_spikes_per_bin : int, default: 50 + The number of spikes per bin. + amplitude_extension : str, default: 'spike_amplitudes' + The name of the extension to load the amplitudes from. 'spike_amplitudes' or 'amplitude_scalings'. + unit_ids : list or None + List of unit ids to compute the amplitude spread. If None, all units are used. - Input: - ------ - amplitudes : numpy.ndarray - Array of amplitudes (don't need to be in physical units) + Returns + ------- + amplitude_spreads : dict + The amplitude spread for each unit. + Notes + ----- + Designed by Simon Musall and ported to SpikeInterface by Alessio Buccino. """ + assert amplitude_extension in ( + "spike_amplitudes", + "amplitude_scalings", + ), "Invalid amplitude_extension. It can be either 'spike_amplitudes' or 'amplitude_scalings'" sorting = waveform_extractor.sorting spikes = sorting.to_spike_vector() num_spikes = sorting.count_num_spikes_per_unit() @@ -639,23 +661,31 @@ def compute_amplitude_spreads( if waveform_extractor.is_extension(amplitude_extension): sac = waveform_extractor.load_extension(amplitude_extension) amps = sac.get_data(outputs="concatenated") + if amplitude_extension == "spike_amplitudes": + amps = np.concatenate(amps) else: warnings.warn("") empty_dict = {unit_id: np.nan for unit_id in unit_ids} return empty_dict all_unit_ids = list(sorting.unit_ids) + amplitude_spreads = {} for unit_id in unit_ids: amps_unit = amps[spikes["unit_index"] == all_unit_ids.index(unit_id)] - if num_spikes[unit_id] < spikes_bin_size: - amp_spread = np.var(amps_unit) + amp_mean = np.abs(np.mean(amps_unit)) + if num_spikes[unit_id] < num_spikes_per_bin: + amp_spread = np.std(amps_unit) / amp_mean else: - amp_spread = [] - for i in range(0, num_spikes[unit_id], spikes_bin_size): - amp_spread.append(np.var(amps_unit[i : i + spikes_bin_size])) - amp_spread = np.median(amp_spread) + amp_spreads = [] + for i in range(0, num_spikes[unit_id], num_spikes_per_bin): + amp_spreads.append(np.std(amps_unit[i : i + num_spikes_per_bin]) / amp_mean) + amp_spread = np.median(amp_spreads) + amplitude_spreads[unit_id] = amp_spread + + return amplitude_spreads + - return amp_spread +_default_params["amplitude_spread"] = dict(num_spikes_per_bin=100, amplitude_extension="spike_amplitudes") def compute_amplitude_cutoffs( diff --git a/src/spikeinterface/qualitymetrics/quality_metric_list.py b/src/spikeinterface/qualitymetrics/quality_metric_list.py index 917927f44a..ee25ce64fd 100644 --- a/src/spikeinterface/qualitymetrics/quality_metric_list.py +++ b/src/spikeinterface/qualitymetrics/quality_metric_list.py @@ -13,6 +13,7 @@ compute_drift_metrics, compute_synchrony_metrics, compute_firing_ranges, + compute_amplitude_spreads, ) from .pca_metrics import ( @@ -41,6 +42,7 @@ "sliding_rp_violation": compute_sliding_rp_violations, "amplitude_cutoff": compute_amplitude_cutoffs, "amplitude_median": compute_amplitude_medians, + "amplitude_spread": compute_amplitude_spreads, "synchrony": compute_synchrony_metrics, "firing_range": compute_firing_ranges, "drift": compute_drift_metrics, diff --git a/src/spikeinterface/qualitymetrics/tests/test_metrics_functions.py b/src/spikeinterface/qualitymetrics/tests/test_metrics_functions.py index d927d64c4f..a570b75b52 100644 --- a/src/spikeinterface/qualitymetrics/tests/test_metrics_functions.py +++ b/src/spikeinterface/qualitymetrics/tests/test_metrics_functions.py @@ -12,6 +12,7 @@ compute_principal_components, compute_spike_locations, compute_spike_amplitudes, + compute_amplitude_scalings, ) from spikeinterface.qualitymetrics import ( @@ -31,6 +32,8 @@ compute_drift_metrics, compute_amplitude_medians, compute_synchrony_metrics, + compute_firing_ranges, + compute_amplitude_spreads, ) @@ -212,6 +215,12 @@ def test_calculate_firing_rate_num_spikes(waveform_extractor_simple): # np.testing.assert_array_equal(list(num_spikes_gt.values()), list(num_spikes.values())) +def test_calculate_firing_range(waveform_extractor_simple): + we = waveform_extractor_simple + firing_ranges = compute_firing_ranges(we) + print(firing_ranges) + + def test_calculate_amplitude_cutoff(waveform_extractor_simple): we = waveform_extractor_simple spike_amps = compute_spike_amplitudes(we) @@ -234,6 +243,19 @@ def test_calculate_amplitude_median(waveform_extractor_simple): # assert np.allclose(list(amp_medians_gt.values()), list(amp_medians.values()), rtol=0.05) +def test_calculate_amplitude_spread(waveform_extractor_simple): + we = waveform_extractor_simple + spike_amps = compute_spike_amplitudes(we) + amp_spreads = compute_amplitude_spreads(we, num_spikes_per_bin=20) + print(amp_spreads) + + amps_scalings = compute_amplitude_scalings(we) + amp_spreads_scalings = compute_amplitude_spreads( + we, num_spikes_per_bin=20, amplitude_extension="amplitude_scalings" + ) + print(amp_spreads_scalings) + + def test_calculate_snrs(waveform_extractor_simple): we = waveform_extractor_simple snrs = compute_snrs(we) @@ -358,4 +380,6 @@ def test_calculate_drift_metrics(waveform_extractor_simple): # test_calculate_isi_violations(we) # test_calculate_sliding_rp_violations(we) # test_calculate_drift_metrics(we) - test_synchrony_metrics(we) + # test_synchrony_metrics(we) + test_calculate_firing_range(we) + test_calculate_amplitude_spread(we) From 0d87ea07eab0baa02ee34915d96be8a6c623b222 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 14 Sep 2023 10:11:38 +0200 Subject: [PATCH 03/44] Update doc/modules/qualitymetrics/amplitude_spread.rst Co-authored-by: Zach McKenzie <92116279+zm711@users.noreply.github.com> --- doc/modules/qualitymetrics/amplitude_spread.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/modules/qualitymetrics/amplitude_spread.rst b/doc/modules/qualitymetrics/amplitude_spread.rst index 0ae0761265..cc79ebbe1d 100644 --- a/doc/modules/qualitymetrics/amplitude_spread.rst +++ b/doc/modules/qualitymetrics/amplitude_spread.rst @@ -6,9 +6,9 @@ Calculation ----------- The amplitude spread is a measure of the amplitude variability. -It is computed the ratio between the standard deviation and the amplitude mean (aka coefficient of variation). +It is computed as the ratio between the standard deviation and the amplitude mean (aka the coefficient of variation). To obtain a better estimate of this measure, it is first computed separately for several bins of a prefixed number of spikes -(e.g 100) and then the median of these values is taken. +(e.g. 100) and then the median of these values is taken. The computation requires either spike amplitudes (see :py:func:`~spikeinterface.postprocessing.compute_spike_amplitudes()`) or amplitude scalings (see :py:func:`~spikeinterface.postprocessing.compute_amplitude_scalings()`) to be pre-computed. From 2513a0e14cb5144c1747aa21cda9670c39449b80 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 14 Sep 2023 10:11:44 +0200 Subject: [PATCH 04/44] Update doc/modules/qualitymetrics/firing_range.rst Co-authored-by: Zach McKenzie <92116279+zm711@users.noreply.github.com> --- doc/modules/qualitymetrics/firing_range.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/qualitymetrics/firing_range.rst b/doc/modules/qualitymetrics/firing_range.rst index 0d17eedc13..1b82c7540f 100644 --- a/doc/modules/qualitymetrics/firing_range.rst +++ b/doc/modules/qualitymetrics/firing_range.rst @@ -13,7 +13,7 @@ taking the difference between the 95-th and 5th percentiles firing rates compute Expectation and use ------------------- -Very high levels of firing ranges, outside of a physiolocigal range, might indicate noise contamination. +Very high levels of firing ranges, outside of a physiological range, might indicate noise contamination. Example code From 78959e349b3783e77a3eca2a18967140909ba619 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 14 Sep 2023 10:11:52 +0200 Subject: [PATCH 05/44] Update doc/modules/qualitymetrics/amplitude_spread.rst Co-authored-by: Zach McKenzie <92116279+zm711@users.noreply.github.com> --- doc/modules/qualitymetrics/amplitude_spread.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/qualitymetrics/amplitude_spread.rst b/doc/modules/qualitymetrics/amplitude_spread.rst index cc79ebbe1d..bdd23892c5 100644 --- a/doc/modules/qualitymetrics/amplitude_spread.rst +++ b/doc/modules/qualitymetrics/amplitude_spread.rst @@ -17,7 +17,7 @@ or amplitude scalings (see :py:func:`~spikeinterface.postprocessing.compute_ampl Expectation and use ------------------- -Very high levels of amplitude_spread ranges, outside of a physiolocigal range, might indicate noise contamination. +Very high levels of amplitude_spread ranges, outside of a physiological range, might indicate noise contamination. Example code From a311455f34bb4fbe085b9191cd61b91e6efbb14a Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 14 Sep 2023 10:12:13 +0200 Subject: [PATCH 06/44] Update doc/modules/qualitymetrics/firing_range.rst Co-authored-by: Zach McKenzie <92116279+zm711@users.noreply.github.com> --- doc/modules/qualitymetrics/firing_range.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/qualitymetrics/firing_range.rst b/doc/modules/qualitymetrics/firing_range.rst index 1b82c7540f..3fd3d53573 100644 --- a/doc/modules/qualitymetrics/firing_range.rst +++ b/doc/modules/qualitymetrics/firing_range.rst @@ -6,7 +6,7 @@ Calculation ----------- The firing range indicates the dispersion of the firing rate of a unit across the recording. It is computed by -taking the difference between the 95-th and 5th percentiles firing rates computed over short time bins (e.g. 10 s). +taking the difference between the 95th percentile's firing rate and the 5th percentile's firing rate computed over short time bins (e.g. 10 s). From dcf2935acffb6d0634ba210fa6a590597173eabb Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 14 Sep 2023 10:30:45 +0200 Subject: [PATCH 07/44] quantile -> percentile --- src/spikeinterface/qualitymetrics/misc_metrics.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/spikeinterface/qualitymetrics/misc_metrics.py b/src/spikeinterface/qualitymetrics/misc_metrics.py index 6c237ee720..541d201c5e 100644 --- a/src/spikeinterface/qualitymetrics/misc_metrics.py +++ b/src/spikeinterface/qualitymetrics/misc_metrics.py @@ -566,8 +566,8 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), **k _default_params["synchrony"] = dict(synchrony_sizes=(0, 2, 4)) -def compute_firing_ranges(waveform_extractor, bin_size_s=5, quantiles=(0.05, 0.95), unit_ids=None): - """Calculate firing range, the range between the 5th and 95th quantiles of the firing rates distribution +def compute_firing_ranges(waveform_extractor, bin_size_s=5, percentiles=(0.05, 0.95), unit_ids=None): + """Calculate firing range, the range between the 5th and 95th percentiles of the firing rates distribution computed in non-overlapping time bins. Parameters @@ -576,8 +576,8 @@ def compute_firing_ranges(waveform_extractor, bin_size_s=5, quantiles=(0.05, 0.9 The waveform extractor object. bin_size_s : float, default: 5 The size of the bin in seconds. - quantiles : tuple, default: (0.05, 0.95) - The quantiles to compute. + percentiles : tuple, default: (0.05, 0.95) + The percentiles to compute. unit_ids : list or None List of unit ids to compute the firing range. If None, all units are used. @@ -611,14 +611,14 @@ def compute_firing_ranges(waveform_extractor, bin_size_s=5, quantiles=(0.05, 0.9 # finally we compute the percentiles firing_ranges = {} for unit_id in unit_ids: - firing_ranges[unit_id] = np.percentile(firing_rate_histograms[unit_id], quantiles[1]) - np.percentile( - firing_rate_histograms[unit_id], quantiles[0] + firing_ranges[unit_id] = np.percentile(firing_rate_histograms[unit_id], percentiles[1]) - np.percentile( + firing_rate_histograms[unit_id], percentiles[0] ) return firing_ranges -_default_params["firing_range"] = dict(bin_size_s=5, quantiles=(0.05, 0.95)) +_default_params["firing_range"] = dict(bin_size_s=5, percentiles=(0.05, 0.95)) def compute_amplitude_spreads( From 34a8df2e4db5c412b8a699057b05d44d699a8c40 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Thu, 14 Sep 2023 14:10:28 +0200 Subject: [PATCH 08/44] Modify amplitude_spread implementations and docs --- ...{amplitude_spread.rst => amplitude_cv.rst} | 27 ++++-- doc/modules/qualitymetrics/amplitudes.png | Bin 0 -> 214334 bytes .../qualitymetrics/misc_metrics.py | 90 +++++++++++++----- .../qualitymetrics/quality_metric_list.py | 4 +- .../tests/test_metrics_functions.py | 21 ++-- 5 files changed, 98 insertions(+), 44 deletions(-) rename doc/modules/qualitymetrics/{amplitude_spread.rst => amplitude_cv.rst} (50%) create mode 100644 doc/modules/qualitymetrics/amplitudes.png diff --git a/doc/modules/qualitymetrics/amplitude_spread.rst b/doc/modules/qualitymetrics/amplitude_cv.rst similarity index 50% rename from doc/modules/qualitymetrics/amplitude_spread.rst rename to doc/modules/qualitymetrics/amplitude_cv.rst index bdd23892c5..981813ef09 100644 --- a/doc/modules/qualitymetrics/amplitude_spread.rst +++ b/doc/modules/qualitymetrics/amplitude_cv.rst @@ -1,14 +1,15 @@ -Amplitude spread (:code:`amplitude_spread`) -=========================================== +Amplitude CV (:code:`amplitude_cv_median`, :code:`amplitude_cv_range`) +====================================================================== Calculation ----------- -The amplitude spread is a measure of the amplitude variability. -It is computed as the ratio between the standard deviation and the amplitude mean (aka the coefficient of variation). -To obtain a better estimate of this measure, it is first computed separately for several bins of a prefixed number of spikes -(e.g. 100) and then the median of these values is taken. +The amplitude CV (coefficient of variation) is a measure of the amplitude variability. +It is computed as the ratio between the standard deviation and the amplitude mean. +To obtain a better estimate of this measure, it is first computed separately for several temporal bins. +Out of these values, the median and the range (percentile distance, by default between the +5th and 95th percentiles) are computed. The computation requires either spike amplitudes (see :py:func:`~spikeinterface.postprocessing.compute_spike_amplitudes()`) or amplitude scalings (see :py:func:`~spikeinterface.postprocessing.compute_amplitude_scalings()`) to be pre-computed. @@ -17,7 +18,13 @@ or amplitude scalings (see :py:func:`~spikeinterface.postprocessing.compute_ampl Expectation and use ------------------- -Very high levels of amplitude_spread ranges, outside of a physiological range, might indicate noise contamination. +The amplitude CV median is expected to be relatively low for well-isolated units, indicating a "stereotypical" spike shape. + +The amplitude CV range can be high in the presence of noise contamination, due to amplitude outliers like in +the example below. + +.. image:: amplitudes.png + :width: 600 Example code @@ -30,9 +37,9 @@ Example code # Make recording, sorting and wvf_extractor object for your data. # It is required to run `compute_spike_amplitudes(wvf_extractor)` or # `compute_amplitude_scalings(wvf_extractor)` (if missing, values will be NaN) - amplitude_spread = qm.compute_firing_ranges(wvf_extractor, amplitude_extension='spike_amplitudes') - # amplitude_spread is a dict containing the units' IDs as keys, - # and their amplitude_spread (in units of standard deviation). + amplitude_cv_median, amplitude_cv_range = qm.compute_amplitude_cv_metrics(wvf_extractor) + # amplitude_cv_median and amplitude_cv_range are dicts containing the unit ids as keys, + # and their amplitude_cv metrics as values. diff --git a/doc/modules/qualitymetrics/amplitudes.png b/doc/modules/qualitymetrics/amplitudes.png new file mode 100644 index 0000000000000000000000000000000000000000..0ee4dd1edacb27e459b5601eb313aec675fbf890 GIT binary patch literal 214334 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

*j_z* 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≪7gP>~iN#rHNOs?ge9LVji( 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 Date: Thu, 14 Sep 2023 14:22:03 +0200 Subject: [PATCH 09/44] Update quality metrics index docs --- doc/modules/qualitymetrics.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/modules/qualitymetrics.rst b/doc/modules/qualitymetrics.rst index 8c7c0a2cc3..447d83db52 100644 --- a/doc/modules/qualitymetrics.rst +++ b/doc/modules/qualitymetrics.rst @@ -25,9 +25,11 @@ For more details about each metric and it's availability and use within SpikeInt :glob: qualitymetrics/amplitude_cutoff + qualitymetrics/amplitude_cv qualitymetrics/amplitude_median qualitymetrics/d_prime qualitymetrics/drift + qualitymetrics/firing_range qualitymetrics/firing_rate qualitymetrics/isi_violations qualitymetrics/isolation_distance From fac98233b84fa440b374d944d1c27b9d200cd0c1 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Tue, 19 Sep 2023 15:31:10 +0200 Subject: [PATCH 10/44] add tutorial to load matlab data --- doc/how_to/index.rst | 1 + doc/how_to/load_matalb_data.rst | 66 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 doc/how_to/load_matalb_data.rst diff --git a/doc/how_to/index.rst b/doc/how_to/index.rst index dabad818f9..fa7210d4f0 100644 --- a/doc/how_to/index.rst +++ b/doc/how_to/index.rst @@ -7,3 +7,4 @@ How to guides get_started analyse_neuropixels handle_drift + load_matalb_data diff --git a/doc/how_to/load_matalb_data.rst b/doc/how_to/load_matalb_data.rst new file mode 100644 index 0000000000..39b9a48d65 --- /dev/null +++ b/doc/how_to/load_matalb_data.rst @@ -0,0 +1,66 @@ +Exporting MATLAB Data to Binary & Loading in SpikeInterface +=========================================================== + +In this tutorial, we'll go through the process of exporting your data from MATLAB in a binary format and then loading it using SpikeInterface in Python. Let's break down the steps. + +Exporting Data from MATLAB +-------------------------- + +First, ensure your data is structured correctly. The data matrix should be organized such that the first dimension corresponds to samples/time and the second dimension to channels. + +.. code-block:: matlab + + % Define the size of your data + num_samples = 1000; + num_channels = 384; + + % Generate random data as an example + data = rand(num_samples, num_channels); + + % Write the data to a binary file + fileID = fopen('your_data_as_a_binary.bin', 'wb'); + fwrite(fileID, data, 'double'); + fclose(fileID); + +.. note:: + + In a real-world scenario, replace the random data generation with your actual data. + +Loading Data in SpikeInterface +----------------------------- + +This should produce a binary file called `your_data_as_a_binary.bin` in your current MATLAB directory. +You will need the complete path (i.e. its location on your computer) to load it in Python. + +Once you have your data in a binary format, you can seamlessly load it into SpikeInterface using the following script: + +.. code-block:: python + + from spikeinterface.core.binaryrecordingextractor import BinaryRecordingExtractor + from pathlib import Path + + # Define the path to your binary file + file_path = Path("/The/Path/To/Your/Data/your_data_as_a_binary.bin") + + # Ensure the file exists + assert file_path.is_file() + + # Specify the parameters of your recording + sampling_frequency = 30_000.0 # in Hz, adjust as per your matlab dataset + num_channels = 384 # adjust as per your matlab dataset + dtype = "float64" + + # Load the data using SpikeInterface + recording = BinaryRecordingExtractor(file_path, sampling_frequency=sampling_frequency, + num_channels=num_channels, dtype=dtype, gain_to_uV=1, offset_to_uV=0) + + # Verify the shape of your data + assert recording.get_traces().shape == (num_samples, num_channels) + +Common Pitfalls & Tips +---------------------- + +1. **Data Shape**: Always ensure that your MATLAB data matrix's first dimension corresponds to samples/time and the second to channels. +2. **File Path**: Double-check the file path in Python to ensure you're pointing to the right directory. +3. **Data Type**: When moving data between MATLAB and Python, it's crucial to keep the data type consistent. In our example, we used `double` in MATLAB, which corresponds to `float64` in Python. +4. **Sampling Frequency**: Ensure you set the correct sampling frequency when loading data into SpikeInterface. From 26cfd5db963796865b4a5ec877bfdd37e8616537 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 19 Sep 2023 17:01:24 +0200 Subject: [PATCH 11/44] Percentiles need 0-100 and ad duinit_ids to syncrhony metrics --- .../qualitymetrics/misc_metrics.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/spikeinterface/qualitymetrics/misc_metrics.py b/src/spikeinterface/qualitymetrics/misc_metrics.py index 6a42b12bb5..38add13c02 100644 --- a/src/spikeinterface/qualitymetrics/misc_metrics.py +++ b/src/spikeinterface/qualitymetrics/misc_metrics.py @@ -499,7 +499,7 @@ def compute_sliding_rp_violations( ) -def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), **kwargs): +def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), unit_ids=None, **kwargs): """Compute synchrony metrics. Synchrony metrics represent the rate of occurrences of "synchrony_size" spikes at the exact same sample index. @@ -509,6 +509,8 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), **k The waveform extractor object. synchrony_sizes : list or tuple, default: (2, 4, 8) The synchrony sizes to compute. + unit_ids : list or None, default: None + List of unit ids to compute the synchrony metrics. If None, all units are used. Returns ------- @@ -526,6 +528,9 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), **k sorting = waveform_extractor.sorting spikes = sorting.to_spike_vector(concatenated=False) + if unit_ids is None: + unit_ids = sorting.unit_ids + # Pre-allocate synchrony counts synchrony_counts = {} for synchrony_size in synchrony_sizes: @@ -538,20 +543,20 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), **k unique_spike_index, complexity = np.unique(spikes_in_segment["sample_index"], return_counts=True) # add counts for this segment - for unit_index in np.arange(len(sorting.unit_ids)): + for unit_id in unit_ids: + unit_index = sorting.unit_ids.index(unit_id) spikes_per_unit = spikes_in_segment[spikes_in_segment["unit_index"] == unit_index] # some segments/units might have no spikes if len(spikes_per_unit) == 0: continue spike_complexity = complexity[np.in1d(unique_spike_index, spikes_per_unit["sample_index"])] for synchrony_size in synchrony_sizes: - synchrony_counts[synchrony_size][unit_index] += np.count_nonzero(spike_complexity >= synchrony_size) + synchrony_counts[synchrony_size][unit_id] += np.count_nonzero(spike_complexity >= synchrony_size) # add counts for this segment synchrony_metrics_dict = { f"sync_spike_{synchrony_size}": { - unit_id: synchrony_counts[synchrony_size][unit_index] / spike_counts[unit_id] - for unit_index, unit_id in enumerate(sorting.unit_ids) + unit_id: synchrony_counts[synchrony_size][unit_id] / spike_counts[unit_id] for unit_id in unit_ids } for synchrony_size in synchrony_sizes } @@ -565,7 +570,7 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), **k _default_params["synchrony"] = dict(synchrony_sizes=(0, 2, 4)) -def compute_firing_ranges(waveform_extractor, bin_size_s=5, percentiles=(0.05, 0.95), unit_ids=None): +def compute_firing_ranges(waveform_extractor, bin_size_s=5, percentiles=(5, 95), unit_ids=None, **kwargs): """Calculate firing range, the range between the 5th and 95th percentiles of the firing rates distribution computed in non-overlapping time bins. @@ -575,7 +580,7 @@ def compute_firing_ranges(waveform_extractor, bin_size_s=5, percentiles=(0.05, 0 The waveform extractor object. bin_size_s : float, default: 5 The size of the bin in seconds. - percentiles : tuple, default: (0.05, 0.95) + percentiles : tuple, default: (5, 95) The percentiles to compute. unit_ids : list or None List of unit ids to compute the firing range. If None, all units are used. @@ -617,13 +622,13 @@ def compute_firing_ranges(waveform_extractor, bin_size_s=5, percentiles=(0.05, 0 return firing_ranges -_default_params["firing_range"] = dict(bin_size_s=5, percentiles=(0.05, 0.95)) +_default_params["firing_range"] = dict(bin_size_s=5, percentiles=(5, 95)) def compute_amplitude_cv_metrics( waveform_extractor, average_num_spikes_per_bin=50, - percentiles=(0.05, 0.95), + percentiles=(5, 95), min_num_bins=10, amplitude_extension="spike_amplitudes", unit_ids=None, @@ -726,7 +731,7 @@ def compute_amplitude_cv_metrics( _default_params["amplitude_cv"] = dict( - average_num_spikes_per_bin=50, percentiles=(0.05, 0.95), min_num_bins=10, amplitude_extension="spike_amplitudes" + average_num_spikes_per_bin=50, percentiles=(5, 95), min_num_bins=10, amplitude_extension="spike_amplitudes" ) From 2bd7dd6c1c0fea0e094293f1fb17f9293ce30bb6 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 19 Sep 2023 17:13:06 +0200 Subject: [PATCH 12/44] oups --- src/spikeinterface/qualitymetrics/misc_metrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spikeinterface/qualitymetrics/misc_metrics.py b/src/spikeinterface/qualitymetrics/misc_metrics.py index 38add13c02..0a37da99c3 100644 --- a/src/spikeinterface/qualitymetrics/misc_metrics.py +++ b/src/spikeinterface/qualitymetrics/misc_metrics.py @@ -536,6 +536,7 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), uni for synchrony_size in synchrony_sizes: synchrony_counts[synchrony_size] = np.zeros(len(waveform_extractor.unit_ids), dtype=np.int64) + all_unit_ids = list(sorting.unit_ids) for segment_index in range(sorting.get_num_segments()): spikes_in_segment = spikes[segment_index] @@ -544,7 +545,7 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), uni # add counts for this segment for unit_id in unit_ids: - unit_index = sorting.unit_ids.index(unit_id) + unit_index = all_unit_ids.index(unit_id) spikes_per_unit = spikes_in_segment[spikes_in_segment["unit_index"] == unit_index] # some segments/units might have no spikes if len(spikes_per_unit) == 0: From 7c958c3789f5591ad9fb8c9a4eaef1b905e5c929 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 19 Sep 2023 17:16:52 +0200 Subject: [PATCH 13/44] Unify imports and comments for quality metrics docs --- doc/modules/qualitymetrics/amplitude_cutoff.rst | 6 +++--- doc/modules/qualitymetrics/amplitude_cv.rst | 4 ++-- doc/modules/qualitymetrics/amplitude_median.rst | 6 +++--- doc/modules/qualitymetrics/d_prime.rst | 4 ++-- doc/modules/qualitymetrics/drift.rst | 4 ++-- doc/modules/qualitymetrics/firing_range.rst | 6 +++--- doc/modules/qualitymetrics/firing_rate.rst | 6 +++--- doc/modules/qualitymetrics/isi_violations.rst | 4 ++-- doc/modules/qualitymetrics/presence_ratio.rst | 6 +++--- doc/modules/qualitymetrics/sliding_rp_violations.rst | 4 ++-- doc/modules/qualitymetrics/snr.rst | 6 +++--- doc/modules/qualitymetrics/synchrony.rst | 4 ++-- 12 files changed, 30 insertions(+), 30 deletions(-) diff --git a/doc/modules/qualitymetrics/amplitude_cutoff.rst b/doc/modules/qualitymetrics/amplitude_cutoff.rst index 9f747f8d40..a1e4d85d01 100644 --- a/doc/modules/qualitymetrics/amplitude_cutoff.rst +++ b/doc/modules/qualitymetrics/amplitude_cutoff.rst @@ -21,12 +21,12 @@ Example code .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # It is also recommended to run `compute_spike_amplitudes(wvf_extractor)` # in order to use amplitudes from all spikes - fraction_missing = qm.compute_amplitude_cutoffs(wvf_extractor, peak_sign="neg") - # fraction_missing is a dict containing the units' IDs as keys, + fraction_missing = sqm.compute_amplitude_cutoffs(wvf_extractor, peak_sign="neg") + # fraction_missing is a dict containing the unit IDs as keys, # and their estimated fraction of missing spikes as values. Reference diff --git a/doc/modules/qualitymetrics/amplitude_cv.rst b/doc/modules/qualitymetrics/amplitude_cv.rst index 981813ef09..3edb1f9833 100644 --- a/doc/modules/qualitymetrics/amplitude_cv.rst +++ b/doc/modules/qualitymetrics/amplitude_cv.rst @@ -32,12 +32,12 @@ Example code .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # Make recording, sorting and wvf_extractor object for your data. # It is required to run `compute_spike_amplitudes(wvf_extractor)` or # `compute_amplitude_scalings(wvf_extractor)` (if missing, values will be NaN) - amplitude_cv_median, amplitude_cv_range = qm.compute_amplitude_cv_metrics(wvf_extractor) + amplitude_cv_median, amplitude_cv_range = sqm.compute_amplitude_cv_metrics(wvf_extractor) # amplitude_cv_median and amplitude_cv_range are dicts containing the unit ids as keys, # and their amplitude_cv metrics as values. diff --git a/doc/modules/qualitymetrics/amplitude_median.rst b/doc/modules/qualitymetrics/amplitude_median.rst index ffc45d1cf6..3ac52560e8 100644 --- a/doc/modules/qualitymetrics/amplitude_median.rst +++ b/doc/modules/qualitymetrics/amplitude_median.rst @@ -20,12 +20,12 @@ Example code .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # It is also recommended to run `compute_spike_amplitudes(wvf_extractor)` # in order to use amplitude values from all spikes. - amplitude_medians = qm.compute_amplitude_medians(wvf_extractor) - # amplitude_medians is a dict containing the units' IDs as keys, + amplitude_medians = sqm.compute_amplitude_medians(wvf_extractor) + # amplitude_medians is a dict containing the unit IDs as keys, # and their estimated amplitude medians as values. Reference diff --git a/doc/modules/qualitymetrics/d_prime.rst b/doc/modules/qualitymetrics/d_prime.rst index abb8c1dc74..e3bd61c580 100644 --- a/doc/modules/qualitymetrics/d_prime.rst +++ b/doc/modules/qualitymetrics/d_prime.rst @@ -32,9 +32,9 @@ Example code .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm - d_prime = qm.lda_metrics(all_pcs, all_labels, 0) + d_prime = sqm.lda_metrics(all_pcs, all_labels, 0) Reference diff --git a/doc/modules/qualitymetrics/drift.rst b/doc/modules/qualitymetrics/drift.rst index 4e78150ba7..ae52f7f883 100644 --- a/doc/modules/qualitymetrics/drift.rst +++ b/doc/modules/qualitymetrics/drift.rst @@ -40,12 +40,12 @@ Example code .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # Make recording, sorting and wvf_extractor object for your data. # It is required to run `compute_spike_locations(wvf_extractor)` # (if missing, values will be NaN) - drift_ptps, drift_stds, drift_mads = qm.compute_drift_metrics(wvf_extractor, peak_sign="neg") + drift_ptps, drift_stds, drift_mads = sqm.compute_drift_metrics(wvf_extractor, peak_sign="neg") # drift_ptps, drift_stds, and drift_mads are dict containing the units' ID as keys, # and their metrics as values. diff --git a/doc/modules/qualitymetrics/firing_range.rst b/doc/modules/qualitymetrics/firing_range.rst index 3fd3d53573..925539e9c6 100644 --- a/doc/modules/qualitymetrics/firing_range.rst +++ b/doc/modules/qualitymetrics/firing_range.rst @@ -21,11 +21,11 @@ Example code .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # Make recording, sorting and wvf_extractor object for your data. - firing_range = qm.compute_firing_ranges(wvf_extractor) - # firing_range is a dict containing the units' IDs as keys, + firing_range = sqm.compute_firing_ranges(wvf_extractor) + # firing_range is a dict containing the unit IDs as keys, # and their firing firing_range as values (in Hz). References diff --git a/doc/modules/qualitymetrics/firing_rate.rst b/doc/modules/qualitymetrics/firing_rate.rst index eddef3e48f..c0e15d7c2e 100644 --- a/doc/modules/qualitymetrics/firing_rate.rst +++ b/doc/modules/qualitymetrics/firing_rate.rst @@ -37,11 +37,11 @@ With SpikeInterface: .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # Make recording, sorting and wvf_extractor object for your data. - firing_rate = qm.compute_firing_rates(wvf_extractor) - # firing_rate is a dict containing the units' IDs as keys, + firing_rate = sqm.compute_firing_rates(wvf_extractor) + # firing_rate is a dict containing the unit IDs as keys, # and their firing rates across segments as values (in Hz). References diff --git a/doc/modules/qualitymetrics/isi_violations.rst b/doc/modules/qualitymetrics/isi_violations.rst index 947e7d4938..725d9b0fd6 100644 --- a/doc/modules/qualitymetrics/isi_violations.rst +++ b/doc/modules/qualitymetrics/isi_violations.rst @@ -77,11 +77,11 @@ With SpikeInterface: .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # Make recording, sorting and wvf_extractor object for your data. - isi_violations_ratio, isi_violations_count = qm.compute_isi_violations(wvf_extractor, isi_threshold_ms=1.0) + isi_violations_ratio, isi_violations_count = sqm.compute_isi_violations(wvf_extractor, isi_threshold_ms=1.0) References ---------- diff --git a/doc/modules/qualitymetrics/presence_ratio.rst b/doc/modules/qualitymetrics/presence_ratio.rst index e4de2248bd..5a420c8ccf 100644 --- a/doc/modules/qualitymetrics/presence_ratio.rst +++ b/doc/modules/qualitymetrics/presence_ratio.rst @@ -23,12 +23,12 @@ Example code .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # Make recording, sorting and wvf_extractor object for your data. - presence_ratio = qm.compute_presence_ratios(wvf_extractor) - # presence_ratio is a dict containing the units' IDs as keys + presence_ratio = sqm.compute_presence_ratios(wvf_extractor) + # presence_ratio is a dict containing the unit IDs as keys # and their presence ratio (between 0 and 1) as values. Links to original implementations diff --git a/doc/modules/qualitymetrics/sliding_rp_violations.rst b/doc/modules/qualitymetrics/sliding_rp_violations.rst index 843242c1e8..de68c3a92f 100644 --- a/doc/modules/qualitymetrics/sliding_rp_violations.rst +++ b/doc/modules/qualitymetrics/sliding_rp_violations.rst @@ -27,11 +27,11 @@ With SpikeInterface: .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # Make recording, sorting and wvf_extractor object for your data. - contamination = qm.compute_sliding_rp_violations(wvf_extractor, bin_size_ms=0.25) + contamination = sqm.compute_sliding_rp_violations(wvf_extractor, bin_size_ms=0.25) References ---------- diff --git a/doc/modules/qualitymetrics/snr.rst b/doc/modules/qualitymetrics/snr.rst index 288ab60515..b88d3291be 100644 --- a/doc/modules/qualitymetrics/snr.rst +++ b/doc/modules/qualitymetrics/snr.rst @@ -41,12 +41,12 @@ With SpikeInterface: .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # Make recording, sorting and wvf_extractor object for your data. - SNRs = qm.compute_snrs(wvf_extractor) - # SNRs is a dict containing the units' IDs as keys and their SNRs as values. + SNRs = sqm.compute_snrs(wvf_extractor) + # SNRs is a dict containing the unit IDs as keys and their SNRs as values. Links to original implementations --------------------------------- diff --git a/doc/modules/qualitymetrics/synchrony.rst b/doc/modules/qualitymetrics/synchrony.rst index 2f566bf8a7..0750940199 100644 --- a/doc/modules/qualitymetrics/synchrony.rst +++ b/doc/modules/qualitymetrics/synchrony.rst @@ -27,9 +27,9 @@ Example code .. code-block:: python - import spikeinterface.qualitymetrics as qm + import spikeinterface.qualitymetrics as sqm # Make recording, sorting and wvf_extractor object for your data. - synchrony = qm.compute_synchrony_metrics(wvf_extractor, synchrony_sizes=(2, 4, 8)) + synchrony = sqm.compute_synchrony_metrics(wvf_extractor, synchrony_sizes=(2, 4, 8)) # synchrony is a tuple of dicts with the synchrony metrics for each unit From 16cf79e222c51ab54f82f0783a8f23734c270bdb Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 19 Sep 2023 17:56:04 +0200 Subject: [PATCH 14/44] Default synchrony sizes and assertion --- src/spikeinterface/qualitymetrics/misc_metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spikeinterface/qualitymetrics/misc_metrics.py b/src/spikeinterface/qualitymetrics/misc_metrics.py index 0a37da99c3..b02bfae9ba 100644 --- a/src/spikeinterface/qualitymetrics/misc_metrics.py +++ b/src/spikeinterface/qualitymetrics/misc_metrics.py @@ -523,7 +523,7 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), uni Based on concepts described in [Gruen]_ This code was adapted from `Elephant - Electrophysiology Analysis Toolkit `_ """ - assert np.all(s > 1 for s in synchrony_sizes), "Synchrony sizes must be greater than 1" + assert np.all([s > 1 for s in synchrony_sizes]), "Synchrony sizes must be greater than 1" spike_counts = waveform_extractor.sorting.count_num_spikes_per_unit() sorting = waveform_extractor.sorting spikes = sorting.to_spike_vector(concatenated=False) @@ -568,7 +568,7 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), uni return synchrony_metrics -_default_params["synchrony"] = dict(synchrony_sizes=(0, 2, 4)) +_default_params["synchrony"] = dict(synchrony_sizes=(2, 4, 8)) def compute_firing_ranges(waveform_extractor, bin_size_s=5, percentiles=(5, 95), unit_ids=None, **kwargs): From d3fe469bb95d4a8b3e6cff1ecde37e1bc5c4e0c6 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 19 Sep 2023 20:11:19 +0200 Subject: [PATCH 15/44] Update src/spikeinterface/qualitymetrics/misc_metrics.py --- src/spikeinterface/qualitymetrics/misc_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spikeinterface/qualitymetrics/misc_metrics.py b/src/spikeinterface/qualitymetrics/misc_metrics.py index c742141d5d..f449b3c31b 100644 --- a/src/spikeinterface/qualitymetrics/misc_metrics.py +++ b/src/spikeinterface/qualitymetrics/misc_metrics.py @@ -523,7 +523,7 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), uni Based on concepts described in [Gruen]_ This code was adapted from `Elephant - Electrophysiology Analysis Toolkit `_ """ - assert np.all([s > 1 for s in synchrony_sizes]), "Synchrony sizes must be greater than 1" + assert min(synchrony_sizes) > 1, "Synchrony sizes must be greater than 1" spike_counts = waveform_extractor.sorting.count_num_spikes_per_unit() sorting = waveform_extractor.sorting spikes = sorting.to_spike_vector(concatenated=False) From a395c3c7253cd7dadd813b25a4862610221f9cf4 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 20 Sep 2023 10:24:30 +0200 Subject: [PATCH 16/44] suggestions --- doc/how_to/index.rst | 2 +- ...d_matalb_data.rst => load_matlab_data.rst} | 26 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) rename doc/how_to/{load_matalb_data.rst => load_matlab_data.rst} (70%) diff --git a/doc/how_to/index.rst b/doc/how_to/index.rst index fa7210d4f0..da94cf549c 100644 --- a/doc/how_to/index.rst +++ b/doc/how_to/index.rst @@ -7,4 +7,4 @@ How to guides get_started analyse_neuropixels handle_drift - load_matalb_data + load_matlab_data diff --git a/doc/how_to/load_matalb_data.rst b/doc/how_to/load_matlab_data.rst similarity index 70% rename from doc/how_to/load_matalb_data.rst rename to doc/how_to/load_matlab_data.rst index 39b9a48d65..cca579036a 100644 --- a/doc/how_to/load_matalb_data.rst +++ b/doc/how_to/load_matlab_data.rst @@ -7,15 +7,16 @@ Exporting Data from MATLAB -------------------------- First, ensure your data is structured correctly. The data matrix should be organized such that the first dimension corresponds to samples/time and the second dimension to channels. +In the following MATLAB code, we generate random data as an example and then write it to a binary file. .. code-block:: matlab % Define the size of your data - num_samples = 1000; - num_channels = 384; + numSamples = 1000; + numChannels = 384; % Generate random data as an example - data = rand(num_samples, num_channels); + data = rand(numSamples, numChannels); % Write the data to a binary file fileID = fopen('your_data_as_a_binary.bin', 'wb'); @@ -36,22 +37,24 @@ Once you have your data in a binary format, you can seamlessly load it into Spik .. code-block:: python - from spikeinterface.core.binaryrecordingextractor import BinaryRecordingExtractor + import spikeinterface as si from pathlib import Path - # Define the path to your binary file + # In linux or mac file_path = Path("/The/Path/To/Your/Data/your_data_as_a_binary.bin") + # or for Windows + # file_path = Path(r"c:\path\to\your\data\your_data_as_a_binary.bin") # Ensure the file exists assert file_path.is_file() # Specify the parameters of your recording - sampling_frequency = 30_000.0 # in Hz, adjust as per your matlab dataset - num_channels = 384 # adjust as per your matlab dataset - dtype = "float64" + sampling_frequency = 30_000.0 # in Hz, adjust as per your MATLAB dataset + num_channels = 384 # adjust as per your MATLAB dataset + dtype = "float64" # equivalent of MATLAB double # Load the data using SpikeInterface - recording = BinaryRecordingExtractor(file_path, sampling_frequency=sampling_frequency, + recording = si.read_binary(file_path, sampling_frequency=sampling_frequency, num_channels=num_channels, dtype=dtype, gain_to_uV=1, offset_to_uV=0) # Verify the shape of your data @@ -61,6 +64,7 @@ Common Pitfalls & Tips ---------------------- 1. **Data Shape**: Always ensure that your MATLAB data matrix's first dimension corresponds to samples/time and the second to channels. -2. **File Path**: Double-check the file path in Python to ensure you're pointing to the right directory. +2. **File Path**: Double-check the file path in Python to ensure you are pointing to the right directory. 3. **Data Type**: When moving data between MATLAB and Python, it's crucial to keep the data type consistent. In our example, we used `double` in MATLAB, which corresponds to `float64` in Python. -4. **Sampling Frequency**: Ensure you set the correct sampling frequency when loading data into SpikeInterface. +4. **Sampling Frequency**: Ensure you set the correct sampling frequency in Hz when loading data into SpikeInterface. +5. **Working on Python**: Matlab to python can feel like a big jump. If you are new to Python, we recommend checking out numpy's [Python for MATLAB Users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) guide. From 6130e5bad0c8d825a4c44da881b5473e691a8712 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 20 Sep 2023 10:27:17 +0200 Subject: [PATCH 17/44] add an assertion --- doc/how_to/load_matlab_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to/load_matlab_data.rst b/doc/how_to/load_matlab_data.rst index cca579036a..0a8345b792 100644 --- a/doc/how_to/load_matlab_data.rst +++ b/doc/how_to/load_matlab_data.rst @@ -46,7 +46,7 @@ Once you have your data in a binary format, you can seamlessly load it into Spik # file_path = Path(r"c:\path\to\your\data\your_data_as_a_binary.bin") # Ensure the file exists - assert file_path.is_file() + assert file_path.is_file(), f"Your path {file_path} is not a file, you probably have a typo or got the wrong path." # Specify the parameters of your recording sampling_frequency = 30_000.0 # in Hz, adjust as per your MATLAB dataset From 9a97e68f848d1126126bfecd819f456e12113813 Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Wed, 20 Sep 2023 10:52:05 +0200 Subject: [PATCH 18/44] Improve the concept of check_if_json_serializable to more serialation engine like pickle. --- src/spikeinterface/comparison/hybrid.py | 6 ++- .../comparison/multicomparisons.py | 7 ++- src/spikeinterface/core/base.py | 50 +++++++++++++------ src/spikeinterface/core/generate.py | 2 + src/spikeinterface/core/numpyextractors.py | 16 ++++-- src/spikeinterface/core/old_api_utils.py | 8 ++- src/spikeinterface/core/tests/test_base.py | 12 +++-- .../core/tests/test_jsonification.py | 10 +++- .../core/tests/test_waveform_extractor.py | 41 +++++++++++++-- src/spikeinterface/core/waveform_extractor.py | 34 ++++++++++--- src/spikeinterface/preprocessing/motion.py | 3 +- src/spikeinterface/sorters/basesorter.py | 3 +- 12 files changed, 150 insertions(+), 42 deletions(-) diff --git a/src/spikeinterface/comparison/hybrid.py b/src/spikeinterface/comparison/hybrid.py index af410255b9..c48ce70147 100644 --- a/src/spikeinterface/comparison/hybrid.py +++ b/src/spikeinterface/comparison/hybrid.py @@ -84,7 +84,8 @@ def __init__( ) # save injected sorting if necessary self.injected_sorting = injected_sorting - if not self.injected_sorting.check_if_json_serializable(): + # if not self.injected_sorting.check_if_json_serializable(): + if not self.injected_sorting.check_serializablility("json"): assert injected_sorting_folder is not None, "Provide injected_sorting_folder to injected sorting object" self.injected_sorting = self.injected_sorting.save(folder=injected_sorting_folder) @@ -180,7 +181,8 @@ def __init__( self.injected_sorting = injected_sorting # save injected sorting if necessary - if not self.injected_sorting.check_if_json_serializable(): + # if not self.injected_sorting.check_if_json_serializable(): + if not self.injected_sorting.check_serializablility("json"): assert injected_sorting_folder is not None, "Provide injected_sorting_folder to injected sorting object" self.injected_sorting = self.injected_sorting.save(folder=injected_sorting_folder) diff --git a/src/spikeinterface/comparison/multicomparisons.py b/src/spikeinterface/comparison/multicomparisons.py index 9e02fd5b2d..3a7075905e 100644 --- a/src/spikeinterface/comparison/multicomparisons.py +++ b/src/spikeinterface/comparison/multicomparisons.py @@ -182,7 +182,8 @@ def get_agreement_sorting(self, minimum_agreement_count=1, minimum_agreement_cou def save_to_folder(self, save_folder): for sorting in self.object_list: assert ( - sorting.check_if_json_serializable() + # sorting.check_if_json_serializable() + sorting.check_serializablility("json") ), "MultiSortingComparison.save_to_folder() need json serializable sortings" save_folder = Path(save_folder) @@ -244,7 +245,9 @@ def __init__( BaseSorting.__init__(self, sampling_frequency=sampling_frequency, unit_ids=unit_ids) - self._is_json_serializable = False + # self._is_json_serializable = False + self._serializablility["json"] = False + self._serializablility["pickle"] = True if len(unit_ids) > 0: for k in ("agreement_number", "avg_agreement", "unit_ids"): diff --git a/src/spikeinterface/core/base.py b/src/spikeinterface/core/base.py index 87c0805630..d87bd617c4 100644 --- a/src/spikeinterface/core/base.py +++ b/src/spikeinterface/core/base.py @@ -58,7 +58,8 @@ def __init__(self, main_ids: Sequence) -> None: self._properties = {} self._is_dumpable = True - self._is_json_serializable = True + # self._is_json_serializable = True + self._serializablility = {'json': True, 'pickle': True} # extractor specific list of pip extra requirements self.extra_requirements = [] @@ -490,6 +491,18 @@ def check_if_dumpable(self): return all([v.check_if_dumpable() for k, v in value.items()]) return self._is_dumpable + def check_serializablility(self, type="json"): + kwargs = self._kwargs + for value in kwargs.values(): + # here we check if the value is a BaseExtractor, a list of BaseExtractors, or a dict of BaseExtractors + if isinstance(value, BaseExtractor): + return value.check_serializablility(type=type) + elif isinstance(value, list) and (len(value) > 0) and isinstance(value[0], BaseExtractor): + return all([v.check_serializablility(type=type) for v in value]) + elif isinstance(value, dict) and isinstance(value[list(value.keys())[0]], BaseExtractor): + return all([v.check_serializablility(type=type) for k, v in value.items()]) + return self._serializablility[type] + def check_if_json_serializable(self): """ Check if the object is json serializable, including nested objects. @@ -499,16 +512,23 @@ def check_if_json_serializable(self): bool True if the object is json serializable, False otherwise. """ - kwargs = self._kwargs - for value in kwargs.values(): - # here we check if the value is a BaseExtractor, a list of BaseExtractors, or a dict of BaseExtractors - if isinstance(value, BaseExtractor): - return value.check_if_json_serializable() - elif isinstance(value, list) and (len(value) > 0) and isinstance(value[0], BaseExtractor): - return all([v.check_if_json_serializable() for v in value]) - elif isinstance(value, dict) and isinstance(value[list(value.keys())[0]], BaseExtractor): - return all([v.check_if_json_serializable() for k, v in value.items()]) - return self._is_json_serializable + # we keep this for backward compatilibity or not ???? + return self.check_serializablility("json") + + # kwargs = self._kwargs + # for value in kwargs.values(): + # # here we check if the value is a BaseExtractor, a list of BaseExtractors, or a dict of BaseExtractors + # if isinstance(value, BaseExtractor): + # return value.check_if_json_serializable() + # elif isinstance(value, list) and (len(value) > 0) and isinstance(value[0], BaseExtractor): + # return all([v.check_if_json_serializable() for v in value]) + # elif isinstance(value, dict) and isinstance(value[list(value.keys())[0]], BaseExtractor): + # return all([v.check_if_json_serializable() for k, v in value.items()]) + # return self._is_json_serializable + + def check_if_pickle_serializable(self): + # is this needed + return self.check_serializablility("pickle") @staticmethod def _get_file_path(file_path: Union[str, Path], extensions: Sequence) -> Path: @@ -557,7 +577,7 @@ def dump(self, file_path: Union[str, Path], relative_to=None, folder_metadata=No if str(file_path).endswith(".json"): self.dump_to_json(file_path, relative_to=relative_to, folder_metadata=folder_metadata) elif str(file_path).endswith(".pkl") or str(file_path).endswith(".pickle"): - self.dump_to_pickle(file_path, relative_to=relative_to, folder_metadata=folder_metadata) + self.dump_to_pickle(file_path, folder_metadata=folder_metadata) else: raise ValueError("Dump: file must .json or .pkl") @@ -576,7 +596,8 @@ def dump_to_json(self, file_path: Union[str, Path, None] = None, relative_to=Non folder_metadata: str, Path, or None Folder with files containing additional information (e.g. probe in BaseRecording) and properties. """ - assert self.check_if_json_serializable(), "The extractor is not json serializable" + # assert self.check_if_json_serializable(), "The extractor is not json serializable" + assert self.check_serializablility("json"), "The extractor is not json serializable" # Writing paths as relative_to requires recursively expanding the dict if relative_to: @@ -814,7 +835,8 @@ def save_to_folder(self, name=None, folder=None, verbose=True, **save_kwargs): # dump provenance provenance_file = folder / f"provenance.json" - if self.check_if_json_serializable(): + # if self.check_if_json_serializable(): + if self.check_serializablility("json"): self.dump(provenance_file) else: provenance_file.write_text(json.dumps({"warning": "the provenace is not dumpable!!!"}), encoding="utf8") diff --git a/src/spikeinterface/core/generate.py b/src/spikeinterface/core/generate.py index 07837bcef7..706054c957 100644 --- a/src/spikeinterface/core/generate.py +++ b/src/spikeinterface/core/generate.py @@ -1431,5 +1431,7 @@ def generate_ground_truth_recording( ) recording.annotate(is_filtered=True) recording.set_probe(probe, in_place=True) + recording.set_property("gain_to_uV", np.ones(num_channels)) + recording.set_property("offset_to_uV", np.zeros(num_channels)) return recording, sorting diff --git a/src/spikeinterface/core/numpyextractors.py b/src/spikeinterface/core/numpyextractors.py index d5663156c7..f55b975ddb 100644 --- a/src/spikeinterface/core/numpyextractors.py +++ b/src/spikeinterface/core/numpyextractors.py @@ -64,7 +64,9 @@ def __init__(self, traces_list, sampling_frequency, t_starts=None, channel_ids=N assert len(t_starts) == len(traces_list), "t_starts must be a list of same size than traces_list" t_starts = [float(t_start) for t_start in t_starts] - self._is_json_serializable = False + # self._is_json_serializable = False + self._serializablility["json"] = False + self._serializablility["pickle"] = False for i, traces in enumerate(traces_list): if t_starts is None: @@ -127,7 +129,9 @@ def __init__(self, spikes, sampling_frequency, unit_ids): BaseSorting.__init__(self, sampling_frequency, unit_ids) self._is_dumpable = True - self._is_json_serializable = False + # self._is_json_serializable = False + self._serializablility["json"] = False + self._serializablility["pickle"] = False if spikes.size == 0: nseg = 1 @@ -358,7 +362,9 @@ def __init__(self, shm_name, shape, sampling_frequency, unit_ids, dtype=minimum_ BaseSorting.__init__(self, sampling_frequency, unit_ids) self._is_dumpable = True - self._is_json_serializable = False + # self._is_json_serializable = False + self._serializablility["json"] = False + self._serializablility["pickle"] = False self.shm = SharedMemory(shm_name, create=False) self.shm_spikes = np.ndarray(shape=shape, dtype=dtype, buffer=self.shm.buf) @@ -517,7 +523,9 @@ def __init__(self, snippets_list, spikesframes_list, sampling_frequency, nbefore ) self._is_dumpable = False - self._is_json_serializable = False + # self._is_json_serializable = False + self._serializablility["json"] = False + self._serializablility["pickle"] = False for snippets, spikesframes in zip(snippets_list, spikesframes_list): snp_segment = NumpySnippetsSegment(snippets, spikesframes) diff --git a/src/spikeinterface/core/old_api_utils.py b/src/spikeinterface/core/old_api_utils.py index 1ff31127f4..38fbef1547 100644 --- a/src/spikeinterface/core/old_api_utils.py +++ b/src/spikeinterface/core/old_api_utils.py @@ -183,7 +183,9 @@ def __init__(self, oldapi_recording_extractor): # set _is_dumpable to False to use dumping mechanism of old extractor self._is_dumpable = False - self._is_json_serializable = False + # self._is_json_serializable = False + self._serializablility["json"] = False + self._serializablility["pickle"] = False self.annotate(is_filtered=oldapi_recording_extractor.is_filtered) @@ -269,7 +271,9 @@ def __init__(self, oldapi_sorting_extractor): self.add_sorting_segment(sorting_segment) self._is_dumpable = False - self._is_json_serializable = False + # self._is_json_serializable = False + self._serializablility["json"] = False + self._serializablility["pickle"] = False # add old properties copy_properties(oldapi_extractor=oldapi_sorting_extractor, new_extractor=self) diff --git a/src/spikeinterface/core/tests/test_base.py b/src/spikeinterface/core/tests/test_base.py index ea1a9cf0d2..77a5d7d9bf 100644 --- a/src/spikeinterface/core/tests/test_base.py +++ b/src/spikeinterface/core/tests/test_base.py @@ -50,18 +50,22 @@ def test_check_if_json_serializable(): test_extractor = generate_recording(seed=0, durations=[2]) # make a list of dumpable objects - test_extractor._is_json_serializable = True + # test_extractor._is_json_serializable = True + test_extractor._serializablility["json"] = True extractors_json_serializable = make_nested_extractors(test_extractor) for extractor in extractors_json_serializable: print(extractor) - assert extractor.check_if_json_serializable() + # assert extractor.check_if_json_serializable() + assert extractor.check_serializablility("json") # make not dumpable - test_extractor._is_json_serializable = False + # test_extractor._is_json_serializable = False + test_extractor._serializablility["json"] = False extractors_not_json_serializable = make_nested_extractors(test_extractor) for extractor in extractors_not_json_serializable: print(extractor) - assert not extractor.check_if_json_serializable() + # assert not extractor.check_if_json_serializable() + assert not extractor.check_serializablility("json") if __name__ == "__main__": diff --git a/src/spikeinterface/core/tests/test_jsonification.py b/src/spikeinterface/core/tests/test_jsonification.py index 473648c5ec..8572cda23e 100644 --- a/src/spikeinterface/core/tests/test_jsonification.py +++ b/src/spikeinterface/core/tests/test_jsonification.py @@ -142,9 +142,12 @@ def __init__(self, attribute, other_extractor=None, extractor_list=None, extract self.extractor_list = extractor_list self.extractor_dict = extractor_dict + BaseExtractor.__init__(self, main_ids=['1', '2']) # this already the case by default self._is_dumpable = True - self._is_json_serializable = True + # self._is_json_serializable = True + self._serializablility["json"] = True + self._serializablility["pickle"] = True self._kwargs = { "attribute": attribute, @@ -195,3 +198,8 @@ def test_encoding_numpy_scalars_within_nested_extractors_list(nested_extractor_l def test_encoding_numpy_scalars_within_nested_extractors_dict(nested_extractor_dict): json.dumps(nested_extractor_dict, cls=SIJsonEncoder) + + +if __name__ == '__main__': + nested_extractor = nested_extractor() + test_encoding_numpy_scalars_within_nested_extractors(nested_extractor_) \ No newline at end of file diff --git a/src/spikeinterface/core/tests/test_waveform_extractor.py b/src/spikeinterface/core/tests/test_waveform_extractor.py index 107ef5f180..f53b9cf18d 100644 --- a/src/spikeinterface/core/tests/test_waveform_extractor.py +++ b/src/spikeinterface/core/tests/test_waveform_extractor.py @@ -6,7 +6,7 @@ import zarr -from spikeinterface.core import generate_recording, generate_sorting, NumpySorting, ChannelSparsity +from spikeinterface.core import generate_recording, generate_sorting, NumpySorting, ChannelSparsity, generate_ground_truth_recording from spikeinterface import WaveformExtractor, BaseRecording, extract_waveforms, load_waveforms from spikeinterface.core.waveform_extractor import precompute_sparsity @@ -509,11 +509,46 @@ def test_compute_sparsity(): ) print(sparsity) +def test_non_json_object(): + recording, sorting = generate_ground_truth_recording( + durations=[30, 40], + sampling_frequency=30000.0, + num_channels=32, + num_units=5, + ) + + # recording is not save to keep it in memory + sorting = sorting.save() + + wf_folder = cache_folder / "test_waveform_extractor" + if wf_folder.is_dir(): + shutil.rmtree(wf_folder) + + + we = extract_waveforms( + recording, + sorting, + wf_folder, + mode="folder", + sparsity=None, + sparse=False, + ms_before=1.0, + ms_after=1.6, + max_spikes_per_unit=50, + n_jobs=4, + chunk_size=30000, + progress_bar=True, + ) + + # This used to fail because of json + we = load_waveforms(wf_folder) + if __name__ == "__main__": - test_WaveformExtractor() + # test_WaveformExtractor() # test_extract_waveforms() - # test_sparsity() # test_portability() # test_recordingless() # test_compute_sparsity() + test_non_json_object() + diff --git a/src/spikeinterface/core/waveform_extractor.py b/src/spikeinterface/core/waveform_extractor.py index 6881ab3ec5..53852bf319 100644 --- a/src/spikeinterface/core/waveform_extractor.py +++ b/src/spikeinterface/core/waveform_extractor.py @@ -159,11 +159,20 @@ def load_from_folder( else: rec_attributes["probegroup"] = None else: - try: - recording = load_extractor(folder / "recording.json", base_folder=folder) - rec_attributes = None - except: + recording = None + if (folder / "recording.json").exists(): + try: + recording = load_extractor(folder / "recording.json", base_folder=folder) + except: + pass + elif (folder / "recording.pickle").exists(): + try: + recording = load_extractor(folder / "recording.pickle") + except: + pass + if recording is None: raise Exception("The recording could not be loaded. You can use the `with_recording=False` argument") + rec_attributes = None if sorting is None: sorting = load_extractor(folder / "sorting.json", base_folder=folder) @@ -271,9 +280,16 @@ def create( else: relative_to = None - if recording.check_if_json_serializable(): + # if recording.check_if_json_serializable(): + if recording.check_serializablility("json"): recording.dump(folder / "recording.json", relative_to=relative_to) - if sorting.check_if_json_serializable(): + elif recording.check_serializablility("pickle"): + # In this case we loose the relative_to!! + # TODO make sure that we do not dump to pickle a NumpyRecording!!!!! + recording.dump(folder / "recording.pickle") + + # if sorting.check_if_json_serializable(): + if sorting.check_serializablility("json"): sorting.dump(folder / "sorting.json", relative_to=relative_to) else: warn( @@ -879,9 +895,11 @@ def save( (folder / "params.json").write_text(json.dumps(check_json(self._params), indent=4), encoding="utf8") if self.has_recording(): - if self.recording.check_if_json_serializable(): + # if self.recording.check_if_json_serializable(): + if self.recording.check_serializablility("json"): self.recording.dump(folder / "recording.json", relative_to=relative_to) - if self.sorting.check_if_json_serializable(): + # if self.sorting.check_if_json_serializable(): + if self.sorting.check_serializablility("json"): self.sorting.dump(folder / "sorting.json", relative_to=relative_to) else: warn( diff --git a/src/spikeinterface/preprocessing/motion.py b/src/spikeinterface/preprocessing/motion.py index e2ef6e6794..0054fb94d4 100644 --- a/src/spikeinterface/preprocessing/motion.py +++ b/src/spikeinterface/preprocessing/motion.py @@ -333,7 +333,8 @@ def correct_motion( ) (folder / "parameters.json").write_text(json.dumps(parameters, indent=4, cls=SIJsonEncoder), encoding="utf8") (folder / "run_times.json").write_text(json.dumps(run_times, indent=4), encoding="utf8") - if recording.check_if_json_serializable(): + # if recording.check_if_json_serializable(): + if recording.check_serializablility("json"): recording.dump_to_json(folder / "recording.json") np.save(folder / "peaks.npy", peaks) diff --git a/src/spikeinterface/sorters/basesorter.py b/src/spikeinterface/sorters/basesorter.py index c7581ba1e1..da20506965 100644 --- a/src/spikeinterface/sorters/basesorter.py +++ b/src/spikeinterface/sorters/basesorter.py @@ -137,7 +137,8 @@ def initialize_folder(cls, recording, output_folder, verbose, remove_existing_fo ) rec_file = output_folder / "spikeinterface_recording.json" - if recording.check_if_json_serializable(): + # if recording.check_if_json_serializable(): + if recording.check_serializablility("json"): recording.dump_to_json(rec_file, relative_to=output_folder) else: d = {"warning": "The recording is not serializable to json"} From 0842509422d8498fab0c506d6ed2839b4f4d0a74 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 20 Sep 2023 12:29:11 +0200 Subject: [PATCH 19/44] my final version --- doc/how_to/load_matlab_data.rst | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/doc/how_to/load_matlab_data.rst b/doc/how_to/load_matlab_data.rst index 0a8345b792..3e602012a1 100644 --- a/doc/how_to/load_matlab_data.rst +++ b/doc/how_to/load_matlab_data.rst @@ -55,16 +55,42 @@ Once you have your data in a binary format, you can seamlessly load it into Spik # Load the data using SpikeInterface recording = si.read_binary(file_path, sampling_frequency=sampling_frequency, - num_channels=num_channels, dtype=dtype, gain_to_uV=1, offset_to_uV=0) + num_channels=num_channels, dtype=dtype) # Verify the shape of your data assert recording.get_traces().shape == (num_samples, num_channels) +This should be enough to get you started with loading your MATLAB data into SpikeInterface. You can use all the Spikeinterface machinery to process your data, including filtering, spike sorting, and more. + Common Pitfalls & Tips ---------------------- -1. **Data Shape**: Always ensure that your MATLAB data matrix's first dimension corresponds to samples/time and the second to channels. +1. **Data Shape**: Always ensure that your MATLAB data matrix's first dimension corresponds to samples/time and the second to channels. If the time happens to be in the second dimension, you can use `time_axis=1` as an argument in `si.read_binary()` to account for this. 2. **File Path**: Double-check the file path in Python to ensure you are pointing to the right directory. 3. **Data Type**: When moving data between MATLAB and Python, it's crucial to keep the data type consistent. In our example, we used `double` in MATLAB, which corresponds to `float64` in Python. 4. **Sampling Frequency**: Ensure you set the correct sampling frequency in Hz when loading data into SpikeInterface. 5. **Working on Python**: Matlab to python can feel like a big jump. If you are new to Python, we recommend checking out numpy's [Python for MATLAB Users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) guide. + + +Using gains and offsets for integer data +---------------------------------------- + +A common technique used in raw formats is to store data as integer values, which provides a memory-efficient representation (i.e. lower ram) and use a gain and offset to convert it to float values that represent meaningful physical units. +In SpikeInterface this is done using the `gain_to_uV` and `offset_to_uV` parameters as the we handle traces in microvolts. Both values can be passed to `read_binary` when loading the data: + +.. code-block:: python + + sampling_frequency = 30_000.0 # in Hz, adjust as per your MATLAB dataset + num_channels = 384 # adjust as per your MATLAB dataset + dtype_int = 'int16' # adjust as per your MATLAB dataset + gain_to_uV = 0.195 # adjust as per your MATLAB dataset + offset_to_uV = 0 # adjust as per your MATLAB dataset + + recording = si.read_binary(file_path, sampling_frequency=sampling_frequency, + num_channels=num_channels, dtype=dtype_int, + gain_to_uV=gain_to_uV, offset_to_uV=offset_to_uV) + + recording.get_traces(start) + + +This will equip your recording object with capabilities to convert the data to float values in uV using the `get_traces()` method with the `return_scaled` parameter set to True. From 1ead6a33e658bf5a0365d21506a90dd9bd32e67c Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 20 Sep 2023 12:45:06 +0200 Subject: [PATCH 20/44] final review --- doc/how_to/load_matlab_data.rst | 72 +++++++++++++++++---------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/doc/how_to/load_matlab_data.rst b/doc/how_to/load_matlab_data.rst index 3e602012a1..0a80f1fdf9 100644 --- a/doc/how_to/load_matlab_data.rst +++ b/doc/how_to/load_matlab_data.rst @@ -1,13 +1,13 @@ Exporting MATLAB Data to Binary & Loading in SpikeInterface =========================================================== -In this tutorial, we'll go through the process of exporting your data from MATLAB in a binary format and then loading it using SpikeInterface in Python. Let's break down the steps. +In this tutorial, we will walk through the process of exporting data from MATLAB in a binary format and subsequently loading it using SpikeInterface in Python. Exporting Data from MATLAB -------------------------- -First, ensure your data is structured correctly. The data matrix should be organized such that the first dimension corresponds to samples/time and the second dimension to channels. -In the following MATLAB code, we generate random data as an example and then write it to a binary file. +Begin by ensuring your data structure is correct. Organize your data matrix so that the first dimension corresponds to samples/time and the second to channels. +Here, we present a MATLAB code that creates a random dataset and writes it to a binary file as an illustration. .. code-block:: matlab @@ -25,72 +25,76 @@ In the following MATLAB code, we generate random data as an example and then wri .. note:: - In a real-world scenario, replace the random data generation with your actual data. + In your own script, replace the random data generation with your actual dataset. Loading Data in SpikeInterface ----------------------------- -This should produce a binary file called `your_data_as_a_binary.bin` in your current MATLAB directory. -You will need the complete path (i.e. its location on your computer) to load it in Python. +After executing the above MATLAB code, a binary file named `your_data_as_a_binary.bin` will be created in your MATLAB directory. To load this file in Python, you'll need its full path. -Once you have your data in a binary format, you can seamlessly load it into SpikeInterface using the following script: +Use the following Python script to load the binary data into SpikeInterface: .. code-block:: python import spikeinterface as si from pathlib import Path - # In linux or mac + # Define file path + # For Linux or macOS: file_path = Path("/The/Path/To/Your/Data/your_data_as_a_binary.bin") - # or for Windows + # For Windows: # file_path = Path(r"c:\path\to\your\data\your_data_as_a_binary.bin") - # Ensure the file exists - assert file_path.is_file(), f"Your path {file_path} is not a file, you probably have a typo or got the wrong path." + # Confirm file existence + assert file_path.is_file(), f"Error: {file_path} is not a valid file. Please check the path." - # Specify the parameters of your recording - sampling_frequency = 30_000.0 # in Hz, adjust as per your MATLAB dataset - num_channels = 384 # adjust as per your MATLAB dataset - dtype = "float64" # equivalent of MATLAB double + # Define recording parameters + sampling_frequency = 30_000.0 # Adjust according to your MATLAB dataset + num_channels = 384 # Adjust according to your MATLAB dataset + dtype = "float64" # MATLAB's double corresponds to Python's float64 - # Load the data using SpikeInterface + # Load data using SpikeInterface recording = si.read_binary(file_path, sampling_frequency=sampling_frequency, num_channels=num_channels, dtype=dtype) - # Verify the shape of your data - assert recording.get_traces().shape == (num_samples, num_channels) + # Confirm the data shape + assert recording.get_traces().shape == (numSamples, num_channels) -This should be enough to get you started with loading your MATLAB data into SpikeInterface. You can use all the Spikeinterface machinery to process your data, including filtering, spike sorting, and more. +Follow the steps above to seamlessly import your MATLAB data into SpikeInterface. Once loaded, you can harness the full power of SpikeInterface for data processing, including filtering, spike sorting, and more. Common Pitfalls & Tips ---------------------- -1. **Data Shape**: Always ensure that your MATLAB data matrix's first dimension corresponds to samples/time and the second to channels. If the time happens to be in the second dimension, you can use `time_axis=1` as an argument in `si.read_binary()` to account for this. -2. **File Path**: Double-check the file path in Python to ensure you are pointing to the right directory. -3. **Data Type**: When moving data between MATLAB and Python, it's crucial to keep the data type consistent. In our example, we used `double` in MATLAB, which corresponds to `float64` in Python. -4. **Sampling Frequency**: Ensure you set the correct sampling frequency in Hz when loading data into SpikeInterface. -5. **Working on Python**: Matlab to python can feel like a big jump. If you are new to Python, we recommend checking out numpy's [Python for MATLAB Users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) guide. - +1. **Data Shape**: Make sure your MATLAB data matrix's first dimension is samples/time and the second is channels. If your time is in the second dimension, use `time_axis=1` in `si.read_binary()`. +2. **File Path**: Always double-check the Python file path. +3. **Data Type Consistency**: Ensure data types between MATLAB and Python are consistent. MATLAB's `double` is equivalent to nUMPY's `float64`. +4. **Sampling Frequency**: Set the appropriate sampling frequency in Hz for SpikeInterface. +5. **Transition to Python**: Moving from MATLAB to Python can be challenging. For newcomers to Python, consider reviewing numpy's [Numpy for MATLAB Users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) guide. Using gains and offsets for integer data ---------------------------------------- -A common technique used in raw formats is to store data as integer values, which provides a memory-efficient representation (i.e. lower ram) and use a gain and offset to convert it to float values that represent meaningful physical units. -In SpikeInterface this is done using the `gain_to_uV` and `offset_to_uV` parameters as the we handle traces in microvolts. Both values can be passed to `read_binary` when loading the data: +Raw data formats often store data as integer values for memory efficiency. To give these integers meaningful physical units, you can apply a gain and an offset. +In SpikeInterface, you can use the `gain_to_uV` and `offset_to_uV` parameters, since traces are handled in microvolts (uV). Both parameters can be integrated into the `read_binary` function. +If your data in MATLAB is stored as `int16`, and you know the gain and offset, you can use the following code to load the data: .. code-block:: python - sampling_frequency = 30_000.0 # in Hz, adjust as per your MATLAB dataset - num_channels = 384 # adjust as per your MATLAB dataset - dtype_int = 'int16' # adjust as per your MATLAB dataset - gain_to_uV = 0.195 # adjust as per your MATLAB dataset - offset_to_uV = 0 # adjust as per your MATLAB dataset + sampling_frequency = 30_000.0 # Adjust according to your MATLAB dataset + num_channels = 384 # Adjust according to your MATLAB dataset + dtype_int = 'int16' # Adjust according to your MATLAB dataset + gain_to_uV = 0.195 # Adjust according to your MATLAB dataset + offset_to_uV = 0 # Adjust according to your MATLAB dataset recording = si.read_binary(file_path, sampling_frequency=sampling_frequency, num_channels=num_channels, dtype=dtype_int, gain_to_uV=gain_to_uV, offset_to_uV=offset_to_uV) - recording.get_traces(start) + recording.get_traces(return_scaled=True) # Return traces in micro volts (uV) + +This will equip your recording object with capabilities to convert the data to float values in uV using the `get_traces()` method with the `return_scaled` parameter set to `True`. + +.. note:: -This will equip your recording object with capabilities to convert the data to float values in uV using the `get_traces()` method with the `return_scaled` parameter set to True. + The gain and offset parameters are usually format depend and you will need to find out the correct values for your data format. You can load your data without gain and offset but then the traces will be in integer values and not in uV. From e31978ce8355dda2d87a713c2495ec915b805f92 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 20 Sep 2023 12:53:47 +0200 Subject: [PATCH 21/44] typo --- doc/how_to/load_matlab_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to/load_matlab_data.rst b/doc/how_to/load_matlab_data.rst index 0a80f1fdf9..ca543ba43a 100644 --- a/doc/how_to/load_matlab_data.rst +++ b/doc/how_to/load_matlab_data.rst @@ -67,7 +67,7 @@ Common Pitfalls & Tips 1. **Data Shape**: Make sure your MATLAB data matrix's first dimension is samples/time and the second is channels. If your time is in the second dimension, use `time_axis=1` in `si.read_binary()`. 2. **File Path**: Always double-check the Python file path. -3. **Data Type Consistency**: Ensure data types between MATLAB and Python are consistent. MATLAB's `double` is equivalent to nUMPY's `float64`. +3. **Data Type Consistency**: Ensure data types between MATLAB and Python are consistent. MATLAB's `double` is equivalent to Numpy's `float64`. 4. **Sampling Frequency**: Set the appropriate sampling frequency in Hz for SpikeInterface. 5. **Transition to Python**: Moving from MATLAB to Python can be challenging. For newcomers to Python, consider reviewing numpy's [Numpy for MATLAB Users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) guide. From 5aba5e0f65532165488303203d7739e188fe6e0c Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 20 Sep 2023 12:57:44 +0200 Subject: [PATCH 22/44] Update doc/how_to/load_matlab_data.rst Co-authored-by: Zach McKenzie <92116279+zm711@users.noreply.github.com> --- doc/how_to/load_matlab_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to/load_matlab_data.rst b/doc/how_to/load_matlab_data.rst index ca543ba43a..7f90684701 100644 --- a/doc/how_to/load_matlab_data.rst +++ b/doc/how_to/load_matlab_data.rst @@ -97,4 +97,4 @@ This will equip your recording object with capabilities to convert the data to f .. note:: - The gain and offset parameters are usually format depend and you will need to find out the correct values for your data format. You can load your data without gain and offset but then the traces will be in integer values and not in uV. + The gain and offset parameters are usually format dependent and you will need to find out the correct values for your data format. You can load your data without gain and offset but then the traces will be in integer values and not in uV. From 3f4e182380995f56d458163356a70a813af6b146 Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Wed, 20 Sep 2023 14:01:50 +0200 Subject: [PATCH 23/44] More check and clean for check_if_serializable() --- src/spikeinterface/comparison/hybrid.py | 4 +- .../comparison/multicomparisons.py | 2 - src/spikeinterface/core/base.py | 46 +++++++++---------- src/spikeinterface/core/generate.py | 2 + src/spikeinterface/core/numpyextractors.py | 8 ++-- src/spikeinterface/core/old_api_utils.py | 2 - src/spikeinterface/core/tests/test_base.py | 7 +-- .../core/tests/test_waveform_extractor.py | 2 + src/spikeinterface/core/waveform_extractor.py | 18 +++++--- src/spikeinterface/preprocessing/motion.py | 1 - 10 files changed, 44 insertions(+), 48 deletions(-) diff --git a/src/spikeinterface/comparison/hybrid.py b/src/spikeinterface/comparison/hybrid.py index c48ce70147..3b8e9e0a72 100644 --- a/src/spikeinterface/comparison/hybrid.py +++ b/src/spikeinterface/comparison/hybrid.py @@ -84,8 +84,8 @@ def __init__( ) # save injected sorting if necessary self.injected_sorting = injected_sorting - # if not self.injected_sorting.check_if_json_serializable(): if not self.injected_sorting.check_serializablility("json"): + # TODO later : also use pickle assert injected_sorting_folder is not None, "Provide injected_sorting_folder to injected sorting object" self.injected_sorting = self.injected_sorting.save(folder=injected_sorting_folder) @@ -181,8 +181,8 @@ def __init__( self.injected_sorting = injected_sorting # save injected sorting if necessary - # if not self.injected_sorting.check_if_json_serializable(): if not self.injected_sorting.check_serializablility("json"): + # TODO later : also use pickle assert injected_sorting_folder is not None, "Provide injected_sorting_folder to injected sorting object" self.injected_sorting = self.injected_sorting.save(folder=injected_sorting_folder) diff --git a/src/spikeinterface/comparison/multicomparisons.py b/src/spikeinterface/comparison/multicomparisons.py index 3a7075905e..09a8c8aed1 100644 --- a/src/spikeinterface/comparison/multicomparisons.py +++ b/src/spikeinterface/comparison/multicomparisons.py @@ -182,7 +182,6 @@ def get_agreement_sorting(self, minimum_agreement_count=1, minimum_agreement_cou def save_to_folder(self, save_folder): for sorting in self.object_list: assert ( - # sorting.check_if_json_serializable() sorting.check_serializablility("json") ), "MultiSortingComparison.save_to_folder() need json serializable sortings" @@ -245,7 +244,6 @@ def __init__( BaseSorting.__init__(self, sampling_frequency=sampling_frequency, unit_ids=unit_ids) - # self._is_json_serializable = False self._serializablility["json"] = False self._serializablility["pickle"] = True diff --git a/src/spikeinterface/core/base.py b/src/spikeinterface/core/base.py index d87bd617c4..63cf8e894f 100644 --- a/src/spikeinterface/core/base.py +++ b/src/spikeinterface/core/base.py @@ -484,11 +484,16 @@ def check_if_dumpable(self): for value in kwargs.values(): # here we check if the value is a BaseExtractor, a list of BaseExtractors, or a dict of BaseExtractors if isinstance(value, BaseExtractor): - return value.check_if_dumpable() - elif isinstance(value, list) and (len(value) > 0) and isinstance(value[0], BaseExtractor): - return all([v.check_if_dumpable() for v in value]) - elif isinstance(value, dict) and isinstance(value[list(value.keys())[0]], BaseExtractor): - return all([v.check_if_dumpable() for k, v in value.items()]) + if not value.check_if_dumpable(): + return False + elif isinstance(value, list): + for v in value: + if isinstance(v, BaseExtractor) and not v.check_if_dumpable(): + return False + elif isinstance(value, dict): + for v in value.values(): + if isinstance(v, BaseExtractor) and not v.check_if_dumpable(): + return False return self._is_dumpable def check_serializablility(self, type="json"): @@ -496,11 +501,16 @@ def check_serializablility(self, type="json"): for value in kwargs.values(): # here we check if the value is a BaseExtractor, a list of BaseExtractors, or a dict of BaseExtractors if isinstance(value, BaseExtractor): - return value.check_serializablility(type=type) - elif isinstance(value, list) and (len(value) > 0) and isinstance(value[0], BaseExtractor): - return all([v.check_serializablility(type=type) for v in value]) - elif isinstance(value, dict) and isinstance(value[list(value.keys())[0]], BaseExtractor): - return all([v.check_serializablility(type=type) for k, v in value.items()]) + if not value.check_serializablility(type=type): + return False + elif isinstance(value, list): + for v in value: + if isinstance(v, BaseExtractor) and not v.check_serializablility(type=type): + return False + elif isinstance(value, dict): + for v in value.values(): + if isinstance(v, BaseExtractor) and not v.check_serializablility(type=type): + return False return self._serializablility[type] def check_if_json_serializable(self): @@ -513,21 +523,11 @@ def check_if_json_serializable(self): True if the object is json serializable, False otherwise. """ # we keep this for backward compatilibity or not ???? + # is this needed ??? I think no. return self.check_serializablility("json") - # kwargs = self._kwargs - # for value in kwargs.values(): - # # here we check if the value is a BaseExtractor, a list of BaseExtractors, or a dict of BaseExtractors - # if isinstance(value, BaseExtractor): - # return value.check_if_json_serializable() - # elif isinstance(value, list) and (len(value) > 0) and isinstance(value[0], BaseExtractor): - # return all([v.check_if_json_serializable() for v in value]) - # elif isinstance(value, dict) and isinstance(value[list(value.keys())[0]], BaseExtractor): - # return all([v.check_if_json_serializable() for k, v in value.items()]) - # return self._is_json_serializable - def check_if_pickle_serializable(self): - # is this needed + # is this needed ??? I think no. return self.check_serializablility("pickle") @staticmethod @@ -596,7 +596,6 @@ def dump_to_json(self, file_path: Union[str, Path, None] = None, relative_to=Non folder_metadata: str, Path, or None Folder with files containing additional information (e.g. probe in BaseRecording) and properties. """ - # assert self.check_if_json_serializable(), "The extractor is not json serializable" assert self.check_serializablility("json"), "The extractor is not json serializable" # Writing paths as relative_to requires recursively expanding the dict @@ -835,7 +834,6 @@ def save_to_folder(self, name=None, folder=None, verbose=True, **save_kwargs): # dump provenance provenance_file = folder / f"provenance.json" - # if self.check_if_json_serializable(): if self.check_serializablility("json"): self.dump(provenance_file) else: diff --git a/src/spikeinterface/core/generate.py b/src/spikeinterface/core/generate.py index 706054c957..362b598b0b 100644 --- a/src/spikeinterface/core/generate.py +++ b/src/spikeinterface/core/generate.py @@ -1056,6 +1056,8 @@ def __init__( dtype = parent_recording.dtype if parent_recording is not None else templates.dtype BaseRecording.__init__(self, sorting.get_sampling_frequency(), channel_ids, dtype) + # Important : self._serializablility is not change here because it will depend on the sorting parents itself. + n_units = len(sorting.unit_ids) assert len(templates) == n_units self.spike_vector = sorting.to_spike_vector() diff --git a/src/spikeinterface/core/numpyextractors.py b/src/spikeinterface/core/numpyextractors.py index f55b975ddb..5ef955a6eb 100644 --- a/src/spikeinterface/core/numpyextractors.py +++ b/src/spikeinterface/core/numpyextractors.py @@ -64,7 +64,6 @@ def __init__(self, traces_list, sampling_frequency, t_starts=None, channel_ids=N assert len(t_starts) == len(traces_list), "t_starts must be a list of same size than traces_list" t_starts = [float(t_start) for t_start in t_starts] - # self._is_json_serializable = False self._serializablility["json"] = False self._serializablility["pickle"] = False @@ -129,9 +128,9 @@ def __init__(self, spikes, sampling_frequency, unit_ids): BaseSorting.__init__(self, sampling_frequency, unit_ids) self._is_dumpable = True - # self._is_json_serializable = False self._serializablility["json"] = False - self._serializablility["pickle"] = False + # theorically this should be False but for simplicity make generators simples we still need this. + self._serializablility["pickle"] = True if spikes.size == 0: nseg = 1 @@ -362,7 +361,7 @@ def __init__(self, shm_name, shape, sampling_frequency, unit_ids, dtype=minimum_ BaseSorting.__init__(self, sampling_frequency, unit_ids) self._is_dumpable = True - # self._is_json_serializable = False + self._serializablility["json"] = False self._serializablility["pickle"] = False @@ -523,7 +522,6 @@ def __init__(self, snippets_list, spikesframes_list, sampling_frequency, nbefore ) self._is_dumpable = False - # self._is_json_serializable = False self._serializablility["json"] = False self._serializablility["pickle"] = False diff --git a/src/spikeinterface/core/old_api_utils.py b/src/spikeinterface/core/old_api_utils.py index 38fbef1547..a31edb0dd7 100644 --- a/src/spikeinterface/core/old_api_utils.py +++ b/src/spikeinterface/core/old_api_utils.py @@ -183,7 +183,6 @@ def __init__(self, oldapi_recording_extractor): # set _is_dumpable to False to use dumping mechanism of old extractor self._is_dumpable = False - # self._is_json_serializable = False self._serializablility["json"] = False self._serializablility["pickle"] = False @@ -271,7 +270,6 @@ def __init__(self, oldapi_sorting_extractor): self.add_sorting_segment(sorting_segment) self._is_dumpable = False - # self._is_json_serializable = False self._serializablility["json"] = False self._serializablility["pickle"] = False diff --git a/src/spikeinterface/core/tests/test_base.py b/src/spikeinterface/core/tests/test_base.py index 77a5d7d9bf..b716f6b1dd 100644 --- a/src/spikeinterface/core/tests/test_base.py +++ b/src/spikeinterface/core/tests/test_base.py @@ -46,16 +46,14 @@ def test_check_if_dumpable(): assert not extractor.check_if_dumpable() -def test_check_if_json_serializable(): +def test_check_if_serializable(): test_extractor = generate_recording(seed=0, durations=[2]) # make a list of dumpable objects - # test_extractor._is_json_serializable = True test_extractor._serializablility["json"] = True extractors_json_serializable = make_nested_extractors(test_extractor) for extractor in extractors_json_serializable: print(extractor) - # assert extractor.check_if_json_serializable() assert extractor.check_serializablility("json") # make not dumpable @@ -64,10 +62,9 @@ def test_check_if_json_serializable(): extractors_not_json_serializable = make_nested_extractors(test_extractor) for extractor in extractors_not_json_serializable: print(extractor) - # assert not extractor.check_if_json_serializable() assert not extractor.check_serializablility("json") if __name__ == "__main__": test_check_if_dumpable() - test_check_if_json_serializable() + test_check_if_serializable() diff --git a/src/spikeinterface/core/tests/test_waveform_extractor.py b/src/spikeinterface/core/tests/test_waveform_extractor.py index f53b9cf18d..3972c9186c 100644 --- a/src/spikeinterface/core/tests/test_waveform_extractor.py +++ b/src/spikeinterface/core/tests/test_waveform_extractor.py @@ -517,6 +517,8 @@ def test_non_json_object(): num_units=5, ) + + print(recording.check_serializablility("pickle")) # recording is not save to keep it in memory sorting = sorting.save() diff --git a/src/spikeinterface/core/waveform_extractor.py b/src/spikeinterface/core/waveform_extractor.py index 53852bf319..3de1429feb 100644 --- a/src/spikeinterface/core/waveform_extractor.py +++ b/src/spikeinterface/core/waveform_extractor.py @@ -280,17 +280,17 @@ def create( else: relative_to = None - # if recording.check_if_json_serializable(): if recording.check_serializablility("json"): recording.dump(folder / "recording.json", relative_to=relative_to) elif recording.check_serializablility("pickle"): # In this case we loose the relative_to!! - # TODO make sure that we do not dump to pickle a NumpyRecording!!!!! recording.dump(folder / "recording.pickle") - # if sorting.check_if_json_serializable(): if sorting.check_serializablility("json"): sorting.dump(folder / "sorting.json", relative_to=relative_to) + elif sorting.check_serializablility("pickle"): + # In this case we loose the relative_to!! + sorting.dump(folder / "sorting.pickle") else: warn( "Sorting object is not dumpable, which might result in downstream errors for " @@ -895,12 +895,16 @@ def save( (folder / "params.json").write_text(json.dumps(check_json(self._params), indent=4), encoding="utf8") if self.has_recording(): - # if self.recording.check_if_json_serializable(): if self.recording.check_serializablility("json"): self.recording.dump(folder / "recording.json", relative_to=relative_to) - # if self.sorting.check_if_json_serializable(): + elif self.recording.check_serializablility("pickle"): + self.recording.dump(folder / "recording.pickle") + + if self.sorting.check_serializablility("json"): self.sorting.dump(folder / "sorting.json", relative_to=relative_to) + elif self.sorting.check_serializablility("pickle"): + self.sorting.dump(folder / "sorting.pickle", relative_to=relative_to) else: warn( "Sorting object is not dumpable, which might result in downstream errors for " @@ -949,10 +953,10 @@ def save( # write metadata zarr_root.attrs["params"] = check_json(self._params) if self.has_recording(): - if self.recording.check_if_json_serializable(): + if self.recording.check_serializablility("json"): rec_dict = self.recording.to_dict(relative_to=relative_to, recursive=True) zarr_root.attrs["recording"] = check_json(rec_dict) - if self.sorting.check_if_json_serializable(): + if self.sorting.check_serializablility("json"): sort_dict = self.sorting.to_dict(relative_to=relative_to, recursive=True) zarr_root.attrs["sorting"] = check_json(sort_dict) else: diff --git a/src/spikeinterface/preprocessing/motion.py b/src/spikeinterface/preprocessing/motion.py index 0054fb94d4..6ab1a9afce 100644 --- a/src/spikeinterface/preprocessing/motion.py +++ b/src/spikeinterface/preprocessing/motion.py @@ -333,7 +333,6 @@ def correct_motion( ) (folder / "parameters.json").write_text(json.dumps(parameters, indent=4, cls=SIJsonEncoder), encoding="utf8") (folder / "run_times.json").write_text(json.dumps(run_times, indent=4), encoding="utf8") - # if recording.check_if_json_serializable(): if recording.check_serializablility("json"): recording.dump_to_json(folder / "recording.json") From 615c5d9cd219e4016e7149f1ce170f043d507333 Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Wed, 20 Sep 2023 14:19:46 +0200 Subject: [PATCH 24/44] Make pickle possible to dump in run sorter when json is not possible. --- src/spikeinterface/sorters/basesorter.py | 61 ++++++++++++------- .../sorters/external/herdingspikes.py | 4 +- .../sorters/external/mountainsort4.py | 4 +- .../sorters/external/mountainsort5.py | 4 +- .../sorters/external/pykilosort.py | 4 +- .../sorters/internal/spyking_circus2.py | 5 +- .../sorters/internal/tridesclous2.py | 4 +- src/spikeinterface/sorters/runsorter.py | 15 ++++- 8 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/spikeinterface/sorters/basesorter.py b/src/spikeinterface/sorters/basesorter.py index da20506965..bbcde31eed 100644 --- a/src/spikeinterface/sorters/basesorter.py +++ b/src/spikeinterface/sorters/basesorter.py @@ -137,9 +137,10 @@ def initialize_folder(cls, recording, output_folder, verbose, remove_existing_fo ) rec_file = output_folder / "spikeinterface_recording.json" - # if recording.check_if_json_serializable(): if recording.check_serializablility("json"): - recording.dump_to_json(rec_file, relative_to=output_folder) + recording.dump(rec_file, relative_to=output_folder) + elif recording.check_serializablility("pickle"): + recording.dump(output_folder / "spikeinterface_recording.pickle") else: d = {"warning": "The recording is not serializable to json"} rec_file.write_text(json.dumps(d, indent=4), encoding="utf8") @@ -186,6 +187,28 @@ def set_params_to_folder(cls, recording, output_folder, new_params, verbose): return params + @classmethod + def load_recording_from_folder(cls, output_folder, with_warnings=False): + + json_file = output_folder / "spikeinterface_recording.json" + pickle_file = output_folder / "spikeinterface_recording.pickle" + + + if json_file.exists(): + with (json_file).open("r", encoding="utf8") as f: + recording_dict = json.load(f) + if "warning" in recording_dict.keys() and with_warnings: + warnings.warn( + "The recording that has been sorted is not JSON serializable: it cannot be registered to the sorting object." + ) + recording = None + else: + recording = load_extractor(json_file, base_folder=output_folder) + elif pickle_file.exits(): + recording = load_extractor(pickle_file) + + return recording + @classmethod def _dump_params(cls, recording, output_folder, sorter_params, verbose): with (output_folder / "spikeinterface_params.json").open(mode="w", encoding="utf8") as f: @@ -272,7 +295,7 @@ def run_from_folder(cls, output_folder, raise_error, verbose): return run_time @classmethod - def get_result_from_folder(cls, output_folder): + def get_result_from_folder(cls, output_folder, register_recording=True, sorting_info=True): output_folder = Path(output_folder) sorter_output_folder = output_folder / "sorter_output" # check errors in log file @@ -295,27 +318,21 @@ def get_result_from_folder(cls, output_folder): # back-compatibility sorting = cls._get_result_from_folder(output_folder) - # register recording to Sorting object - # check if not json serializable - with (output_folder / "spikeinterface_recording.json").open("r", encoding="utf8") as f: - recording_dict = json.load(f) - if "warning" in recording_dict.keys(): - warnings.warn( - "The recording that has been sorted is not JSON serializable: it cannot be registered to the sorting object." - ) - else: - recording = load_extractor(output_folder / "spikeinterface_recording.json", base_folder=output_folder) + if register_recording: + # register recording to Sorting object + recording = cls.load_recording_from_folder( output_folder, with_warnings=False) if recording is not None: - # can be None when not dumpable sorting.register_recording(recording) - # set sorting info to Sorting object - with open(output_folder / "spikeinterface_recording.json", "r") as f: - rec_dict = json.load(f) - with open(output_folder / "spikeinterface_params.json", "r") as f: - params_dict = json.load(f) - with open(output_folder / "spikeinterface_log.json", "r") as f: - log_dict = json.load(f) - sorting.set_sorting_info(rec_dict, params_dict, log_dict) + + if sorting_info: + # set sorting info to Sorting object + with open(output_folder / "spikeinterface_recording.json", "r") as f: + rec_dict = json.load(f) + with open(output_folder / "spikeinterface_params.json", "r") as f: + params_dict = json.load(f) + with open(output_folder / "spikeinterface_log.json", "r") as f: + log_dict = json.load(f) + sorting.set_sorting_info(rec_dict, params_dict, log_dict) return sorting diff --git a/src/spikeinterface/sorters/external/herdingspikes.py b/src/spikeinterface/sorters/external/herdingspikes.py index a8d702ebe9..5180e6f1cc 100644 --- a/src/spikeinterface/sorters/external/herdingspikes.py +++ b/src/spikeinterface/sorters/external/herdingspikes.py @@ -147,9 +147,7 @@ def _run_from_folder(cls, sorter_output_folder, params, verbose): else: new_api = False - recording = load_extractor( - sorter_output_folder.parent / "spikeinterface_recording.json", base_folder=sorter_output_folder.parent - ) + recording = cls.load_recording_from_folder(sorter_output_folder.parent, with_warnings=False) p = params diff --git a/src/spikeinterface/sorters/external/mountainsort4.py b/src/spikeinterface/sorters/external/mountainsort4.py index 69f97fd11c..f6f0b3eaeb 100644 --- a/src/spikeinterface/sorters/external/mountainsort4.py +++ b/src/spikeinterface/sorters/external/mountainsort4.py @@ -89,9 +89,7 @@ def _setup_recording(cls, recording, sorter_output_folder, params, verbose): def _run_from_folder(cls, sorter_output_folder, params, verbose): import mountainsort4 - recording = load_extractor( - sorter_output_folder.parent / "spikeinterface_recording.json", base_folder=sorter_output_folder.parent - ) + recording = cls.load_recording_from_folder(sorter_output_folder.parent, with_warnings=False) # alias to params p = params diff --git a/src/spikeinterface/sorters/external/mountainsort5.py b/src/spikeinterface/sorters/external/mountainsort5.py index df6d276bf5..a88c59d688 100644 --- a/src/spikeinterface/sorters/external/mountainsort5.py +++ b/src/spikeinterface/sorters/external/mountainsort5.py @@ -115,9 +115,7 @@ def _setup_recording(cls, recording, sorter_output_folder, params, verbose): def _run_from_folder(cls, sorter_output_folder, params, verbose): import mountainsort5 as ms5 - recording: BaseRecording = load_extractor( - sorter_output_folder.parent / "spikeinterface_recording.json", base_folder=sorter_output_folder.parent - ) + recording = cls.load_recording_from_folder(sorter_output_folder.parent, with_warnings=False) # alias to params p = params diff --git a/src/spikeinterface/sorters/external/pykilosort.py b/src/spikeinterface/sorters/external/pykilosort.py index 2a41d793d5..1962d56206 100644 --- a/src/spikeinterface/sorters/external/pykilosort.py +++ b/src/spikeinterface/sorters/external/pykilosort.py @@ -148,9 +148,7 @@ def _setup_recording(cls, recording, sorter_output_folder, params, verbose): @classmethod def _run_from_folder(cls, sorter_output_folder, params, verbose): - recording = load_extractor( - sorter_output_folder.parent / "spikeinterface_recording.json", base_folder=sorter_output_folder.parent - ) + recording = cls.load_recording_from_folder(sorter_output_folder.parent, with_warnings=False) if not recording.binary_compatible_with(time_axis=0, file_paths_lenght=1): # saved by setup recording diff --git a/src/spikeinterface/sorters/internal/spyking_circus2.py b/src/spikeinterface/sorters/internal/spyking_circus2.py index 9de2762562..86cce1959b 100644 --- a/src/spikeinterface/sorters/internal/spyking_circus2.py +++ b/src/spikeinterface/sorters/internal/spyking_circus2.py @@ -54,9 +54,8 @@ def _run_from_folder(cls, sorter_output_folder, params, verbose): job_kwargs["verbose"] = verbose job_kwargs["progress_bar"] = verbose - recording = load_extractor( - sorter_output_folder.parent / "spikeinterface_recording.json", base_folder=sorter_output_folder.parent - ) + recording = cls.load_recording_from_folder(sorter_output_folder.parent, with_warnings=False) + sampling_rate = recording.get_sampling_frequency() num_channels = recording.get_num_channels() diff --git a/src/spikeinterface/sorters/internal/tridesclous2.py b/src/spikeinterface/sorters/internal/tridesclous2.py index 42f51d3a77..ed327e0f3c 100644 --- a/src/spikeinterface/sorters/internal/tridesclous2.py +++ b/src/spikeinterface/sorters/internal/tridesclous2.py @@ -49,9 +49,7 @@ def _run_from_folder(cls, sorter_output_folder, params, verbose): import hdbscan - recording_raw = load_extractor( - sorter_output_folder.parent / "spikeinterface_recording.json", base_folder=sorter_output_folder.parent - ) + recording_raw = cls.load_recording_from_folder(sorter_output_folder.parent, with_warnings=False) num_chans = recording_raw.get_num_channels() sampling_frequency = recording_raw.get_sampling_frequency() diff --git a/src/spikeinterface/sorters/runsorter.py b/src/spikeinterface/sorters/runsorter.py index 6e6ccc0358..e930ec7f79 100644 --- a/src/spikeinterface/sorters/runsorter.py +++ b/src/spikeinterface/sorters/runsorter.py @@ -624,10 +624,20 @@ def run_sorter_container( ) -def read_sorter_folder(output_folder, raise_error=True): +def read_sorter_folder(output_folder, register_recording=True, sorting_info=True, raise_error=True): """ Load a sorting object from a spike sorting output folder. The 'output_folder' must contain a valid 'spikeinterface_log.json' file + + + Parameters + ---------- + output_folder: Pth or str + The sorter folder + register_recording: bool, default: True + Attach recording (when json or pickle) to the sorting + sorting_info: bool, default: True + Attach sorting info to the sorting. """ output_folder = Path(output_folder) log_file = output_folder / "spikeinterface_log.json" @@ -647,7 +657,8 @@ def read_sorter_folder(output_folder, raise_error=True): sorter_name = log["sorter_name"] SorterClass = sorter_dict[sorter_name] - sorting = SorterClass.get_result_from_folder(output_folder) + sorting = SorterClass.get_result_from_folder(output_folder, register_recording=register_recording, + sorting_info=sorting_info) return sorting From b231e2dade552413bdd68e18aad95881a047f4cb Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 20 Sep 2023 14:47:14 +0200 Subject: [PATCH 25/44] correction --- doc/how_to/load_matlab_data.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/how_to/load_matlab_data.rst b/doc/how_to/load_matlab_data.rst index 7f90684701..0186ecf72b 100644 --- a/doc/how_to/load_matlab_data.rst +++ b/doc/how_to/load_matlab_data.rst @@ -57,8 +57,8 @@ Use the following Python script to load the binary data into SpikeInterface: recording = si.read_binary(file_path, sampling_frequency=sampling_frequency, num_channels=num_channels, dtype=dtype) - # Confirm the data shape - assert recording.get_traces().shape == (numSamples, num_channels) + # Confirm that the data was loaded correctly by comparing the data shapes and see they match the MATLAB data + print(recording.get_num_frames(), recording.get_num_channels()) Follow the steps above to seamlessly import your MATLAB data into SpikeInterface. Once loaded, you can harness the full power of SpikeInterface for data processing, including filtering, spike sorting, and more. From fb7681520e74a01be0fd4e56740936a4f6de4e25 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 20 Sep 2023 16:40:43 +0200 Subject: [PATCH 26/44] Update doc/how_to/load_matlab_data.rst Co-authored-by: Zach McKenzie <92116279+zm711@users.noreply.github.com> --- doc/how_to/load_matlab_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to/load_matlab_data.rst b/doc/how_to/load_matlab_data.rst index 0186ecf72b..3943fbd30f 100644 --- a/doc/how_to/load_matlab_data.rst +++ b/doc/how_to/load_matlab_data.rst @@ -28,7 +28,7 @@ Here, we present a MATLAB code that creates a random dataset and writes it to a In your own script, replace the random data generation with your actual dataset. Loading Data in SpikeInterface ------------------------------ +------------------------------ After executing the above MATLAB code, a binary file named `your_data_as_a_binary.bin` will be created in your MATLAB directory. To load this file in Python, you'll need its full path. From 9ba6fc6cbf0b0fd3d7bfa0b22108c48a05770b67 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Thu, 21 Sep 2023 14:01:25 +0200 Subject: [PATCH 27/44] Update doc/how_to/load_matlab_data.rst Co-authored-by: Alessio Buccino --- doc/how_to/load_matlab_data.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/how_to/load_matlab_data.rst b/doc/how_to/load_matlab_data.rst index 3943fbd30f..aaca718096 100644 --- a/doc/how_to/load_matlab_data.rst +++ b/doc/how_to/load_matlab_data.rst @@ -93,7 +93,7 @@ If your data in MATLAB is stored as `int16`, and you know the gain and offset, y recording.get_traces(return_scaled=True) # Return traces in micro volts (uV) -This will equip your recording object with capabilities to convert the data to float values in uV using the `get_traces()` method with the `return_scaled` parameter set to `True`. +This will equip your recording object with capabilities to convert the data to float values in uV using the :code:`get_traces()` method with the :code:`return_scaled` parameter set to :code:`True`. .. note:: From f2188266647d7faf721d89089b6f9c0bd1d9e637 Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Fri, 22 Sep 2023 16:22:01 +0200 Subject: [PATCH 28/44] feedback from Ramon --- src/spikeinterface/core/generate.py | 4 ++-- src/spikeinterface/core/tests/test_waveform_extractor.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/spikeinterface/core/generate.py b/src/spikeinterface/core/generate.py index 362b598b0b..05d63f3c8d 100644 --- a/src/spikeinterface/core/generate.py +++ b/src/spikeinterface/core/generate.py @@ -1433,7 +1433,7 @@ def generate_ground_truth_recording( ) recording.annotate(is_filtered=True) recording.set_probe(probe, in_place=True) - recording.set_property("gain_to_uV", np.ones(num_channels)) - recording.set_property("offset_to_uV", np.zeros(num_channels)) + recording.set_channel_gains(1.) + recording.set_channel_offsets(0.) return recording, sorting diff --git a/src/spikeinterface/core/tests/test_waveform_extractor.py b/src/spikeinterface/core/tests/test_waveform_extractor.py index 3972c9186c..f53b9cf18d 100644 --- a/src/spikeinterface/core/tests/test_waveform_extractor.py +++ b/src/spikeinterface/core/tests/test_waveform_extractor.py @@ -517,8 +517,6 @@ def test_non_json_object(): num_units=5, ) - - print(recording.check_serializablility("pickle")) # recording is not save to keep it in memory sorting = sorting.save() From b23e7e444065ee9b7a72c549a9c0aee22ce39c25 Mon Sep 17 00:00:00 2001 From: zm711 <92116279+zm711@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:11:30 -0400 Subject: [PATCH 29/44] allow relative path when exporting to phy --- src/spikeinterface/exporters/to_phy.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/spikeinterface/exporters/to_phy.py b/src/spikeinterface/exporters/to_phy.py index c92861a8bf..7de1a128e5 100644 --- a/src/spikeinterface/exporters/to_phy.py +++ b/src/spikeinterface/exporters/to_phy.py @@ -35,6 +35,7 @@ def export_to_phy( template_mode: str = "median", dtype: Optional[npt.DTypeLike] = None, verbose: bool = True, + use_relative_path: bool = False, **job_kwargs, ): """ @@ -64,6 +65,8 @@ def export_to_phy( Dtype to save binary data verbose: bool If True, output is verbose + use_relative_path : bool, default: False + If True saves the `dat_path` as a relative path, else an absolute {} """ @@ -94,7 +97,7 @@ def export_to_phy( used_sparsity = sparsity else: used_sparsity = ChannelSparsity.create_dense(waveform_extractor) - # convinient sparsity dict for the 3 cases to retrieve channl_inds + # convenient sparsity dict for the 3 cases to retrieve channl_inds sparse_dict = used_sparsity.unit_id_to_channel_indices empty_flag = False @@ -106,7 +109,7 @@ def export_to_phy( empty_flag = True unit_ids = non_empty_units if empty_flag: - warnings.warn("Empty units have been removed when being exported to Phy") + warnings.warn("Empty units have been removed while exporting to Phy") if len(unit_ids) == 0: raise Exception("No non-empty units in the sorting result, can't save to Phy.") @@ -149,7 +152,10 @@ def export_to_phy( # write params.py with (output_folder / "params.py").open("w") as f: - f.write(f"dat_path = r'{str(rec_path)}'\n") + if use_relative_path: + f.write(f"dat_path = r'recording.dat'\n") + else: + f.write(f"dat_path = r'{str(rec_path)}'\n") f.write(f"n_channels_dat = {num_chans}\n") f.write(f"dtype = '{dtype_str}'\n") f.write(f"offset = 0\n") From 2602ebc5d830ba5945d1d4245c9ffd6020e0c88f Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 26 Sep 2023 10:11:26 +0200 Subject: [PATCH 30/44] Add ignore_timestamps_errors to extractor --- .../extractors/neoextractors/openephys.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/spikeinterface/extractors/neoextractors/openephys.py b/src/spikeinterface/extractors/neoextractors/openephys.py index a771dc47b1..0d9a3887f8 100644 --- a/src/spikeinterface/extractors/neoextractors/openephys.py +++ b/src/spikeinterface/extractors/neoextractors/openephys.py @@ -45,14 +45,24 @@ class OpenEphysLegacyRecordingExtractor(NeoBaseRecordingExtractor): If there are several blocks (experiments), specify the block index you want to load. all_annotations: bool (default False) Load exhaustively all annotation from neo. + ignore_timestamps_errors: bool (default False) + Ignore the discontinuous timestamps errors in neo. """ mode = "folder" NeoRawIOClass = "OpenEphysRawIO" name = "openephyslegacy" - def __init__(self, folder_path, stream_id=None, stream_name=None, block_index=None, all_annotations=False): - neo_kwargs = self.map_to_neo_kwargs(folder_path) + def __init__( + self, + folder_path, + stream_id=None, + stream_name=None, + block_index=None, + all_annotations=False, + ignore_timestamps_errors=False, + ): + neo_kwargs = self.map_to_neo_kwargs(folder_path, ignore_timestamps_errors) NeoBaseRecordingExtractor.__init__( self, stream_id=stream_id, @@ -64,8 +74,8 @@ def __init__(self, folder_path, stream_id=None, stream_name=None, block_index=No self._kwargs.update(dict(folder_path=str(Path(folder_path).absolute()))) @classmethod - def map_to_neo_kwargs(cls, folder_path): - neo_kwargs = {"dirname": str(folder_path)} + def map_to_neo_kwargs(cls, folder_path, ignore_timestamps_errors=False): + neo_kwargs = {"dirname": str(folder_path), "ignore_timestamps_errors": ignore_timestamps_errors} return neo_kwargs From 8ea82ee0a43f04c8a51017651710e19eb9a156db Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 26 Sep 2023 13:17:39 +0200 Subject: [PATCH 31/44] check neo version and pop ignore_timestamps_errors for version 0.12.0 and older --- .../extractors/neoextractors/openephys.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/spikeinterface/extractors/neoextractors/openephys.py b/src/spikeinterface/extractors/neoextractors/openephys.py index 0d9a3887f8..cd2b6fb941 100644 --- a/src/spikeinterface/extractors/neoextractors/openephys.py +++ b/src/spikeinterface/extractors/neoextractors/openephys.py @@ -22,6 +22,19 @@ from spikeinterface.extractors.neuropixels_utils import get_neuropixels_sample_shifts +def drop_invalid_neo_arguments_for_version_0_12_0(neo_kwargs): + # Temporary function until neo version 0.13.0 is released + from packaging.version import Version + from importlib.metadata import version as lib_version + + neo_version = lib_version("neo") + # The possibility of ignoring timestamps errors is not present in neo <= 0.12.0 + if Version(neo_version) <= Version("0.12.0"): + neo_kwargs.pop("ignore_timestamps_errors") + + return neo_kwargs + + class OpenEphysLegacyRecordingExtractor(NeoBaseRecordingExtractor): """ Class for reading data saved by the Open Ephys GUI. @@ -76,6 +89,7 @@ def __init__( @classmethod def map_to_neo_kwargs(cls, folder_path, ignore_timestamps_errors=False): neo_kwargs = {"dirname": str(folder_path), "ignore_timestamps_errors": ignore_timestamps_errors} + neo_kwargs = drop_invalid_neo_arguments_for_version_0_12_0(neo_kwargs) return neo_kwargs From a970899c2e5162e842be6b0237a4338063508513 Mon Sep 17 00:00:00 2001 From: Zach McKenzie <92116279+zm711@users.noreply.github.com> Date: Tue, 26 Sep 2023 08:34:03 -0400 Subject: [PATCH 32/44] handle case of if-else copy_binary Co-authored-by: Alessio Buccino --- src/spikeinterface/exporters/to_phy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/spikeinterface/exporters/to_phy.py b/src/spikeinterface/exporters/to_phy.py index 7de1a128e5..4af6f73b25 100644 --- a/src/spikeinterface/exporters/to_phy.py +++ b/src/spikeinterface/exporters/to_phy.py @@ -153,7 +153,10 @@ def export_to_phy( # write params.py with (output_folder / "params.py").open("w") as f: if use_relative_path: - f.write(f"dat_path = r'recording.dat'\n") + if copy_binary: + f.write(f"dat_path = r'recording.dat'\n") + else: + f.write(f"dat_path = r'{str(Path(rec_path).relative_to(output_folder))}'\n") else: f.write(f"dat_path = r'{str(rec_path)}'\n") f.write(f"n_channels_dat = {num_chans}\n") From ad78ef269136a0d4bec37236a79c30f15862581f Mon Sep 17 00:00:00 2001 From: Zach McKenzie <92116279+zm711@users.noreply.github.com> Date: Tue, 26 Sep 2023 12:10:06 -0400 Subject: [PATCH 33/44] improve docstring-feedback --- src/spikeinterface/exporters/to_phy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spikeinterface/exporters/to_phy.py b/src/spikeinterface/exporters/to_phy.py index 4af6f73b25..edfca0fa52 100644 --- a/src/spikeinterface/exporters/to_phy.py +++ b/src/spikeinterface/exporters/to_phy.py @@ -66,7 +66,8 @@ def export_to_phy( verbose: bool If True, output is verbose use_relative_path : bool, default: False - If True saves the `dat_path` as a relative path, else an absolute + If True and `copy_binary=True` saves the binary file `dat_path` in the `params.py` relative to `output_folder` (ie `dat_path=r'recording.dat'`). If `copy_binary=False`, then uses a path relative to the `output_folder` + If False, uses an absolute path in the `params.py` (ie `dat_path=r'path/to/the/recording.dat'`) {} """ From 3c3451ecf6452419ebf83dd6dd2d9454ba7e6419 Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Wed, 27 Sep 2023 10:00:35 +0200 Subject: [PATCH 34/44] replace is_dumpable() by a more explicit naming : is_memory_serializable() --- src/spikeinterface/core/base.py | 53 +++++++++---------- src/spikeinterface/core/job_tools.py | 2 +- src/spikeinterface/core/numpyextractors.py | 6 +-- src/spikeinterface/core/old_api_utils.py | 6 +-- src/spikeinterface/core/tests/test_base.py | 10 ++-- .../core/tests/test_jsonification.py | 3 +- .../postprocessing/spike_amplitudes.py | 2 +- 7 files changed, 38 insertions(+), 44 deletions(-) diff --git a/src/spikeinterface/core/base.py b/src/spikeinterface/core/base.py index 63cf8e894f..3b8765a398 100644 --- a/src/spikeinterface/core/base.py +++ b/src/spikeinterface/core/base.py @@ -57,9 +57,7 @@ def __init__(self, main_ids: Sequence) -> None: # * number of units for sorting self._properties = {} - self._is_dumpable = True - # self._is_json_serializable = True - self._serializablility = {'json': True, 'pickle': True} + self._serializablility = {'memory': True, 'json': True, 'pickle': True} # extractor specific list of pip extra requirements self.extra_requirements = [] @@ -472,31 +470,8 @@ def clone(self) -> "BaseExtractor": clone = BaseExtractor.from_dict(d) return clone - def check_if_dumpable(self): - """Check if the object is dumpable, including nested objects. - - Returns - ------- - bool - True if the object is dumpable, False otherwise. - """ - kwargs = self._kwargs - for value in kwargs.values(): - # here we check if the value is a BaseExtractor, a list of BaseExtractors, or a dict of BaseExtractors - if isinstance(value, BaseExtractor): - if not value.check_if_dumpable(): - return False - elif isinstance(value, list): - for v in value: - if isinstance(v, BaseExtractor) and not v.check_if_dumpable(): - return False - elif isinstance(value, dict): - for v in value.values(): - if isinstance(v, BaseExtractor) and not v.check_if_dumpable(): - return False - return self._is_dumpable - def check_serializablility(self, type="json"): + def check_serializablility(self, type): kwargs = self._kwargs for value in kwargs.values(): # here we check if the value is a BaseExtractor, a list of BaseExtractors, or a dict of BaseExtractors @@ -512,6 +487,26 @@ def check_serializablility(self, type="json"): if isinstance(v, BaseExtractor) and not v.check_serializablility(type=type): return False return self._serializablility[type] + + + def check_if_dumpable(self): + warnings.warn( + "check_if_dumpable() is replace by is_memory_serializable()", DeprecationWarning, stacklevel=2 + ) + return self.check_serializablility("memory") + + def is_memory_serializable(self): + """ + Check if the object is serializable to memory with pickle, including nested objects. + + Returns + ------- + bool + True if the object is json serializable, False otherwise. + """ + return self.check_serializablility("memory") + + def check_if_json_serializable(self): """ @@ -636,7 +631,7 @@ def dump_to_pickle( folder_metadata: str, Path, or None Folder with files containing additional information (e.g. probe in BaseRecording) and properties. """ - assert self.check_if_dumpable(), "The extractor is not dumpable" + assert self.check_if_pickle_serializable(), "The extractor is not dumpable" dump_dict = self.to_dict( include_annotations=True, @@ -931,7 +926,7 @@ def save_to_zarr( zarr_root = zarr.open(zarr_path_init, mode="w", storage_options=storage_options) - if self.check_if_dumpable(): + if self.check_if_json_serializable(): zarr_root.attrs["provenance"] = check_json(self.to_dict()) else: zarr_root.attrs["provenance"] = None diff --git a/src/spikeinterface/core/job_tools.py b/src/spikeinterface/core/job_tools.py index c0ee77d2fd..0535872ca6 100644 --- a/src/spikeinterface/core/job_tools.py +++ b/src/spikeinterface/core/job_tools.py @@ -167,7 +167,7 @@ def ensure_n_jobs(recording, n_jobs=1): print(f"Python {sys.version} does not support parallel processing") n_jobs = 1 - if not recording.check_if_dumpable(): + if not recording.is_memory_serializable(): if n_jobs != 1: raise RuntimeError( "Recording is not dumpable and can't be processed in parallel. " diff --git a/src/spikeinterface/core/numpyextractors.py b/src/spikeinterface/core/numpyextractors.py index 5ef955a6eb..d09016c8f1 100644 --- a/src/spikeinterface/core/numpyextractors.py +++ b/src/spikeinterface/core/numpyextractors.py @@ -127,7 +127,7 @@ def __init__(self, spikes, sampling_frequency, unit_ids): """ """ BaseSorting.__init__(self, sampling_frequency, unit_ids) - self._is_dumpable = True + self._serializablility["memory"] = True self._serializablility["json"] = False # theorically this should be False but for simplicity make generators simples we still need this. self._serializablility["pickle"] = True @@ -360,8 +360,8 @@ def __init__(self, shm_name, shape, sampling_frequency, unit_ids, dtype=minimum_ assert shape[0] > 0, "SharedMemorySorting only supported with no empty sorting" BaseSorting.__init__(self, sampling_frequency, unit_ids) - self._is_dumpable = True + self._serializablility["memory"] = True self._serializablility["json"] = False self._serializablility["pickle"] = False @@ -521,7 +521,7 @@ def __init__(self, snippets_list, spikesframes_list, sampling_frequency, nbefore dtype=dtype, ) - self._is_dumpable = False + self._serializablility["memory"] = False self._serializablility["json"] = False self._serializablility["pickle"] = False diff --git a/src/spikeinterface/core/old_api_utils.py b/src/spikeinterface/core/old_api_utils.py index a31edb0dd7..879700cc15 100644 --- a/src/spikeinterface/core/old_api_utils.py +++ b/src/spikeinterface/core/old_api_utils.py @@ -181,8 +181,8 @@ def __init__(self, oldapi_recording_extractor): dtype=oldapi_recording_extractor.get_dtype(return_scaled=False), ) - # set _is_dumpable to False to use dumping mechanism of old extractor - self._is_dumpable = False + # set to False to use dumping mechanism of old extractor + self._serializablility["memory"] = False self._serializablility["json"] = False self._serializablility["pickle"] = False @@ -269,7 +269,7 @@ def __init__(self, oldapi_sorting_extractor): sorting_segment = OldToNewSortingSegment(oldapi_sorting_extractor) self.add_sorting_segment(sorting_segment) - self._is_dumpable = False + self._serializablility["memory"] = False self._serializablility["json"] = False self._serializablility["pickle"] = False diff --git a/src/spikeinterface/core/tests/test_base.py b/src/spikeinterface/core/tests/test_base.py index b716f6b1dd..28dbd166ec 100644 --- a/src/spikeinterface/core/tests/test_base.py +++ b/src/spikeinterface/core/tests/test_base.py @@ -31,19 +31,19 @@ def make_nested_extractors(extractor): ) -def test_check_if_dumpable(): +def test_is_memory_serializable(): test_extractor = generate_recording(seed=0, durations=[2]) # make a list of dumpable objects extractors_dumpable = make_nested_extractors(test_extractor) for extractor in extractors_dumpable: - assert extractor.check_if_dumpable() + assert extractor.is_memory_serializable() # make not dumpable - test_extractor._is_dumpable = False + test_extractor._serializablility["memory"] = False extractors_not_dumpable = make_nested_extractors(test_extractor) for extractor in extractors_not_dumpable: - assert not extractor.check_if_dumpable() + assert not extractor.is_memory_serializable() def test_check_if_serializable(): @@ -66,5 +66,5 @@ def test_check_if_serializable(): if __name__ == "__main__": - test_check_if_dumpable() + test_is_memory_serializable() test_check_if_serializable() diff --git a/src/spikeinterface/core/tests/test_jsonification.py b/src/spikeinterface/core/tests/test_jsonification.py index 8572cda23e..026e676966 100644 --- a/src/spikeinterface/core/tests/test_jsonification.py +++ b/src/spikeinterface/core/tests/test_jsonification.py @@ -144,8 +144,7 @@ def __init__(self, attribute, other_extractor=None, extractor_list=None, extract BaseExtractor.__init__(self, main_ids=['1', '2']) # this already the case by default - self._is_dumpable = True - # self._is_json_serializable = True + self._serializablility["memory"] = True self._serializablility["json"] = True self._serializablility["pickle"] = True diff --git a/src/spikeinterface/postprocessing/spike_amplitudes.py b/src/spikeinterface/postprocessing/spike_amplitudes.py index 38cb714d59..aa99f7fc5e 100644 --- a/src/spikeinterface/postprocessing/spike_amplitudes.py +++ b/src/spikeinterface/postprocessing/spike_amplitudes.py @@ -75,7 +75,7 @@ def _run(self, **job_kwargs): n_jobs = ensure_n_jobs(recording, job_kwargs.get("n_jobs", None)) if n_jobs != 1: # TODO: avoid dumping sorting and use spike vector and peak pipeline instead - assert sorting.check_if_dumpable(), ( + assert sorting.is_memory_serializable(), ( "The sorting object is not dumpable and cannot be processed in parallel. You can use the " "`sorting.save()` function to make it dumpable" ) From 9d3dceaacc77158487c47972a2d949a71bb3c65a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:52:23 +0000 Subject: [PATCH 35/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spikeinterface/comparison/multicomparisons.py | 4 ++-- src/spikeinterface/core/base.py | 10 ++-------- src/spikeinterface/core/generate.py | 4 ++-- src/spikeinterface/core/numpyextractors.py | 2 +- .../core/tests/test_jsonification.py | 8 ++++---- .../core/tests/test_waveform_extractor.py | 15 ++++++++++----- src/spikeinterface/core/waveform_extractor.py | 1 - src/spikeinterface/sorters/basesorter.py | 6 ++---- src/spikeinterface/sorters/runsorter.py | 7 ++++--- 9 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/spikeinterface/comparison/multicomparisons.py b/src/spikeinterface/comparison/multicomparisons.py index 6fe474822b..f44e14c4c4 100644 --- a/src/spikeinterface/comparison/multicomparisons.py +++ b/src/spikeinterface/comparison/multicomparisons.py @@ -189,8 +189,8 @@ def save_to_folder(self, save_folder): stacklevel=2, ) for sorting in self.object_list: - assert ( - sorting.check_serializablility("json") + assert sorting.check_serializablility( + "json" ), "MultiSortingComparison.save_to_folder() need json serializable sortings" save_folder = Path(save_folder) diff --git a/src/spikeinterface/core/base.py b/src/spikeinterface/core/base.py index 3b8765a398..6e91cedcb5 100644 --- a/src/spikeinterface/core/base.py +++ b/src/spikeinterface/core/base.py @@ -57,7 +57,7 @@ def __init__(self, main_ids: Sequence) -> None: # * number of units for sorting self._properties = {} - self._serializablility = {'memory': True, 'json': True, 'pickle': True} + self._serializablility = {"memory": True, "json": True, "pickle": True} # extractor specific list of pip extra requirements self.extra_requirements = [] @@ -470,7 +470,6 @@ def clone(self) -> "BaseExtractor": clone = BaseExtractor.from_dict(d) return clone - def check_serializablility(self, type): kwargs = self._kwargs for value in kwargs.values(): @@ -488,11 +487,8 @@ def check_serializablility(self, type): return False return self._serializablility[type] - def check_if_dumpable(self): - warnings.warn( - "check_if_dumpable() is replace by is_memory_serializable()", DeprecationWarning, stacklevel=2 - ) + warnings.warn("check_if_dumpable() is replace by is_memory_serializable()", DeprecationWarning, stacklevel=2) return self.check_serializablility("memory") def is_memory_serializable(self): @@ -506,8 +502,6 @@ def is_memory_serializable(self): """ return self.check_serializablility("memory") - - def check_if_json_serializable(self): """ Check if the object is json serializable, including nested objects. diff --git a/src/spikeinterface/core/generate.py b/src/spikeinterface/core/generate.py index 05d63f3c8d..eeb1e8af60 100644 --- a/src/spikeinterface/core/generate.py +++ b/src/spikeinterface/core/generate.py @@ -1433,7 +1433,7 @@ def generate_ground_truth_recording( ) recording.annotate(is_filtered=True) recording.set_probe(probe, in_place=True) - recording.set_channel_gains(1.) - recording.set_channel_offsets(0.) + recording.set_channel_gains(1.0) + recording.set_channel_offsets(0.0) return recording, sorting diff --git a/src/spikeinterface/core/numpyextractors.py b/src/spikeinterface/core/numpyextractors.py index d09016c8f1..3d7ec6cd1a 100644 --- a/src/spikeinterface/core/numpyextractors.py +++ b/src/spikeinterface/core/numpyextractors.py @@ -523,7 +523,7 @@ def __init__(self, snippets_list, spikesframes_list, sampling_frequency, nbefore self._serializablility["memory"] = False self._serializablility["json"] = False - self._serializablility["pickle"] = False + self._serializablility["pickle"] = False for snippets, spikesframes in zip(snippets_list, spikesframes_list): snp_segment = NumpySnippetsSegment(snippets, spikesframes) diff --git a/src/spikeinterface/core/tests/test_jsonification.py b/src/spikeinterface/core/tests/test_jsonification.py index 026e676966..1c491bd7a6 100644 --- a/src/spikeinterface/core/tests/test_jsonification.py +++ b/src/spikeinterface/core/tests/test_jsonification.py @@ -142,11 +142,11 @@ def __init__(self, attribute, other_extractor=None, extractor_list=None, extract self.extractor_list = extractor_list self.extractor_dict = extractor_dict - BaseExtractor.__init__(self, main_ids=['1', '2']) + BaseExtractor.__init__(self, main_ids=["1", "2"]) # this already the case by default self._serializablility["memory"] = True self._serializablility["json"] = True - self._serializablility["pickle"] = True + self._serializablility["pickle"] = True self._kwargs = { "attribute": attribute, @@ -199,6 +199,6 @@ def test_encoding_numpy_scalars_within_nested_extractors_dict(nested_extractor_d json.dumps(nested_extractor_dict, cls=SIJsonEncoder) -if __name__ == '__main__': +if __name__ == "__main__": nested_extractor = nested_extractor() - test_encoding_numpy_scalars_within_nested_extractors(nested_extractor_) \ No newline at end of file + test_encoding_numpy_scalars_within_nested_extractors(nested_extractor_) diff --git a/src/spikeinterface/core/tests/test_waveform_extractor.py b/src/spikeinterface/core/tests/test_waveform_extractor.py index f53b9cf18d..12dac52d43 100644 --- a/src/spikeinterface/core/tests/test_waveform_extractor.py +++ b/src/spikeinterface/core/tests/test_waveform_extractor.py @@ -6,7 +6,13 @@ import zarr -from spikeinterface.core import generate_recording, generate_sorting, NumpySorting, ChannelSparsity, generate_ground_truth_recording +from spikeinterface.core import ( + generate_recording, + generate_sorting, + NumpySorting, + ChannelSparsity, + generate_ground_truth_recording, +) from spikeinterface import WaveformExtractor, BaseRecording, extract_waveforms, load_waveforms from spikeinterface.core.waveform_extractor import precompute_sparsity @@ -509,14 +515,15 @@ def test_compute_sparsity(): ) print(sparsity) + def test_non_json_object(): recording, sorting = generate_ground_truth_recording( durations=[30, 40], sampling_frequency=30000.0, num_channels=32, num_units=5, - ) - + ) + # recording is not save to keep it in memory sorting = sorting.save() @@ -524,7 +531,6 @@ def test_non_json_object(): if wf_folder.is_dir(): shutil.rmtree(wf_folder) - we = extract_waveforms( recording, sorting, @@ -551,4 +557,3 @@ def test_non_json_object(): # test_recordingless() # test_compute_sparsity() test_non_json_object() - diff --git a/src/spikeinterface/core/waveform_extractor.py b/src/spikeinterface/core/waveform_extractor.py index 3de1429feb..cd8a62f5bc 100644 --- a/src/spikeinterface/core/waveform_extractor.py +++ b/src/spikeinterface/core/waveform_extractor.py @@ -900,7 +900,6 @@ def save( elif self.recording.check_serializablility("pickle"): self.recording.dump(folder / "recording.pickle") - if self.sorting.check_serializablility("json"): self.sorting.dump(folder / "sorting.json", relative_to=relative_to) elif self.sorting.check_serializablility("pickle"): diff --git a/src/spikeinterface/sorters/basesorter.py b/src/spikeinterface/sorters/basesorter.py index bbcde31eed..8d87558191 100644 --- a/src/spikeinterface/sorters/basesorter.py +++ b/src/spikeinterface/sorters/basesorter.py @@ -189,11 +189,9 @@ def set_params_to_folder(cls, recording, output_folder, new_params, verbose): @classmethod def load_recording_from_folder(cls, output_folder, with_warnings=False): - json_file = output_folder / "spikeinterface_recording.json" pickle_file = output_folder / "spikeinterface_recording.pickle" - if json_file.exists(): with (json_file).open("r", encoding="utf8") as f: recording_dict = json.load(f) @@ -206,7 +204,7 @@ def load_recording_from_folder(cls, output_folder, with_warnings=False): recording = load_extractor(json_file, base_folder=output_folder) elif pickle_file.exits(): recording = load_extractor(pickle_file) - + return recording @classmethod @@ -320,7 +318,7 @@ def get_result_from_folder(cls, output_folder, register_recording=True, sorting_ if register_recording: # register recording to Sorting object - recording = cls.load_recording_from_folder( output_folder, with_warnings=False) + recording = cls.load_recording_from_folder(output_folder, with_warnings=False) if recording is not None: sorting.register_recording(recording) diff --git a/src/spikeinterface/sorters/runsorter.py b/src/spikeinterface/sorters/runsorter.py index e930ec7f79..bd5667b15f 100644 --- a/src/spikeinterface/sorters/runsorter.py +++ b/src/spikeinterface/sorters/runsorter.py @@ -629,7 +629,7 @@ def read_sorter_folder(output_folder, register_recording=True, sorting_info=True Load a sorting object from a spike sorting output folder. The 'output_folder' must contain a valid 'spikeinterface_log.json' file - + Parameters ---------- output_folder: Pth or str @@ -657,8 +657,9 @@ def read_sorter_folder(output_folder, register_recording=True, sorting_info=True sorter_name = log["sorter_name"] SorterClass = sorter_dict[sorter_name] - sorting = SorterClass.get_result_from_folder(output_folder, register_recording=register_recording, - sorting_info=sorting_info) + sorting = SorterClass.get_result_from_folder( + output_folder, register_recording=register_recording, sorting_info=sorting_info + ) return sorting From 7329927cfb3035d764648a2175d617aa8999c67b Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Wed, 27 Sep 2023 10:54:57 +0200 Subject: [PATCH 36/44] rename to check_if_memory_serializable --- src/spikeinterface/core/base.py | 6 +----- src/spikeinterface/core/job_tools.py | 2 +- src/spikeinterface/core/tests/test_base.py | 8 ++++---- src/spikeinterface/postprocessing/spike_amplitudes.py | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/spikeinterface/core/base.py b/src/spikeinterface/core/base.py index 6e91cedcb5..b1b5065339 100644 --- a/src/spikeinterface/core/base.py +++ b/src/spikeinterface/core/base.py @@ -487,11 +487,7 @@ def check_serializablility(self, type): return False return self._serializablility[type] - def check_if_dumpable(self): - warnings.warn("check_if_dumpable() is replace by is_memory_serializable()", DeprecationWarning, stacklevel=2) - return self.check_serializablility("memory") - - def is_memory_serializable(self): + def check_if_memory_serializable(self): """ Check if the object is serializable to memory with pickle, including nested objects. diff --git a/src/spikeinterface/core/job_tools.py b/src/spikeinterface/core/job_tools.py index 0535872ca6..9369ad0b61 100644 --- a/src/spikeinterface/core/job_tools.py +++ b/src/spikeinterface/core/job_tools.py @@ -167,7 +167,7 @@ def ensure_n_jobs(recording, n_jobs=1): print(f"Python {sys.version} does not support parallel processing") n_jobs = 1 - if not recording.is_memory_serializable(): + if not recording.check_if_memory_serializable(): if n_jobs != 1: raise RuntimeError( "Recording is not dumpable and can't be processed in parallel. " diff --git a/src/spikeinterface/core/tests/test_base.py b/src/spikeinterface/core/tests/test_base.py index 28dbd166ec..8d0907c700 100644 --- a/src/spikeinterface/core/tests/test_base.py +++ b/src/spikeinterface/core/tests/test_base.py @@ -31,19 +31,19 @@ def make_nested_extractors(extractor): ) -def test_is_memory_serializable(): +def test_check_if_memory_serializable(): test_extractor = generate_recording(seed=0, durations=[2]) # make a list of dumpable objects extractors_dumpable = make_nested_extractors(test_extractor) for extractor in extractors_dumpable: - assert extractor.is_memory_serializable() + assert extractor.check_if_memory_serializable() # make not dumpable test_extractor._serializablility["memory"] = False extractors_not_dumpable = make_nested_extractors(test_extractor) for extractor in extractors_not_dumpable: - assert not extractor.is_memory_serializable() + assert not extractor.check_if_memory_serializable() def test_check_if_serializable(): @@ -66,5 +66,5 @@ def test_check_if_serializable(): if __name__ == "__main__": - test_is_memory_serializable() + test_check_if_memory_serializable() test_check_if_serializable() diff --git a/src/spikeinterface/postprocessing/spike_amplitudes.py b/src/spikeinterface/postprocessing/spike_amplitudes.py index aa99f7fc5e..9eb5a815d4 100644 --- a/src/spikeinterface/postprocessing/spike_amplitudes.py +++ b/src/spikeinterface/postprocessing/spike_amplitudes.py @@ -75,7 +75,7 @@ def _run(self, **job_kwargs): n_jobs = ensure_n_jobs(recording, job_kwargs.get("n_jobs", None)) if n_jobs != 1: # TODO: avoid dumping sorting and use spike vector and peak pipeline instead - assert sorting.is_memory_serializable(), ( + assert sorting.check_if_memory_serializable(), ( "The sorting object is not dumpable and cannot be processed in parallel. You can use the " "`sorting.save()` function to make it dumpable" ) From b9c6a38e99430fc7b734e0751871e6d08eb5aea1 Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Wed, 27 Sep 2023 10:56:28 +0200 Subject: [PATCH 37/44] oups --- src/spikeinterface/core/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spikeinterface/core/base.py b/src/spikeinterface/core/base.py index b1b5065339..e3b88588e2 100644 --- a/src/spikeinterface/core/base.py +++ b/src/spikeinterface/core/base.py @@ -494,7 +494,7 @@ def check_if_memory_serializable(self): Returns ------- bool - True if the object is json serializable, False otherwise. + True if the object is memory serializable, False otherwise. """ return self.check_serializablility("memory") From 331379a3f441e2691eb15985b60254fcc9e3f887 Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Wed, 27 Sep 2023 11:13:29 +0200 Subject: [PATCH 38/44] Remove "dumpable" naming also in doc and warnings. --- doc/modules/core.rst | 3 +-- src/spikeinterface/comparison/hybrid.py | 4 ++-- src/spikeinterface/core/base.py | 8 ++++---- src/spikeinterface/core/job_tools.py | 4 ++-- src/spikeinterface/core/tests/test_base.py | 17 ++++++++--------- .../core/tests/test_core_tools.py | 1 - src/spikeinterface/core/tests/test_job_tools.py | 6 +++--- .../core/tests/test_waveform_extractor.py | 2 +- src/spikeinterface/core/waveform_extractor.py | 15 ++++++++------- .../postprocessing/spike_amplitudes.py | 6 ------ .../sorters/tests/test_launcher.py | 2 +- 11 files changed, 30 insertions(+), 38 deletions(-) diff --git a/doc/modules/core.rst b/doc/modules/core.rst index fdc4d71fe7..976a82a4a3 100644 --- a/doc/modules/core.rst +++ b/doc/modules/core.rst @@ -547,8 +547,7 @@ workflow. In order to do this, one can use the :code:`Numpy*` classes, :py:class:`~spikeinterface.core.NumpyRecording`, :py:class:`~spikeinterface.core.NumpySorting`, :py:class:`~spikeinterface.core.NumpyEvent`, and :py:class:`~spikeinterface.core.NumpySnippets`. These object behave exactly like normal SpikeInterface objects, -but they are not bound to a file. This makes these objects *not dumpable*, so parallel processing is not supported. -In order to make them *dumpable*, one can simply :code:`save()` them (see :ref:`save_load`). +but they are not bound to a file. Also note the class :py:class:`~spikeinterface.core.SharedMemorySorting` which is very similar to Similar to :py:class:`~spikeinterface.core.NumpySorting` but with an unerlying SharedMemory which is usefull for diff --git a/src/spikeinterface/comparison/hybrid.py b/src/spikeinterface/comparison/hybrid.py index 3b8e9e0a72..e0c98cd772 100644 --- a/src/spikeinterface/comparison/hybrid.py +++ b/src/spikeinterface/comparison/hybrid.py @@ -39,7 +39,7 @@ class HybridUnitsRecording(InjectTemplatesRecording): The refractory period of the injected spike train (in ms). injected_sorting_folder: str | Path | None If given, the injected sorting is saved to this folder. - It must be specified if injected_sorting is None or not dumpable. + It must be specified if injected_sorting is None or not serialisable to file. Returns ------- @@ -138,7 +138,7 @@ class HybridSpikesRecording(InjectTemplatesRecording): this refractory period. injected_sorting_folder: str | Path | None If given, the injected sorting is saved to this folder. - It must be specified if injected_sorting is None or not dumpable. + It must be specified if injected_sorting is None or not serializable to file. Returns ------- diff --git a/src/spikeinterface/core/base.py b/src/spikeinterface/core/base.py index e3b88588e2..73f8619348 100644 --- a/src/spikeinterface/core/base.py +++ b/src/spikeinterface/core/base.py @@ -621,7 +621,7 @@ def dump_to_pickle( folder_metadata: str, Path, or None Folder with files containing additional information (e.g. probe in BaseRecording) and properties. """ - assert self.check_if_pickle_serializable(), "The extractor is not dumpable" + assert self.check_if_pickle_serializable(), "The extractor is not serializable to file with pickle" dump_dict = self.to_dict( include_annotations=True, @@ -658,8 +658,8 @@ def load(file_path: Union[str, Path], base_folder: Optional[Union[Path, str, boo d = pickle.load(f) else: raise ValueError(f"Impossible to load {file_path}") - if "warning" in d and "not dumpable" in d["warning"]: - print("The extractor was not dumpable") + if "warning" in d: + print("The extractor was not serializable to file") return None extractor = BaseExtractor.from_dict(d, base_folder=base_folder) return extractor @@ -822,7 +822,7 @@ def save_to_folder(self, name=None, folder=None, verbose=True, **save_kwargs): if self.check_serializablility("json"): self.dump(provenance_file) else: - provenance_file.write_text(json.dumps({"warning": "the provenace is not dumpable!!!"}), encoding="utf8") + provenance_file.write_text(json.dumps({"warning": "the provenace is not json serializable!!!"}), encoding="utf8") self.save_metadata_to_folder(folder) diff --git a/src/spikeinterface/core/job_tools.py b/src/spikeinterface/core/job_tools.py index 9369ad0b61..84ee502c14 100644 --- a/src/spikeinterface/core/job_tools.py +++ b/src/spikeinterface/core/job_tools.py @@ -170,8 +170,8 @@ def ensure_n_jobs(recording, n_jobs=1): if not recording.check_if_memory_serializable(): if n_jobs != 1: raise RuntimeError( - "Recording is not dumpable and can't be processed in parallel. " - "You can use the `recording.save()` function to make it dumpable or set 'n_jobs' to 1." + "Recording is not serializable to memory and can't be processed in parallel. " + "You can use the `rec = recording.save(folder=...)` function or set 'n_jobs' to 1." ) return n_jobs diff --git a/src/spikeinterface/core/tests/test_base.py b/src/spikeinterface/core/tests/test_base.py index 8d0907c700..a944be3da0 100644 --- a/src/spikeinterface/core/tests/test_base.py +++ b/src/spikeinterface/core/tests/test_base.py @@ -34,30 +34,29 @@ def make_nested_extractors(extractor): def test_check_if_memory_serializable(): test_extractor = generate_recording(seed=0, durations=[2]) - # make a list of dumpable objects - extractors_dumpable = make_nested_extractors(test_extractor) - for extractor in extractors_dumpable: + # make a list of memory serializable objects + extractors_mem_serializable = make_nested_extractors(test_extractor) + for extractor in extractors_mem_serializable: assert extractor.check_if_memory_serializable() - # make not dumpable + # make not not memory serilizable test_extractor._serializablility["memory"] = False - extractors_not_dumpable = make_nested_extractors(test_extractor) - for extractor in extractors_not_dumpable: + extractors_not_mem_serializable = make_nested_extractors(test_extractor) + for extractor in extractors_not_mem_serializable: assert not extractor.check_if_memory_serializable() def test_check_if_serializable(): test_extractor = generate_recording(seed=0, durations=[2]) - # make a list of dumpable objects + # make a list of json serializable objects test_extractor._serializablility["json"] = True extractors_json_serializable = make_nested_extractors(test_extractor) for extractor in extractors_json_serializable: print(extractor) assert extractor.check_serializablility("json") - # make not dumpable - # test_extractor._is_json_serializable = False + # make of not json serializable objects test_extractor._serializablility["json"] = False extractors_not_json_serializable = make_nested_extractors(test_extractor) for extractor in extractors_not_json_serializable: diff --git a/src/spikeinterface/core/tests/test_core_tools.py b/src/spikeinterface/core/tests/test_core_tools.py index a3cd0caa92..223b2a8a3a 100644 --- a/src/spikeinterface/core/tests/test_core_tools.py +++ b/src/spikeinterface/core/tests/test_core_tools.py @@ -142,7 +142,6 @@ def test_write_memory_recording(): recording = NoiseGeneratorRecording( num_channels=2, durations=[10.325, 3.5], sampling_frequency=30_000, strategy="tile_pregenerated" ) - # make dumpable recording = recording.save() # write with loop diff --git a/src/spikeinterface/core/tests/test_job_tools.py b/src/spikeinterface/core/tests/test_job_tools.py index 7d7af6025b..a904e4dd32 100644 --- a/src/spikeinterface/core/tests/test_job_tools.py +++ b/src/spikeinterface/core/tests/test_job_tools.py @@ -36,7 +36,7 @@ def test_ensure_n_jobs(): n_jobs = ensure_n_jobs(recording, n_jobs=1) assert n_jobs == 1 - # dumpable + # check serializable n_jobs = ensure_n_jobs(recording.save(), n_jobs=-1) assert n_jobs > 1 @@ -45,7 +45,7 @@ def test_ensure_chunk_size(): recording = generate_recording(num_channels=2) dtype = recording.get_dtype() assert dtype == "float32" - # make dumpable + # make serializable recording = recording.save() chunk_size = ensure_chunk_size(recording, total_memory="512M", chunk_size=None, chunk_memory=None, n_jobs=2) @@ -90,7 +90,7 @@ def init_func(arg1, arg2, arg3): def test_ChunkRecordingExecutor(): recording = generate_recording(num_channels=2) - # make dumpable + # make serializable recording = recording.save() init_args = "a", 120, "yep" diff --git a/src/spikeinterface/core/tests/test_waveform_extractor.py b/src/spikeinterface/core/tests/test_waveform_extractor.py index 12dac52d43..2bbf5e9b0f 100644 --- a/src/spikeinterface/core/tests/test_waveform_extractor.py +++ b/src/spikeinterface/core/tests/test_waveform_extractor.py @@ -315,7 +315,7 @@ def test_recordingless(): recording = recording.save(folder=cache_folder / "recording1") sorting = sorting.save(folder=cache_folder / "sorting1") - # recording and sorting are not dumpable + # recording and sorting are not serializable wf_folder = cache_folder / "wf_recordingless" # save with relative paths diff --git a/src/spikeinterface/core/waveform_extractor.py b/src/spikeinterface/core/waveform_extractor.py index cd8a62f5bc..2710ff1338 100644 --- a/src/spikeinterface/core/waveform_extractor.py +++ b/src/spikeinterface/core/waveform_extractor.py @@ -290,11 +290,12 @@ def create( sorting.dump(folder / "sorting.json", relative_to=relative_to) elif sorting.check_serializablility("pickle"): # In this case we loose the relative_to!! + # TODO later the dump to pickle should dump the dictionary and so relative could be put back sorting.dump(folder / "sorting.pickle") else: warn( - "Sorting object is not dumpable, which might result in downstream errors for " - "parallel processing. To make the sorting dumpable, use the `sorting.save()` function." + "Sorting object is not serializable to file, which might result in downstream errors for " + "parallel processing. To make the sorting serializable, use the `sorting = sorting.save()` function." ) # dump some attributes of the recording for the mode with_recording=False at next load @@ -903,11 +904,11 @@ def save( if self.sorting.check_serializablility("json"): self.sorting.dump(folder / "sorting.json", relative_to=relative_to) elif self.sorting.check_serializablility("pickle"): - self.sorting.dump(folder / "sorting.pickle", relative_to=relative_to) + self.sorting.dump(folder / "sorting.pickle") else: warn( - "Sorting object is not dumpable, which might result in downstream errors for " - "parallel processing. To make the sorting dumpable, use the `sorting.save()` function." + "Sorting object is not serializable to file, which might result in downstream errors for " + "parallel processing. To make the sorting serializable, use the `sorting = sorting.save()` function." ) # dump some attributes of the recording for the mode with_recording=False at next load @@ -960,8 +961,8 @@ def save( zarr_root.attrs["sorting"] = check_json(sort_dict) else: warn( - "Sorting object is not dumpable, which might result in downstream errors for " - "parallel processing. To make the sorting dumpable, use the `sorting.save()` function." + "Sorting object is not json serializable, which might result in downstream errors for " + "parallel processing. To make the sorting serializable, use the `sorting = sorting.save()` function." ) recording_info = zarr_root.create_group("recording_info") recording_info.attrs["recording_attributes"] = check_json(rec_attributes) diff --git a/src/spikeinterface/postprocessing/spike_amplitudes.py b/src/spikeinterface/postprocessing/spike_amplitudes.py index 9eb5a815d4..ccd2121174 100644 --- a/src/spikeinterface/postprocessing/spike_amplitudes.py +++ b/src/spikeinterface/postprocessing/spike_amplitudes.py @@ -73,12 +73,6 @@ def _run(self, **job_kwargs): func = _spike_amplitudes_chunk init_func = _init_worker_spike_amplitudes n_jobs = ensure_n_jobs(recording, job_kwargs.get("n_jobs", None)) - if n_jobs != 1: - # TODO: avoid dumping sorting and use spike vector and peak pipeline instead - assert sorting.check_if_memory_serializable(), ( - "The sorting object is not dumpable and cannot be processed in parallel. You can use the " - "`sorting.save()` function to make it dumpable" - ) init_args = (recording, sorting.to_multiprocessing(n_jobs), extremum_channels_index, peak_shifts, return_scaled) processor = ChunkRecordingExecutor( recording, func, init_func, init_args, handle_returns=True, job_name="extract amplitudes", **job_kwargs diff --git a/src/spikeinterface/sorters/tests/test_launcher.py b/src/spikeinterface/sorters/tests/test_launcher.py index 14c938f8ba..a5e29c8fd9 100644 --- a/src/spikeinterface/sorters/tests/test_launcher.py +++ b/src/spikeinterface/sorters/tests/test_launcher.py @@ -178,7 +178,7 @@ def test_run_sorters_with_list(): if working_folder.is_dir(): shutil.rmtree(working_folder) - # make dumpable + # make serializable rec0 = load_extractor(cache_folder / "toy_rec_0") rec1 = load_extractor(cache_folder / "toy_rec_1") From 0ea10e3baf97fbcedc8c25c2745754cacabb7b5c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 09:13:52 +0000 Subject: [PATCH 39/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spikeinterface/core/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/spikeinterface/core/base.py b/src/spikeinterface/core/base.py index 73f8619348..e8b3232e13 100644 --- a/src/spikeinterface/core/base.py +++ b/src/spikeinterface/core/base.py @@ -822,7 +822,9 @@ def save_to_folder(self, name=None, folder=None, verbose=True, **save_kwargs): if self.check_serializablility("json"): self.dump(provenance_file) else: - provenance_file.write_text(json.dumps({"warning": "the provenace is not json serializable!!!"}), encoding="utf8") + provenance_file.write_text( + json.dumps({"warning": "the provenace is not json serializable!!!"}), encoding="utf8" + ) self.save_metadata_to_folder(folder) From eb80725559f6d5b3d1c882e9254e39e39331952d Mon Sep 17 00:00:00 2001 From: Garcia Samuel Date: Wed, 27 Sep 2023 11:30:41 +0200 Subject: [PATCH 40/44] Update doc/modules/qualitymetrics/amplitude_cv.rst --- doc/modules/qualitymetrics/amplitude_cv.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/qualitymetrics/amplitude_cv.rst b/doc/modules/qualitymetrics/amplitude_cv.rst index 3edb1f9833..13117b607c 100644 --- a/doc/modules/qualitymetrics/amplitude_cv.rst +++ b/doc/modules/qualitymetrics/amplitude_cv.rst @@ -46,7 +46,7 @@ Example code References ---------- -.. autofunction:: spikeinterface.qualitymetrics.misc_metrics.compute_amplitude_spreads +.. autofunction:: spikeinterface.qualitymetrics.misc_metrics.compute_amplitude_cv_metrics Literature From 7605222e5707f6451a2ecc8b4fdbde747883c7bc Mon Sep 17 00:00:00 2001 From: Zach McKenzie <92116279+zm711@users.noreply.github.com> Date: Wed, 27 Sep 2023 06:49:32 -0400 Subject: [PATCH 41/44] rec_path = None, from Sam Co-authored-by: Garcia Samuel --- src/spikeinterface/exporters/to_phy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/spikeinterface/exporters/to_phy.py b/src/spikeinterface/exporters/to_phy.py index edfca0fa52..54ad0ea366 100644 --- a/src/spikeinterface/exporters/to_phy.py +++ b/src/spikeinterface/exporters/to_phy.py @@ -156,6 +156,8 @@ def export_to_phy( if use_relative_path: if copy_binary: f.write(f"dat_path = r'recording.dat'\n") + elif rec_path == "None": + f.write(f"dat_path = {rec_path}\n") else: f.write(f"dat_path = r'{str(Path(rec_path).relative_to(output_folder))}'\n") else: From f16b12c040ab512ce30e17219ca61e84168cc586 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:49:49 +0000 Subject: [PATCH 42/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spikeinterface/exporters/to_phy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spikeinterface/exporters/to_phy.py b/src/spikeinterface/exporters/to_phy.py index 54ad0ea366..ebc810b953 100644 --- a/src/spikeinterface/exporters/to_phy.py +++ b/src/spikeinterface/exporters/to_phy.py @@ -157,7 +157,7 @@ def export_to_phy( if copy_binary: f.write(f"dat_path = r'recording.dat'\n") elif rec_path == "None": - f.write(f"dat_path = {rec_path}\n") + f.write(f"dat_path = {rec_path}\n") else: f.write(f"dat_path = r'{str(Path(rec_path).relative_to(output_folder))}'\n") else: From 957a169e9cb663446398ed7e44abe47209e85619 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Wed, 27 Sep 2023 13:18:45 +0200 Subject: [PATCH 43/44] hotfix: synchrony metrics indexing --- src/spikeinterface/qualitymetrics/misc_metrics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/spikeinterface/qualitymetrics/misc_metrics.py b/src/spikeinterface/qualitymetrics/misc_metrics.py index f449b3c31b..e9726a16da 100644 --- a/src/spikeinterface/qualitymetrics/misc_metrics.py +++ b/src/spikeinterface/qualitymetrics/misc_metrics.py @@ -552,12 +552,13 @@ def compute_synchrony_metrics(waveform_extractor, synchrony_sizes=(2, 4, 8), uni continue spike_complexity = complexity[np.isin(unique_spike_index, spikes_per_unit["sample_index"])] for synchrony_size in synchrony_sizes: - synchrony_counts[synchrony_size][unit_id] += np.count_nonzero(spike_complexity >= synchrony_size) + synchrony_counts[synchrony_size][unit_index] += np.count_nonzero(spike_complexity >= synchrony_size) # add counts for this segment synchrony_metrics_dict = { f"sync_spike_{synchrony_size}": { - unit_id: synchrony_counts[synchrony_size][unit_id] / spike_counts[unit_id] for unit_id in unit_ids + unit_id: synchrony_counts[synchrony_size][all_unit_ids.index(unit_id)] / spike_counts[unit_id] + for unit_id in unit_ids } for synchrony_size in synchrony_sizes } From 5fbc88d416f863784ee7ed890c45f04726d4dc5a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:01:43 +0000 Subject: [PATCH 44/44] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../sortingcomponents/clustering/clustering_tools.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/spikeinterface/sortingcomponents/clustering/clustering_tools.py b/src/spikeinterface/sortingcomponents/clustering/clustering_tools.py index 273b1402fe..af3a9cb86a 100644 --- a/src/spikeinterface/sortingcomponents/clustering/clustering_tools.py +++ b/src/spikeinterface/sortingcomponents/clustering/clustering_tools.py @@ -533,7 +533,13 @@ def remove_duplicates( def remove_duplicates_via_matching( - waveform_extractor, noise_levels, peak_labels, method_kwargs={}, job_kwargs={}, tmp_folder=None, method="circus-omp-svd" + waveform_extractor, + noise_levels, + peak_labels, + method_kwargs={}, + job_kwargs={}, + tmp_folder=None, + method="circus-omp-svd", ): from spikeinterface.sortingcomponents.matching import find_spikes_from_templates from spikeinterface import get_noise_levels