From WaveformExtractor to SortingAnalyzer

From spikeinterface version 0.101, the internal structure of the postprocessing module has changed. We explain the motivation for the change in more detail below.

From the user point of view, the key change is the deletion of the WaveformExtractor class and the addition of the new SortingAnalyzer class. Hence any code using WaveformExtractors will have to be updated. If you want to continue using WaveformExtractor you need to use spikeinterface versions 0.100 and below.

Updating your old code should be straightforward. On this page, we demonstrate how to convert old WaveformExtractor folders to new SortingAnalyzer folders and have written a code dictionary which should make updating your codebase simple.

Why change?

Previously, a lot of post-processing was handled by the WaveformExtractor class. However, as the name suggests, this singles out waveforms as special. But many outputs don’t care about waveforms. And this isn’t just a naming problem: you had to create a WaveformExtractor object (and thus calculate waveforms) to calculate e.g. correlograms, which didn’t depend on them. Then to access this data you had to access the WaveformExtractor. We felt that this was confusing to the user: why are the correlograms stored in a waveform class?? This mixing of waveforms with non-waveform objects also makes the codebase messy and harder to develop.

Our new class is called the SortingAnalyzer. You can think of this as combining a recording and a sorting into one. There are then lots of extensions which you can calculate. Waveforms are one extension of the SortingAnalyzer, but aren’t special. We hope the new class is simpler to understand, especially for beginners. It also comes with some new features: you can calculate multiple extensions at the same time, it has better caching and better zarr support (hopefully leading to fast interactive widgets in the future).

Convert a WaveformExtractor folder to a SortingAnalyzer folder

There are several backward compatibility tools to help deal with existing WaveformExtractor folders. For now, you can still read these folders using load_waveforms, which creates a MockWaveformExtractor object. This object contains a SortingAnalyzer which can be accessed and then saved as follows

waveform_folder_path = "path_to/my_waveform_extractor_folder"
new_sorting_analyzer_path = "path_to/my_new_sorting_analyzer_folder"
# On Windows
# new_sorting_analyzer_path = r"path_to\my_new_sorting_analyzer_folder"
wvf_extractor = load_waveform(folder=waveform_folder_path)
sorting_analyzer = wvf_extractor.sorting_analyzer
sorting_analyzer.save_as(folder=new_sorting_analyzer_path, format="binary_folder")

The above code creates a SortingAnalyzer folder at new_sorting_analyzer_path.

Dictionary between SortingAnalyzer and WaveformExtractor

This section provides a dictionary for code, to help translate between a SortingAnalyzer and a WaveformExtractor, so that users can easily update old code. If you want to learn how to use SortingAnalyzer from scratch, the Get Started guide and the postprocessing module documentation are better places to start.

This section is split into four subsections:

Throughout this section, we assume that all functions have been imported into the namespace. E.g. we have run code such as from spikeinterface.core import create_sorting_analyzer for each function. If you have imported the full package (ie you have run import spikeinterface.full as si) you need to prepend all functions by si.. If you have imported individual modules (ie you have run import spikeinterface.postprocessing as spost) we will need to prepend functions by the appropriate submodule name.

In the following we start with a recording called recording and a sorting object called sorting. We’ll then create, export, explore and compute things using a SortingAnalyzer called analyzer and a WaveformExtractor called wvf_extractor. We’ll do the same calculations for both objects. The WaveformExtractor code will be on the left while the SortingAnalyzer code will be displayed on the right:

WaveformExtractor

SortingAnalyzer

Create, load and save

First, create the object from a recording and a sorting.

wvf_extractor = extract_waveforms(
    sorting=sorting,
    recording=recording
)
analyzer = create_sorting_analyzer(
    sorting=sorting,
    recording=recording
)

By default, the object is stored in memory. In this case, if you end your session without saving (which you can do using save_as, see below) you’ll lose everything! Alternatively, we can save it locally at the point of creation by specifying a folder and a format. Additionally, you can decide whether to use sparsity or not

wvf_extractor = extract_waveforms(
    sorting=sorting,
    recording=recording,
    folder="my_waveform_extractor",
    mode="folder",
    sparse=True
)
analyzer = create_sorting_analyzer(
    sorting=sorting,
    recording=recording,
    folder="my_sorting_analyzer",
    format="binary_folder",
    sparse=True
)

You can save the object after you’ve created it, with the option of saving it to a new format

wvf_extractor.save(
    format="zarr",
    folder="/path/to_my/result.zarr"
)
analyzer.save_as(
    format="zarr",
    folder="/path/to_my/result.zarr"
)

If you already have the object saved, you can load it

wvf_extractor = load_waveforms(
    folder="my_waveform_extractor"
)
analyzer = load_sorting_analyzer(
    folder="my_sorting_analyzer"
)

Checking basic properties

The object contains both a sorting and a recording object. These can be isolated

the_recording = wvf_extractor.recording
the_sorting = wvf_extractor.sorting
the_recording = analyzer.recording
the_sorting = analyzer.sorting

You can then check any recording or sorting properties from these objects.

There is much information about the recording and sorting contained in the parent object. E.g. you can get the channel locations as follows

channel_locations =
    wvf_extractor.get_channel_locations()
channel_locations =
    analyzer.get_channel_locations()

Many properties can be accessed in a similar way

wvf_extractor.get_num_channels()
wvf_extractor.get_num_samples()
wvf_extractor.get_num_segments()
wvf_extractor.get_probe()
wvf_extractor.get_probegroup()
wvf_extractor.get_total_duration()
wvf_extractor.get_total_samples()
analyzer.get_num_channels()
analyzer.get_num_samples()
analyzer.get_num_segments()
analyzer.get_probe()
analyzer.get_probegroup()
analyzer.get_total_duration()
analyzer.get_total_samples()

…while some are simply properties of the object

wvf_extractor.channel_ids
wvf_extractor.unit_ids
wvf_extractor.sampling_frequency
analyzer.channel_ids
analyzer.unit_ids
analyzer.sampling_frequency

You can also find some fundamental properties of the object, though these are mostly used internally:

wvf_extractor.folder
wvf_extractor.format
wvf_extractor.is_read_only()
wvf_extractor.dtype
wvf_extractor.is_sparse()
analyzer.folder
analyzer.format
analyzer.is_read_only()
analyzer.get_dtype()
analyzer.is_sparse()

Compute Extensions

Waveforms, templates, quality metrics etc are all extensions of the SortingAnalyzer object. Some extensions depend on other extensions. To calculate a child we must first have calculated its parents. The relationship between all the currently available extensions is displayed below:

Child parent relationships

We see that to compute spike_amplitudes we must first compute templates. To compute templates we must first compute waveforms. To compute waveforms we must first compute random_spikes. Phew! Some of these extensions were calculated automatically for WaveformExtractors, so the code looks slightly different. Let’s calculate these extensions, and also add a parameter for spike_amplitudes

wvf_extractor.precompute_templates(
    modes=("average",)
)
compute_spike_amplitudes(
    waveform_extractor=wvf_extractor,
    peak_sign="pos"
)
analyzer.compute("random_spikes")
analyzer.compute("waveforms")
analyzer.compute("templates")
analyzer.compute(
    "spike_amplitudes",
    peak_sign="pos"
)

Note that if a parent is recomputed, all its children are deleted to maintain data consistency. Also note that the load_if_exists semantic that was found in the WaveformExtractor has been removed from the SortingAnalyzer. This means that each call to compute will recompute and overwrite the previous results. Adjust your scripts according to account for this new behavior. Read more about this, the extensions and their keyword arguments in the postprocessing module documentation

In many cases, you can still use the old notation for SortingAnalyzer objects, such as compute_spike_amplitudes(sorting_analyzer=analyzer).

In all cases, if the object has been saved locally, the extensions will be saved locally too. You can check which extensions have been saved

wvf_extractor.get_available_extension_names()
analyzer.get_saved_extension_names()

You can now also check which extensions are currently loaded in memory. The WaveformExtractor checks the local folder and the memory:

wvf_extractor.get_available_extension_names()
analyzer.get_loaded_extension_names()

If there is an extensions which is saved but not yet loaded you can load it:

wvf_extractor.load_extension(
    extension_name="spike_amplitudes"
)
analyzer.load_extension(
    extension_name="spike_amplitudes"
)

You can also check if a certain extension is loaded

wvf_extractor.has_extension(
    extension_name="spike_amplitudes"
)
analyzer.has_extension(
    extension_name="spike_amplitudes"
)

You can delete extensions. Note that if you delete a parent, all of its children will be deleted too. We’ll now delete templates from the SortingAnalyzer and spike_amplitudes from our WaveformExtractor.

wvf_extractor.delete_extension(
    extension_name="spike_amplitudes"
)
# This also deletes any children
# such as spike_amplitudes
analyzer.delete_extension(
    extension_name="templates"
)

Once you have computed an extension, you often want to look at the data associated with it. This has been standardized for the SortingAnalyzer object, through the get_data method. The retrieval methods for the WaveformExtractor object were less uniform, and depended on which extension you were interested in. We won’t list them all here.

wv_data = wvf_extractor.get_waveforms(
    unit_id=0
)

ul_data = compute_unit_locations(
    waveform_extractor=wvf_extractor
)
wv = analyzer.get_extension(
    extension_name="waveforms"
)
wv_data = wv.get_data()
ul = analyzer.get_extension(
    extension_name="unit_locations"
)
ul_data = ul.get_data()

You can also access the parameters used in the extension calculation, which is very simple for the new SortingAnalyzer:

ul_ex = wvf_extractor.load_extension(
    extension_name="unit_locations"
)
ul_parms = ul_ex.load_params_from_folder(
    folder="my_waveform_extractor"
)
ul_params = ul.params

Quality metrics

Quality metrics for the SortingAnalyzer are also extensions. You can calculate a specific quality metric using the metric_names argument. In contrast, for WaveformExtractors you need to find the correct function. The old functions still work for SortingAnalyzers.

amp_cut_data = compute_amplitude_cutoffs(
    waveform_extractor=wvf_extractor
)
#or: compute_amplitude_cutoffs(
#        wvf_extractor
#    )
amp_cutoff = analyzer.compute(
    "quality_metrics",
    metric_names=["amplitude_cutoff"]
)
amp_cut_data = amp_cutoff.get_data()
#or: compute_amplitude_cutoff(analyzer)

Or you can calculate all available quality metrics. Here, we also pass a list of quality metric parameters.

dqm_params = get_default_qm_params()
amp_cut_data = compute_quality_metrics(
    waveform_extractor=wvf_extractor,
    qm_params=dqm_params
)
dqm_params = get_default_qm_params()
amp_cutoff = analyzer.compute(
    "quality_metrics",
    qm_params=dqm_params
)
#alt: compute_quality_metrics(analyzer)

Learn more about the possible quality metrics and their keyword arguments in the quality metrics documentation page.