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.

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
13
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
@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
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
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
 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
 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
class WebVisualizer:
    """
    Generates a standalone HTML file to visualize the SC Network.
    """

    @staticmethod
    def generate_html(layers: list[Any], filename="network_viz.html") -> None:
        # 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
 99
100
101
102
103
104
105
106
107
108
def cross_correlogram(spike_monitor, i, j, max_lag_ms=50, ax=None):
    """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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def firing_rate_plot(spike_monitor, bin_ms=10, ax=None):
    """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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def instantaneous_rate_plot(spike_monitor, neuron_id, sigma_ms=20, ax=None):
    """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
84
85
86
87
88
89
90
91
92
93
94
95
96
def isi_histogram(spike_monitor, neuron_id, bins=50, ax=None):
    """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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def network_graph(network, ax=None):
    """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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def phase_portrait(state_monitor, var_x="v", var_y="w", neuron_id=0, ax=None):
    """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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def population_activity(spike_monitor, bin_ms=5, ax=None):
    """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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def psd_plot(spike_monitor, neuron_id, ax=None):
    """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
35
36
37
38
39
40
41
42
43
44
def raster_plot(spike_monitor, ax=None, color="k", marker=".", s=1):
    """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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def 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.
    """
    _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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def voltage_trace(state_monitor, neuron_ids=None, ax=None):
    """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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def weight_matrix(projection, ax=None, cmap="RdBu_r"):
    """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