Skip to content

Try to remove get_template_extremum_channel() and peak_sign in many place#4374

Draft
samuelgarcia wants to merge 6 commits intoSpikeInterface:mainfrom
samuelgarcia:less_peak_sign_more_main_channel
Draft

Try to remove get_template_extremum_channel() and peak_sign in many place#4374
samuelgarcia wants to merge 6 commits intoSpikeInterface:mainfrom
samuelgarcia:less_peak_sign_more_main_channel

Conversation

@samuelgarcia
Copy link
Copy Markdown
Member

This PR try to

  • change semantic extremum_channel to main_channel
  • remove get_template_extremum_channel() to use instead
    • analyzer.get_main_channel() which is now a property
    • templates.get_main_channel() which compute on the fly the old behavior
  • remove the peak_sign when it was use to get the main channel
  • use explicitly main_channel_peak_sign instead of peak_sign when it is for main channel
  • keep peak_sig when it makes sens, like peak detection or in sorter context.

The main consequences:

  • an analyzer is constructed with main_channel_index. The sparsity is estimated by default on it. This will be faster when
    it is provided
  • The analyzer has always via the sorting object acces to this property so all metrics will consistently depend on it.

@alejoe91
@chrishalcrow
@ecobost
@yger

Copy link
Copy Markdown
Contributor

@ecobost ecobost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @samuelgarcia Thanks for taking time to work in this; I checked the code from analyzer-> metrics->widgets (anything before analyzer I don't really know).

After going through the code, the only extensions that still receive peak_sign are spike_amplitudes, quality metrics and template_metrics; and that argument can be dropped from all of them (in xxxxxx_metrics is not used at all, while in spike_amplitudes is used once but could be replaced with analyzer.main_channel_peak_sign). Given that, I would vote to rename main_channel_peak_sign to just peak_sign in the sorting analyzer (and essentially use it as the global peak_sign that will be used if any extension needs it, similar to sparsity), including ofc to find the main_channel.

In theory, main_channel_peak_mode falls in the same camp but in truth that seems to only be used to pick the main_channel so main_channel_peak_mode as name might be ok.

overwrite=False,
backend_options=None,
**sparsity_kwargs,
sparsity_kwargs=None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This argument could be dropped. It's not the responsibility of this function to create all types of sparsity. You could either provide an Sparsity object or let create_sorting_analyzer create it with the default method (from_radius_and_main_channel_index). I feel like that's fair enough (it's more of a convenience) and simplifies the sparsity logic here (curently there are three or four different ways to get sparsity). It also avoids some possible inconsistencies (like main_channel_peak_sign != sparsity_kwargs['peak_sign']) or num_spikes_for_main_channel vs sparsity_kwargs['num_spikes_for_sparsity']

The one common use case that will be affected is people that want to create a sorting analyzer with a bigger or smaller radius (the only sparsity_kwargs that would affect the radius_um method + main_index); but maybe they'll just have to estimate sparsity explicitly (and feed it here).

)

elif method != "by_property":
nbefore = int(ms_before * recording.sampling_frequency / 1000.0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code (L.816-849) is a duplicate of template_tools.estimate_main_channel_from_recording (used in create_sorting_analyzer to find the main_channel). If it's too bothersome to unify both pieces of code somehow, maybe we can at least refactor so radius method here uses estimate_main_channel_from_recording so we can guarantee that:

sparsity = create_sparsity(peak_sign=my_peak_sign, num_spikes_for_sparsity=my_num_spikes)
analyzer1 =  create_sorting_analyzer(sparsity=sparsity)
analyzer2 = create_sorting_analyzer(main_channel_peak_sign=my_peak_sign, num_spikes_for_main_channel=my_num_spikes)
assert analyzer1.sparsity == analyzer2.sparsity 

Essentially refactor it as

if method== radius:
    if main_channel_index is None:
        main_channel_index = estimate_main_channel_from_recording(...)
    sparsity = .... from_radius_and_main_channel(...)
elif method != 'property'
       # the repeated code here (any changes won't affect radius method)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does not receive peak_sign anymore

sparsity = ChannelSparsity.from_closest_channels(templates_or_sorting_analyzer, num_channels)
elif method == "radius":
assert radius_um is not None, "For the 'radius' method, 'radius_um' needs to be given"
sparsity = ChannelSparsity.from_radius(templates_or_sorting_analyzer, radius_um, peak_sign=peak_sign)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from_radius does not receive peak_sign anymore

ms_before: float = 1.0,
ms_after: float = 2.5,
method: "radius" | "best_channels" | "closest_channels" | "amplitude" | "snr" | "by_property" = "radius",
peak_sign: "neg" | "pos" | "both" = "neg",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider changing default to "both" to agree with default in create_sorting_analyzer (main_channel_peak_sign)


if sorting_analyzer_or_templates.sparsity is None:
sparsity = compute_sparsity(
sorting_analyzer_or_templates, peak_sign=peak_sign, method="radius", radius_um=radius_um
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

peak_sign is no longer used for method radius ((compute_sparsity uses analyzer/templates.get_main_channels) so not needed here. Can also be dropped as an argument (only was used in this line)

unit_ids: str | int | None
A list of unit_id to restrci the computation
peak_sign : "neg" | "pos" | "both", default: "neg"
Sign of the template to compute best channels
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not true anymore; now it is only used to invert the gaussian prototype template (in l. 275); I think peak_sign='neg' default makes sense in this case


if unit_ids is None:
unit_ids = sorting_analyzer.unit_ids
peak_sign = self.params["peak_sign"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used anywhere

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory you can drop peak_sign (it is no longer used anywhere here); in practice, it will be used in the bombcell PR (and probably by Alessio/Chris when they re-work the template metrics) but perhaps rather than as an argument, it can be read from analyzer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is no longer needed (it was only used for the extremum_channel in the PCA metrics)

@ecobost
Copy link
Copy Markdown
Contributor

ecobost commented Feb 7, 2026

Also, given that you are touching this code anyway, do you mind sending peak_sign here too

unit_peak_shifts = get_template_extremum_channel_peak_shift(sorting_analyzer)

I don't mean to make it work with the analyzer (although maybe it makes sense to forward the analyzer.main_channel_peak_sign here too) but just to forward the peak_sign argument like

unit_peak_shifts = get_template_extremum_channel_peak_shift(sorting_analyzer, peak_sign=peak_sign)

thanks 👍🏽

templates_array = self.get_dense_templates()
main_channel_index = _get_main_channel_from_template_array(templates_array, mode, main_channel_peak_sign, self.nbefore)

if outputs is "index":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should use == (rather than is)


if outputs is "index":
main_chans = main_channel_index
elif outputs is "id":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should use == (rather than is)


"""
main_channel_index = self.get_sorting_property("main_channel_index")
if outputs is "index":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should use == (rather than is)

main_channel_index = self.get_sorting_property("main_channel_index")
if outputs is "index":
main_chans = main_channel_index
elif outputs is "id":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should use == (rather than is)

@alejoe91 alejoe91 added the core Changes to core module label Feb 10, 2026
@samuelgarcia samuelgarcia added this to the 0.104.0 milestone Feb 16, 2026
Comment on lines +650 to +654
if "main_channel_peak_sign" not in settings:
# before 0.104.0 was not in main_channel_peak_sign
# TODO make something more fancy that exlore the previous params of extension
new_settings["main_channel_peak_sign"] = "both"
new_settings["main_channel_peak_mode"] = "extremum"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello - Sam asked me to think about backwards compatibility. Here are my thoughts!

Before this PRs, we had peak_sign as a parameter for lots of postprocessig methods. Now we're getting rid of it. We want to ensure backwards compatibility. Note: we cannot be fully backwards compatible - we used to allow for different peak_sign methods for different extensions. Note: THIS WAS BAD. It meant there could be confusing inconsistencies between the results.

When you load an "old" analyzer, you get a sparsity.mask but no main_channel_indices. Use cases of loading an "old" analyzer:

  1. Have a look at it
  2. Do some curation
  3. Recompute extensions with new parameters

The existing analyzer has a sparsity mask. We should respect it. If we do that, 1. is resolved - the analyzer will basically look the same as before.

Currently, when we do a curation, sparsity of the new units is re-computed as a union or intersection. At this stage, we'll also need to recompute the main_channel_index for the merge/split. For this we'll need to know which peak_sign to use.

Recomputing is fine. If a user is recomputing, they know results might be different (improved!!) on a new version.

So, we just need to decide how to compute main_channel_indices in case of curation or re-computation. So that we respect the existing sparsity, I think we should only allow main_channel_indices which exist on the sparsity mask. The options are then:

  1. Set peak_sign to some reasonable default value ('both'!!)
  2. Try to reverse engineer the peak_sign using the templates (note: this is not guaranteed to work: the sparsity estimation that runs on analyzer creation generates templates on the fly - these are not the same as the templates in the analyzer. Or "both" and "neg" will give the same result => problem not invertible. Note: even if we reverse-engineer the peak_sign, the user might have set a different peak_sign for e.g. snr computation)

I think going for "both" is fine. It's easier to explain and the other option is too awkward.

Ok. So my proposal for backwards compat is: on load, compute main_channel_indices using peak_sign = 'both' but only on the channels included in the sparsity mask.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Changes to core module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants