Skip to content

Visualization

12 publication-quality spike train plots: raster, voltage, ISI, cross-correlogram, PSD, firing rate, phase portrait, population activity, instantaneous rate, spike comparison, network graph, weight matrix.

Python
from sc_neurocore.viz import plot_raster, plot_voltage, plot_isi

sc_neurocore.viz

sc_neurocore.viz -- Tier: research (experimental / research).

NeuroArtGenerator dataclass

Generates Art (Images) from Neural State.

Source code in src/sc_neurocore/viz/neuro_art.py
Python
14
15
16
17
18
19
20
21
22
23
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
@dataclass
class NeuroArtGenerator:
    """
    Generates Art (Images) from Neural State.
    """

    resolution: int = 256

    def generate_visual(self, state_vector: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
        """
        Maps a 1D state vector to a 2D RGB abstract image.
        Uses the state to seed a generative pattern.
        """
        # Seed random generator with state hash to be deterministic per state
        # but chaotic
        seed = int(np.sum(np.abs(state_vector)) * 10000) % (2**32)
        rng = np.random.default_rng(seed)

        # Create base canvas
        img = np.zeros((self.resolution, self.resolution, 3), dtype=np.uint8)

        # 'Painters' driven by state elements
        num_painters = min(10, len(state_vector))

        for i in range(num_painters):
            val = state_vector[i]
            # Map value to color
            color = rng.integers(0, 255, 3)
            # Map value to position/size
            x = rng.integers(0, self.resolution)
            y = rng.integers(0, self.resolution)
            radius = int(abs(val) * 50) + 5

            # Draw circle (naive)
            y_grid, x_grid = np.ogrid[: self.resolution, : self.resolution]
            mask = (x_grid - x) ** 2 + (y_grid - y) ** 2 <= radius**2
            img[mask] = color

        return img

generate_visual(state_vector)

Maps a 1D state vector to a 2D RGB abstract image. Uses the state to seed a generative pattern.

Source code in src/sc_neurocore/viz/neuro_art.py
Python
22
23
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
def generate_visual(self, state_vector: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
    """
    Maps a 1D state vector to a 2D RGB abstract image.
    Uses the state to seed a generative pattern.
    """
    # Seed random generator with state hash to be deterministic per state
    # but chaotic
    seed = int(np.sum(np.abs(state_vector)) * 10000) % (2**32)
    rng = np.random.default_rng(seed)

    # Create base canvas
    img = np.zeros((self.resolution, self.resolution, 3), dtype=np.uint8)

    # 'Painters' driven by state elements
    num_painters = min(10, len(state_vector))

    for i in range(num_painters):
        val = state_vector[i]
        # Map value to color
        color = rng.integers(0, 255, 3)
        # Map value to position/size
        x = rng.integers(0, self.resolution)
        y = rng.integers(0, self.resolution)
        radius = int(abs(val) * 50) + 5

        # Draw circle (naive)
        y_grid, x_grid = np.ogrid[: self.resolution, : self.resolution]
        mask = (x_grid - x) ** 2 + (y_grid - y) ** 2 <= radius**2
        img[mask] = color

    return img

WebVisualizer

Generates a standalone HTML file to visualize the SC Network.

Source code in src/sc_neurocore/viz/web_viz.py
Python
 16
 17
 18
 19
 20
 21
 22
 23
 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
135
136
class WebVisualizer:
    """
    Generates a standalone HTML file to visualize the SC Network.
    """

    @staticmethod
    def generate_html(layers: list[Any], filename="network_viz.html") -> None:  # type: ignore[no-untyped-def]
        # 1. Build Graph Data
        nodes = []
        links = []

        # Input Node
        nodes.append({"id": "Input", "group": 0})

        for i, layer in enumerate(layers):
            layer_name = f"L{i}_{layer.__class__.__name__}"

            # Layer Node (representing the whole layer for simplicity)
            nodes.append(
                {"id": layer_name, "group": i + 1, "neurons": getattr(layer, "n_neurons", "?")}
            )

            # Link from prev
            prev = "Input" if i == 0 else f"L{i - 1}_{layers[i - 1].__class__.__name__}"
            links.append({"source": prev, "target": layer_name, "value": 1})

        data = {"nodes": nodes, "links": links}
        json_str = json.dumps(data)

        # 2. Embed in HTML Template
        html_content = f"""
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>SC-NeuroCore Visualization</title>
    <style>
        body {{ font-family: sans-serif; background: #111; color: #eee; }}
        #graph {{ width: 800px; height: 600px; border: 1px solid #333; margin: 20px auto; }}
        .node {{ fill: #69b3a2; stroke: #fff; }}
        .link {{ stroke: #999; stroke-opacity: 0.6; }}
        text {{ fill: #eee; font-size: 12px; }}
    </style>
</head>
<body>
    <h1 style="text-align:center">SC-NeuroCore Topology</h1>
    <div id="graph_container" style="text-align:center">
        <canvas id="graph" width="800" height="600"></canvas>
    </div>
    <script>
        const graphData = {json_str};
        const canvas = document.getElementById('graph');
        const ctx = canvas.getContext('2d');

        // Simple Force Layout Simulation
        const nodes = graphData.nodes;
        const links = graphData.links;

        // Initial random positions
        nodes.forEach(n => {{
            n.x = Math.random() * 800;
            n.y = Math.random() * 600;
            n.vx = 0; n.vy = 0;
        }});

        function draw() {{
            ctx.fillStyle = "#111";
            ctx.fillRect(0,0,800,600);

            // Draw Links
            ctx.strokeStyle = "#555";
            links.forEach(l => {{
                const src = nodes.find(n => n.id === l.source);
                const tgt = nodes.find(n => n.id === l.target);
                if(src && tgt) {{
                    ctx.beginPath();
                    ctx.moveTo(src.x, src.y);
                    ctx.lineTo(tgt.x, tgt.y);
                    ctx.stroke();
                }}
            }});

            // Draw Nodes
            nodes.forEach(n => {{
                ctx.fillStyle = n.group === 0 ? "#ff5555" : "#55aaff";
                ctx.beginPath();
                ctx.arc(n.x, n.y, 10, 0, 2*Math.PI);
                ctx.fill();
                ctx.fillStyle = "#fff";
                ctx.fillText(n.id, n.x + 12, n.y + 4);
                if(n.neurons) ctx.fillText(n.neurons + " neurons", n.x + 12, n.y + 16);
            }});
        }}

        function update() {{
            // Very simple layout: Linear positioning by group
            const groups = [...new Set(nodes.map(n => n.group))];
            const layerHeight = 500 / groups.length;

            nodes.forEach(n => {{
                // Target Y based on group
                const ty = 50 + n.group * 100;
                const tx = 400; // Center X

                n.x += (tx - n.x) * 0.1;
                n.y += (ty - n.y) * 0.1;
            }});

            draw();
            requestAnimationFrame(update);
        }}

        update();
    </script>
</body>
</html>
        """

        with open(filename, "w") as f:
            f.write(html_content)
        logger.info("Generated Visualization: %s", filename)

cross_correlogram(spike_monitor, i, j, max_lag_ms=50, ax=None)

Spike cross-correlation between neurons i and j.

Source code in src/sc_neurocore/viz/plots.py
Python
108
109
110
111
112
113
114
115
116
117
118
119
def cross_correlogram(
    spike_monitor: Any, i: int, j: int, max_lag_ms: int = 50, ax: Any = None
) -> Any:
    """Spike cross-correlation between neurons *i* and *j*."""
    _require_mpl()
    ax = _get_ax(ax)
    corr, lags = spike_monitor.cross_correlation(i, j, max_lag=int(max_lag_ms))
    ax.bar(lags, corr, width=1.0, color="teal", edgecolor="none")
    ax.set_xlabel("Lag (timesteps)")
    ax.set_ylabel("Correlation")
    ax.set_title(f"Cross-Correlogram N{i}–N{j}")
    return ax

firing_rate_plot(spike_monitor, bin_ms=10, ax=None)

Population firing rate histogram (spikes per bin).

Source code in src/sc_neurocore/viz/plots.py
Python
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def firing_rate_plot(spike_monitor: Any, bin_ms: int = 10, ax: Any = None) -> Any:
    """Population firing rate histogram (spikes per bin)."""
    _require_mpl()
    ax = _get_ax(ax)
    times, _ = spike_monitor.raster_data()
    if times.size == 0:
        ax.set_title("Firing Rate (no spikes)")
        return ax
    t_max = int(times.max()) + 1
    bins = np.arange(0, t_max + bin_ms, bin_ms)
    ax.hist(times, bins=bins, color="steelblue", edgecolor="none")
    ax.set_xlabel("Timestep")
    ax.set_ylabel("Spike count")
    ax.set_title("Population Firing Rate")
    return ax

instantaneous_rate_plot(spike_monitor, neuron_id, sigma_ms=20, ax=None)

Gaussian-kernel-smoothed instantaneous firing rate.

Source code in src/sc_neurocore/viz/plots.py
Python
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def instantaneous_rate_plot(
    spike_monitor: Any, neuron_id: int, sigma_ms: int = 20, ax: Any = None
) -> Any:
    """Gaussian-kernel-smoothed instantaneous firing rate."""
    _require_mpl()
    ax = _get_ax(ax)
    trains = spike_monitor.spike_trains
    ts = trains.get(neuron_id, np.array([], dtype=np.int64))
    if ts.size == 0:
        ax.set_title(f"Rate N{neuron_id} (no spikes)")
        return ax
    t_max = int(ts.max()) + 1
    binary = np.zeros(t_max, dtype=np.float64)
    binary[ts] = 1.0
    kernel_width = int(4 * sigma_ms)
    kernel_x = np.arange(-kernel_width, kernel_width + 1, dtype=np.float64)
    kernel = np.exp(-0.5 * (kernel_x / sigma_ms) ** 2)
    kernel /= kernel.sum()
    rate = np.convolve(binary, kernel, mode="same")
    ax.plot(np.arange(t_max), rate, linewidth=0.8)
    ax.set_xlabel("Timestep")
    ax.set_ylabel("Rate (spikes/step)")
    ax.set_title(f"Instantaneous Rate — Neuron {neuron_id}")
    return ax

isi_histogram(spike_monitor, neuron_id, bins=50, ax=None)

Inter-spike interval distribution for a single neuron.

Source code in src/sc_neurocore/viz/plots.py
Python
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def isi_histogram(spike_monitor: Any, neuron_id: int, bins: int = 50, ax: Any = None) -> Any:
    """Inter-spike interval distribution for a single neuron."""
    _require_mpl()
    ax = _get_ax(ax)
    intervals = spike_monitor.isi(neuron_id)
    if intervals.size == 0:
        ax.set_title(f"ISI N{neuron_id} (no spikes)")
        return ax
    ax.hist(intervals, bins=bins, color="coral", edgecolor="none")
    ax.set_xlabel("ISI (timesteps)")
    ax.set_ylabel("Count")
    ax.set_title(f"ISI Distribution — Neuron {neuron_id}")
    return ax

network_graph(network, ax=None)

Node/edge diagram of populations and projections.

Source code in src/sc_neurocore/viz/plots.py
Python
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def network_graph(network: Any, ax: Any = None) -> Any:
    """Node/edge diagram of populations and projections."""
    _require_mpl()
    ax = _get_ax(ax)
    pops = network.populations
    n = len(pops)
    if n == 0:
        ax.set_title("Network (empty)")
        return ax
    angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
    xs, ys = np.cos(angles), np.sin(angles)
    for idx, pop in enumerate(pops):
        ax.scatter(xs[idx], ys[idx], s=200, zorder=3)
        ax.annotate(pop.label, (xs[idx], ys[idx]), ha="center", va="bottom", fontsize=8)
    pop_index = {id(p): idx for idx, p in enumerate(pops)}
    for proj in network.projections:
        si = pop_index.get(id(proj.source))
        ti = pop_index.get(id(proj.target))
        if si is not None and ti is not None:
            ax.annotate(
                "",
                xy=(xs[ti], ys[ti]),
                xytext=(xs[si], ys[si]),
                arrowprops=dict(arrowstyle="->", color="gray"),
            )
    ax.set_aspect("equal")
    ax.set_title("Network Graph")
    ax.axis("off")
    return ax

phase_portrait(state_monitor, var_x='v', var_y='w', neuron_id=0, ax=None)

2-D phase-plane trajectory for a single neuron.

Source code in src/sc_neurocore/viz/plots.py
Python
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def phase_portrait(
    state_monitor: Any,
    var_x: str = "v",
    var_y: str = "w",
    neuron_id: int = 0,
    ax: Any = None,
) -> Any:
    """2-D phase-plane trajectory for a single neuron."""
    _require_mpl()
    ax = _get_ax(ax)
    traces = state_monitor.traces
    x = traces.get(var_x, np.empty((0, 0)))
    y = traces.get(var_y, np.empty((0, 0)))
    if x.size == 0 or y.size == 0:
        ax.set_title("Phase Portrait (no data)")
        return ax
    ax.plot(x[:, neuron_id], y[:, neuron_id], linewidth=0.8)
    ax.set_xlabel(var_x)
    ax.set_ylabel(var_y)
    ax.set_title(f"Phase Portrait — Neuron {neuron_id}")
    return ax

population_activity(spike_monitor, bin_ms=5, ax=None)

Heatmap of binned spike counts per neuron.

Source code in src/sc_neurocore/viz/plots.py
Python
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def population_activity(spike_monitor: Any, bin_ms: int = 5, ax: Any = None) -> Any:
    """Heatmap of binned spike counts per neuron."""
    _require_mpl()
    ax = _get_ax(ax)
    times, ids = spike_monitor.raster_data()
    if times.size == 0:
        ax.set_title("Population Activity (no spikes)")
        return ax
    n_neurons = spike_monitor.population.n
    t_max = int(times.max()) + 1
    n_bins = max(1, t_max // bin_ms)
    mat = np.zeros((n_neurons, n_bins), dtype=np.float64)
    for t, nid in zip(times, ids):
        b = min(int(t) // bin_ms, n_bins - 1)
        mat[nid, b] += 1
    ax.imshow(mat, aspect="auto", origin="lower", interpolation="nearest")
    ax.set_xlabel(f"Time bin ({bin_ms} steps)")
    ax.set_ylabel("Neuron ID")
    ax.set_title("Population Activity")
    return ax

psd_plot(spike_monitor, neuron_id, ax=None)

Power spectral density of a single neuron's spike train.

Source code in src/sc_neurocore/viz/plots.py
Python
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def psd_plot(spike_monitor: Any, neuron_id: int, ax: Any = None) -> Any:
    """Power spectral density of a single neuron's spike train."""
    _require_mpl()
    ax = _get_ax(ax)
    trains = spike_monitor.spike_trains
    ts = trains.get(neuron_id, np.array([], dtype=np.int64))
    if ts.size < 2:
        ax.set_title(f"PSD N{neuron_id} (too few spikes)")
        return ax
    t_max = int(ts.max()) + 1
    binary = np.zeros(t_max, dtype=np.float64)
    binary[ts] = 1.0
    freqs = np.fft.rfftfreq(t_max)
    power = np.abs(np.fft.rfft(binary - binary.mean())) ** 2
    ax.semilogy(freqs[1:], power[1:], linewidth=0.7)
    ax.set_xlabel("Frequency (cycles/step)")
    ax.set_ylabel("Power")
    ax.set_title(f"PSD — Neuron {neuron_id}")
    return ax

raster_plot(spike_monitor, ax=None, color='k', marker='.', s=1)

Spike raster from a SpikeMonitor.

Source code in src/sc_neurocore/viz/plots.py
Python
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def raster_plot(
    spike_monitor: Any,
    ax: Any = None,
    color: str = "k",
    marker: str = ".",
    s: float = 1,
) -> Any:
    """Spike raster from a SpikeMonitor."""
    _require_mpl()
    ax = _get_ax(ax)
    times, ids = spike_monitor.raster_data()
    ax.scatter(times, ids, c=color, marker=marker, s=s, linewidths=0)
    ax.set_xlabel("Timestep")
    ax.set_ylabel("Neuron ID")
    ax.set_title("Spike Raster")
    return ax

spike_train_comparison(trains, labels=None, ax=None)

Overlay multiple spike trains as event plots.

Parameters

trains : list of np.ndarray Each element is a 1-D array of spike timesteps. labels : list of str, optional Labels for each train.

Source code in src/sc_neurocore/viz/plots.py
Python
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def spike_train_comparison(
    trains: list[np.ndarray], labels: list[str] | None = None, ax: Any = None
) -> Any:
    """Overlay multiple spike trains as event plots.

    Parameters
    ----------
    trains : list of np.ndarray
        Each element is a 1-D array of spike timesteps.
    labels : list of str, optional
        Labels for each train.
    """
    _require_mpl()
    ax = _get_ax(ax)
    for idx, tr in enumerate(trains):
        lbl = labels[idx] if labels else f"Train {idx}"
        y_vals = np.full_like(tr, idx, dtype=np.float64)
        ax.scatter(tr, y_vals, s=2, label=lbl, linewidths=0)
    ax.set_xlabel("Timestep")
    ax.set_ylabel("Train index")
    ax.set_title("Spike Train Comparison")
    ax.legend(fontsize="small")
    return ax

voltage_trace(state_monitor, neuron_ids=None, ax=None)

Membrane voltage traces from a StateMonitor.

Source code in src/sc_neurocore/viz/plots.py
Python
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def voltage_trace(state_monitor: Any, neuron_ids: Any = None, ax: Any = None) -> Any:
    """Membrane voltage traces from a StateMonitor."""
    _require_mpl()
    ax = _get_ax(ax)
    traces = state_monitor.traces
    v = traces.get("v", np.empty((0, 0)))
    if v.size == 0:
        return ax
    t = state_monitor.t
    if neuron_ids is None:
        neuron_ids = range(v.shape[1])
    for nid in neuron_ids:
        ax.plot(t, v[:, nid], label=f"N{nid}")
    ax.set_xlabel("Timestep")
    ax.set_ylabel("Membrane voltage")
    ax.set_title("Voltage Traces")
    ax.legend(fontsize="small")
    return ax

weight_matrix(projection, ax=None, cmap='RdBu_r')

Connectivity weight heatmap from a Projection's CSR data.

Source code in src/sc_neurocore/viz/plots.py
Python
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def weight_matrix(projection: Any, ax: Any = None, cmap: str = "RdBu_r") -> Any:
    """Connectivity weight heatmap from a Projection's CSR data."""
    _require_mpl()
    ax = _get_ax(ax)
    n_src = projection.source.n
    n_tgt = projection.target.n
    dense = np.zeros((n_src, n_tgt), dtype=np.float64)
    for i in range(n_src):
        for k in range(projection.indptr[i], projection.indptr[i + 1]):
            dense[i, projection.indices[k]] = projection.data[k]
    im = ax.imshow(dense, aspect="auto", cmap=cmap, interpolation="nearest")
    ax.figure.colorbar(im, ax=ax, fraction=0.046)
    ax.set_xlabel("Target neuron")
    ax.set_ylabel("Source neuron")
    ax.set_title("Weight Matrix")
    return ax