"""Spike-train distance metrics — ISI, SPIKE, SPIKE-synchronization.
Thin wrappers around PySpike (https://github.com/mariomulansky/PySpike),
a BSD-2 library with C/Cython acceleration that implements the
Kreuz / Mulansky family of time-resolved spike-train distances:
- **ISI-distance** (Kreuz 2007) — instantaneous dissimilarity in ISIs.
- **SPIKE-distance** (Kreuz 2013) — instantaneous dissimilarity in
spike timing. Bounded in [0, 1].
- **SPIKE-synchronization** (Kreuz 2015) — fraction of "synchronous"
spikes; symmetric in [0, 1].
All three are *parameter-free* — no kernel bandwidth, no binning.
That's their main advantage over rate-based or kernel-density measures.
Install:
pip install nstat-toolbox[metrics]
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from nstat import nspikeTrain
from nstat.extras._lazy import require_optional
if TYPE_CHECKING: # pragma: no cover - typing only
pass
def _require_pyspike():
return require_optional("pyspike", install_key="metrics")
def _to_pyspike(nst: nspikeTrain):
"""Convert an :class:`nspikeTrain` to a ``pyspike.SpikeTrain``."""
spk = _require_pyspike()
return spk.SpikeTrain(
spike_times=np.asarray(nst.spikeTimes, dtype=float),
edges=(float(nst.minTime), float(nst.maxTime)),
)
[docs]
def isi_distance(a: nspikeTrain, b: nspikeTrain) -> float:
"""ISI-distance between two spike trains (Kreuz 2007).
Returns
-------
float
Scalar ISI-distance in [0, 1].
"""
spk = _require_pyspike()
return float(spk.isi_distance(_to_pyspike(a), _to_pyspike(b)))
[docs]
def spike_distance(a: nspikeTrain, b: nspikeTrain) -> float:
"""SPIKE-distance between two spike trains (Kreuz 2013).
Returns
-------
float
Scalar SPIKE-distance in [0, 1].
"""
spk = _require_pyspike()
return float(spk.spike_distance(_to_pyspike(a), _to_pyspike(b)))
[docs]
def spike_synchronization(a: nspikeTrain, b: nspikeTrain) -> float:
"""SPIKE-synchronization between two spike trains (Kreuz 2015).
Returns
-------
float
Scalar synchronization in [0, 1] — fraction of "coincident"
spikes (1.0 = perfectly synchronous, 0.0 = no coincidences).
"""
spk = _require_pyspike()
return float(spk.spike_sync(_to_pyspike(a), _to_pyspike(b)))
[docs]
def pairwise_spike_distance_matrix(
trains: list[nspikeTrain],
) -> np.ndarray:
"""N×N matrix of pairwise SPIKE-distances over a population.
Parameters
----------
trains
List of nstat spike trains; all must share the same recording
window (``minTime`` / ``maxTime``).
Returns
-------
numpy.ndarray
Symmetric matrix of shape ``(N, N)``; diagonal is zero.
"""
spk = _require_pyspike()
pyspike_trains = [_to_pyspike(t) for t in trains]
return np.asarray(spk.spike_distance_matrix(pyspike_trains), dtype=float)
__all__ = [
"isi_distance",
"spike_distance",
"spike_synchronization",
"pairwise_spike_distance_matrix",
]