| """
|
| visualization_utils.py - Plotting and visualization helpers
|
| """
|
|
|
| from __future__ import annotations
|
|
|
| import logging
|
| from typing import Optional
|
| import matplotlib.pyplot as plt
|
| import networkx as nx
|
| import numpy as np
|
| import plotly.graph_objects as go
|
|
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
| def _pattern_to_grid(pattern: np.ndarray) -> np.ndarray:
|
| """Convert a 1D binary pattern to a 2D grid for display."""
|
| pattern = np.asarray(pattern).astype(float)
|
| size = pattern.shape[0]
|
|
|
|
|
| KNOWN_SIDES = {784: 28, 400: 20, 225: 15, 196: 14, 100: 10, 64: 8, 49: 7, 25: 5}
|
| if size in KNOWN_SIDES:
|
| side = KNOWN_SIDES[size]
|
| return pattern.reshape(side, side)
|
|
|
|
|
| side = int(np.round(np.sqrt(size)))
|
| if side * side == size:
|
| return pattern.reshape(side, side)
|
|
|
|
|
| side = int(np.ceil(np.sqrt(size)))
|
| padded = np.zeros(side * side)
|
| padded[:size] = pattern
|
| return padded.reshape(side, side)
|
|
|
|
|
| def plot_pattern_grid(pattern: np.ndarray, title: str = "Pattern", cmap: str = "Blues") -> plt.Figure:
|
| """Render a 1D binary pattern as a 2D grid image."""
|
| if pattern is None or len(pattern) == 0:
|
| pattern = np.zeros(100)
|
|
|
| pattern = np.asarray(pattern, dtype=float)
|
|
|
|
|
| vmin, vmax = 0.0, 1.0
|
| if pattern.max() > 1.0:
|
| pattern = pattern / 255.0
|
|
|
| grid = _pattern_to_grid(pattern)
|
|
|
| fig, ax = plt.subplots(figsize=(3, 3))
|
| ax.imshow(grid, cmap=cmap, vmin=vmin, vmax=vmax, interpolation='nearest')
|
| ax.set_title(title, fontsize=11, color='white', pad=6, fontweight='bold')
|
| ax.set_xticks([])
|
| ax.set_yticks([])
|
| fig.patch.set_facecolor('#0a1628')
|
| ax.set_facecolor('#0a1628')
|
| for spine in ax.spines.values():
|
| spine.set_color('#4a9eff')
|
| spine.set_linewidth(1.5)
|
| fig.tight_layout(pad=0.5)
|
| return fig
|
|
|
|
|
| def plot_pattern_comparison(original: np.ndarray, partial: np.ndarray, completed: np.ndarray) -> plt.Figure:
|
| """Show original / partial / completed patterns side by side."""
|
| arrays = [np.asarray(a, dtype=float) for a in (original, partial, completed)]
|
|
|
| arrays = [a / 255.0 if a.max() > 1.0 else a for a in arrays]
|
| titles = ["Original", "Partial Input", "Completed"]
|
|
|
| fig, axes = plt.subplots(1, 3, figsize=(10, 3.5))
|
| fig.patch.set_facecolor('#0a1628')
|
|
|
| for ax, arr, title in zip(axes, arrays, titles):
|
| grid = _pattern_to_grid(arr)
|
| ax.imshow(grid, cmap="Blues", vmin=0, vmax=1, interpolation='nearest')
|
| ax.set_title(title, fontsize=12, color='white', fontweight='bold')
|
| ax.set_xticks([])
|
| ax.set_yticks([])
|
| ax.set_facecolor('#0a1628')
|
| for spine in ax.spines.values():
|
| spine.set_color('#4a9eff')
|
| spine.set_linewidth(1.5)
|
| fig.tight_layout()
|
| return fig
|
|
|
|
|
| def plot_confidence_bars(scores: dict[int, float], highlight: Optional[int] = None) -> go.Figure:
|
| """Bar chart of recognition confidence scores."""
|
| if not scores:
|
| scores = {0: 0.0}
|
| labels = [f"Pattern {k}" for k in scores.keys()]
|
| values = list(scores.values())
|
| colors = ['#00ff88' if k == highlight else '#4a9eff' for k in scores.keys()]
|
|
|
| fig = go.Figure(data=[go.Bar(
|
| x=labels, y=values, marker_color=colors,
|
| text=[f"{v:.2f}" for v in values], textposition='outside', textfont_color='white'
|
| )])
|
| fig.update_layout(
|
| title="Recognition Confidence", title_font_color='white',
|
| yaxis_title="Confidence (assembly overlap)", yaxis_range=[0, 1.05],
|
| height=400, paper_bgcolor='#0a1628', plot_bgcolor='#0a1628',
|
| font_color='white', margin=dict(t=50, b=30, l=30, r=30),
|
| xaxis=dict(gridcolor='#1a3a6a', showgrid=True, tickfont_color='white', zeroline=False),
|
| yaxis=dict(gridcolor='#1a3a6a', showgrid=True, tickfont_color='white', zeroline=False),
|
| bargap=0.3,
|
| )
|
| return fig
|
|
|
|
|
| def plot_activity_heatmap(activity: np.ndarray, area_labels: Optional[list[str]] = None) -> go.Figure:
|
| """Heatmap of per-area neural activity."""
|
| if activity is None or activity.size == 0:
|
| activity = np.zeros((1, 100))
|
| n_areas, n_neurons = activity.shape
|
| max_display = min(500, n_neurons)
|
| if n_neurons > max_display:
|
| idx = np.linspace(0, n_neurons - 1, max_display).astype(int)
|
| display_activity = activity[:, idx]
|
| else:
|
| display_activity = activity
|
| labels = area_labels or [f"Area {i}" for i in range(n_areas)]
|
|
|
| fig = go.Figure(data=go.Heatmap(
|
| z=display_activity.astype(int), y=labels,
|
| colorscale=[[0, '#0a1628'], [1, '#4a9eff']],
|
| showscale=True, colorbar=dict(title="Active", title_font_color='white', tickfont_color='white')
|
| ))
|
| fig.update_layout(
|
| title="Neural Activity (k-cap winners highlighted)", title_font_color='white',
|
| xaxis_title="Neuron index (subsampled)",
|
| height=250 + 40 * n_areas, paper_bgcolor='#0a1628', plot_bgcolor='#0a1628',
|
| font_color='white', margin=dict(t=50, b=30, l=60, r=30),
|
| xaxis=dict(gridcolor='#1a3a6a', showgrid=True, tickfont_color='white', zeroline=False),
|
| yaxis=dict(gridcolor='#1a3a6a', showgrid=True, tickfont_color='white', zeroline=False),
|
| )
|
| return fig
|
|
|
|
|
| def plot_assembly_network(n_display_neurons: int = 40, active_indices: Optional[np.ndarray] = None,
|
| area_label: str = "Area") -> go.Figure:
|
| """Draw an illustrative network graph highlighting an assembly."""
|
| if n_display_neurons < 3:
|
| n_display_neurons = 40
|
| G = nx.erdos_renyi_graph(n_display_neurons, p=0.08, seed=0)
|
| pos = nx.spring_layout(G, seed=0)
|
| active_set = set(active_indices.tolist()) if active_indices is not None else set()
|
|
|
| edge_x, edge_y = [], []
|
| for u, v in G.edges():
|
| x0, y0 = pos[u]
|
| x1, y1 = pos[v]
|
| edge_x += [x0, x1, None]
|
| edge_y += [y0, y1, None]
|
| edge_trace = go.Scatter(x=edge_x, y=edge_y, mode="lines",
|
| line=dict(width=0.5, color="#2a4a7a"), hoverinfo="none")
|
|
|
| node_x, node_y, node_color, node_size = [], [], [], []
|
| for n in G.nodes():
|
| x, y = pos[n]
|
| node_x.append(x)
|
| node_y.append(y)
|
| is_active = n in active_set
|
| node_color.append("#00ff88" if is_active else "#4a9eff")
|
| node_size.append(16 if is_active else 8)
|
| node_trace = go.Scatter(x=node_x, y=node_y, mode="markers", hoverinfo="none",
|
| marker=dict(color=node_color, size=node_size,
|
| line=dict(width=1, color="#0a1628")))
|
|
|
| fig = go.Figure(data=[edge_trace, node_trace])
|
| fig.update_layout(
|
| title=f"{area_label}: Assembly Highlighted", title_font_color='white',
|
| showlegend=False, height=400, paper_bgcolor='#0a1628', plot_bgcolor='#0a1628',
|
| font_color='white', margin=dict(t=50, b=10, l=10, r=10),
|
| xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
|
| yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
|
| )
|
| return fig
|
|
|
|
|
| def plot_assembly_overlap_matrix(overlap: np.ndarray) -> go.Figure:
|
| """Heatmap of pairwise overlap between learned assemblies."""
|
| if overlap is None or overlap.size == 0:
|
| overlap = np.array([[1.0]])
|
| labels = [f"P{i}" for i in range(overlap.shape[0])]
|
| fig = go.Figure(data=go.Heatmap(
|
| z=overlap, x=labels, y=labels,
|
| colorscale=[[0, '#0a1628'], [0.5, '#1a3a6a'], [1, '#4a9eff']],
|
| zmin=0, zmax=1, text=np.round(overlap, 2),
|
| texttemplate="%{text}", textfont_color='white'
|
| ))
|
| fig.update_layout(
|
| title="Assembly Overlap Between Patterns", title_font_color='white',
|
| height=400, paper_bgcolor='#0a1628', plot_bgcolor='#0a1628',
|
| font_color='white', margin=dict(t=50, b=30, l=30, r=30),
|
| xaxis=dict(gridcolor='#1a3a6a', showgrid=True, tickfont_color='white', zeroline=False),
|
| yaxis=dict(gridcolor='#1a3a6a', showgrid=True, tickfont_color='white', zeroline=False),
|
| )
|
| return fig
|
|
|
|
|
| def plot_training_timeline(history: list[dict]) -> go.Figure:
|
| """Line chart showing assembly stabilization over rounds."""
|
| if not history:
|
| history = [{"pattern": 0, "round": 0, "stability": 1.0}]
|
| fig = go.Figure()
|
| colors = ['#4a9eff', '#00ff88', '#ff6b6b', '#ffd93d', '#a66cff',
|
| '#ff6b9d', '#6bffb8', '#ffb86b']
|
| patterns = sorted(set(h["pattern"] for h in history))
|
| for idx, p in enumerate(patterns):
|
| rounds = [h["round"] for h in history if h["pattern"] == p]
|
| stability = [h["stability"] for h in history if h["pattern"] == p]
|
| color = colors[idx % len(colors)]
|
| fig.add_trace(go.Scatter(
|
| x=rounds, y=stability, mode="lines+markers", name=f"Pattern {p}",
|
| line=dict(color=color, width=2.5), marker=dict(color=color, size=8)
|
| ))
|
| fig.update_layout(
|
| title="Assembly Formation Timeline", title_font_color='white',
|
| xaxis_title="Training Round",
|
| yaxis_title="Stability (overlap with final assembly)",
|
| yaxis_range=[0, 1.05], height=400, paper_bgcolor='#0a1628', plot_bgcolor='#0a1628',
|
| font_color='white', margin=dict(t=50, b=40, l=50, r=30),
|
| legend=dict(font_color='white', bgcolor='rgba(10, 22, 40, 0.8)',
|
| bordercolor='#1a3a6a', borderwidth=1),
|
| xaxis=dict(gridcolor='#1a3a6a', showgrid=True, tickfont_color='white', zeroline=False),
|
| yaxis=dict(gridcolor='#1a3a6a', showgrid=True, tickfont_color='white', zeroline=False),
|
| )
|
| return fig |