Skip to content

Recorders

Spike train recording and analysis utilities.

  • BitstreamSpikeRecorder — records a 1D binary spike train (0/1 per timestep), returns a uint8 NumPy view, counts spikes, computes mean firing rate from dt_ms, and builds inter-spike-interval histograms.
Python
from sc_neurocore import BitstreamSpikeRecorder

recorder = BitstreamSpikeRecorder(dt_ms=1.0)
for _ in range(1000):
    spike = neuron.step(current)
    recorder.record(spike)

print(f"Total spikes: {recorder.total_spikes()}")
print(f"Firing rate: {recorder.firing_rate_hz():.1f} Hz")

The recorder rejects non-binary spikes, negative dt_ms values, and non-positive histogram bin counts. dt_ms=0.0 is still accepted for legacy dry-run callers and returns a firing rate of 0.0.

Polyglot Mirrors

The Python API is the canonical recorder surface. Parity helper surfaces are kept in accel/julia/recorders/spike_recorder.jl, accel/rust/safety/spike_recorder.rs, and accel/mojo/kernels/spike_recorder.mojo for backend validation and low-level firing-rate/ISI helpers. The Rust safety mirror owns crate-level unit tests; the Julia and Mojo mirrors are syntax-checked in the recorder maintenance lane.

sc_neurocore.recorders.spike_recorder.BitstreamSpikeRecorder dataclass

Record a binary spike train and compute basic spike statistics.

Parameters

dt_ms: Duration represented by one recorded sample in milliseconds. A value of 0.0 is accepted for legacy zero-duration dry runs and produces a firing rate of 0.0. spikes: Existing spike samples to seed the recorder with. Values must be binary integers where 1 means a spike occurred at that sample.

Source code in src/sc_neurocore/recorders/spike_recorder.py
Python
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@dataclass
class BitstreamSpikeRecorder:
    """Record a binary spike train and compute basic spike statistics.

    Parameters
    ----------
    dt_ms:
        Duration represented by one recorded sample in milliseconds. A value of
        ``0.0`` is accepted for legacy zero-duration dry runs and produces a
        firing rate of ``0.0``.
    spikes:
        Existing spike samples to seed the recorder with. Values must be binary
        integers where ``1`` means a spike occurred at that sample.
    """

    dt_ms: float = 1.0
    spikes: list[int] = field(default_factory=list)

    def __post_init__(self) -> None:
        """Validate seeded recorder state."""
        if self.dt_ms < 0.0:
            raise ValueError("dt_ms must be non-negative.")
        for spike in self.spikes:
            self._validate_spike(spike)

    @staticmethod
    def _validate_spike(spike: int) -> None:
        if spike not in (0, 1):
            raise ValueError("Spike must be 0 or 1.")

    def record(self, spike: int) -> None:
        """Append one binary spike sample.

        Parameters
        ----------
        spike:
            Binary sample to record. ``1`` represents a spike and ``0``
            represents silence for the current sample.

        Raises
        ------
        ValueError
            If ``spike`` is not ``0`` or ``1``.
        """
        self._validate_spike(spike)
        self.spikes.append(spike)

    def reset(self) -> None:
        """Remove all recorded spike samples while preserving ``dt_ms``."""
        self.spikes.clear()

    def as_array(self) -> np.ndarray[Any, Any]:
        """Return the recorded spike train as a NumPy ``uint8`` array."""
        return np.array(self.spikes, dtype=np.uint8)

    def total_spikes(self) -> int:
        """Return the number of recorded spike samples equal to ``1``."""
        return int(np.sum(self.as_array()))

    def firing_rate_hz(self) -> float:
        """Return the mean firing rate in hertz.

        The rate is computed as ``total_spikes / duration_seconds``. Empty
        recordings and legacy ``dt_ms == 0.0`` dry runs return ``0.0``.
        """
        spikes = self.as_array()
        sample_count = spikes.size
        if sample_count == 0:
            return 0.0
        duration_ms = sample_count * self.dt_ms
        if duration_ms == 0:
            return 0.0
        return float(self.total_spikes() / (duration_ms / 1000.0))

    def isi_histogram(
        self,
        bins: int = 10,
    ) -> tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]:
        """Compute a histogram of inter-spike intervals in milliseconds.

        Parameters
        ----------
        bins:
            Number of histogram bins. Must be positive.

        Returns
        -------
        hist:
            Histogram counts.
        bin_edges:
            Bin edges (ms).

        Raises
        ------
        ValueError
            If ``bins`` is less than one.
        """
        if bins < 1:
            raise ValueError("bins must be positive.")
        spikes = self.as_array()
        spike_indices = np.where(spikes == 1)[0]

        if spike_indices.size < 2:
            return np.zeros(bins, dtype=int), np.linspace(0, 1, bins + 1)

        isi_steps = np.diff(spike_indices)
        isi_ms = isi_steps * self.dt_ms

        histogram_range = (float(np.min(isi_ms)), float(np.max(isi_ms)))
        hist, bin_edges = np.histogram(isi_ms, bins=bins, range=histogram_range)
        return hist, bin_edges

__post_init__()

Validate seeded recorder state.

Source code in src/sc_neurocore/recorders/spike_recorder.py
Python
42
43
44
45
46
47
def __post_init__(self) -> None:
    """Validate seeded recorder state."""
    if self.dt_ms < 0.0:
        raise ValueError("dt_ms must be non-negative.")
    for spike in self.spikes:
        self._validate_spike(spike)

record(spike)

Append one binary spike sample.

Parameters

spike: Binary sample to record. 1 represents a spike and 0 represents silence for the current sample.

Raises

ValueError If spike is not 0 or 1.

Source code in src/sc_neurocore/recorders/spike_recorder.py
Python
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def record(self, spike: int) -> None:
    """Append one binary spike sample.

    Parameters
    ----------
    spike:
        Binary sample to record. ``1`` represents a spike and ``0``
        represents silence for the current sample.

    Raises
    ------
    ValueError
        If ``spike`` is not ``0`` or ``1``.
    """
    self._validate_spike(spike)
    self.spikes.append(spike)

reset()

Remove all recorded spike samples while preserving dt_ms.

Source code in src/sc_neurocore/recorders/spike_recorder.py
Python
71
72
73
def reset(self) -> None:
    """Remove all recorded spike samples while preserving ``dt_ms``."""
    self.spikes.clear()

as_array()

Return the recorded spike train as a NumPy uint8 array.

Source code in src/sc_neurocore/recorders/spike_recorder.py
Python
75
76
77
def as_array(self) -> np.ndarray[Any, Any]:
    """Return the recorded spike train as a NumPy ``uint8`` array."""
    return np.array(self.spikes, dtype=np.uint8)

total_spikes()

Return the number of recorded spike samples equal to 1.

Source code in src/sc_neurocore/recorders/spike_recorder.py
Python
79
80
81
def total_spikes(self) -> int:
    """Return the number of recorded spike samples equal to ``1``."""
    return int(np.sum(self.as_array()))

firing_rate_hz()

Return the mean firing rate in hertz.

The rate is computed as total_spikes / duration_seconds. Empty recordings and legacy dt_ms == 0.0 dry runs return 0.0.

Source code in src/sc_neurocore/recorders/spike_recorder.py
Python
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def firing_rate_hz(self) -> float:
    """Return the mean firing rate in hertz.

    The rate is computed as ``total_spikes / duration_seconds``. Empty
    recordings and legacy ``dt_ms == 0.0`` dry runs return ``0.0``.
    """
    spikes = self.as_array()
    sample_count = spikes.size
    if sample_count == 0:
        return 0.0
    duration_ms = sample_count * self.dt_ms
    if duration_ms == 0:
        return 0.0
    return float(self.total_spikes() / (duration_ms / 1000.0))

isi_histogram(bins=10)

Compute a histogram of inter-spike intervals in milliseconds.

Parameters

bins: Number of histogram bins. Must be positive.

Returns

hist: Histogram counts. bin_edges: Bin edges (ms).

Raises

ValueError If bins is less than one.

Source code in src/sc_neurocore/recorders/spike_recorder.py
Python
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def isi_histogram(
    self,
    bins: int = 10,
) -> tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]:
    """Compute a histogram of inter-spike intervals in milliseconds.

    Parameters
    ----------
    bins:
        Number of histogram bins. Must be positive.

    Returns
    -------
    hist:
        Histogram counts.
    bin_edges:
        Bin edges (ms).

    Raises
    ------
    ValueError
        If ``bins`` is less than one.
    """
    if bins < 1:
        raise ValueError("bins must be positive.")
    spikes = self.as_array()
    spike_indices = np.where(spikes == 1)[0]

    if spike_indices.size < 2:
        return np.zeros(bins, dtype=int), np.linspace(0, 1, bins + 1)

    isi_steps = np.diff(spike_indices)
    isi_ms = isi_steps * self.dt_ms

    histogram_range = (float(np.min(isi_ms)), float(np.max(isi_ms)))
    hist, bin_edges = np.histogram(isi_ms, bins=bins, range=histogram_range)
    return hist, bin_edges