From 3622b896a0bcf8aa47dbdacf285e1e7e2d848cfa Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sat, 18 Apr 2026 01:51:43 -0400 Subject: [PATCH] amygdala: z-score, hysteresis, default to deepest layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three readability fixes for the F8 screen: * Z-score values per-layer by default (`[z]` toggles to raw dot- product). Raw values are dominated by residual-stream magnitude — z-scores read as "σ above concept-vector baseline" which is interpretable and scale-stable across frames. * Stable ordering with TOP_K + HYSTERESIS hysteresis band. Pinned concept set only rotates when a member drops out of the hysteresis band by |value| rank — bars update values in place without names flickering row-to-row. * Default to the deepest hooked layer (index 3 = layer 58 of 64). Clustering validation showed layer 58 is the only one with strong within-family cohesion (fear +0.37, shame +0.29, sadness +0.25 cosine); earlier layers are mostly noise for this task. Co-Authored-By: Proof of Concept --- src/user/amygdala.rs | 181 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 146 insertions(+), 35 deletions(-) diff --git a/src/user/amygdala.rs b/src/user/amygdala.rs index 380d2bd..b803e26 100644 --- a/src/user/amygdala.rs +++ b/src/user/amygdala.rs @@ -26,10 +26,21 @@ use crate::agent::api::ReadoutManifest; use crate::agent::readout::ReadoutEntry; const TOP_K: usize = 20; +/// Hysteresis band around TOP_K. A concept currently in the display +/// is kept as long as its |z-score| rank stays in the top +/// ``TOP_K + HYSTERESIS``; only falls out when it drops below that. +/// Prevents the ticker-tape flicker that pure top-K sorting produces. +const HYSTERESIS: usize = 20; pub(crate) struct AmygdalaScreen { selected_layer: usize, mode: DisplayMode, + /// Concept indices currently pinned in display order. Values at + /// these indices change every frame; the set only rotates when a + /// pinned concept drops out of the hysteresis band. + display_indices: Vec, + /// Whether to show z-scored values (default) or raw dot products. + normalize: bool, } #[derive(Clone, Copy, PartialEq)] @@ -43,8 +54,15 @@ enum DisplayMode { impl AmygdalaScreen { pub fn new() -> Self { Self { - selected_layer: 0, - mode: DisplayMode::Current, + // Default to the deepest hooked layer — emotion/concept + // circuits concentrate in the last ~20% of the network, + // and our clustering validation showed layer 58 was the + // only one with strong within-family cohesion. Bounded + // down to the actual layer count at render time. + selected_layer: 3, + mode: DisplayMode::MeanRecent, + display_indices: Vec::new(), + normalize: true, } } } @@ -66,6 +84,14 @@ impl ScreenView for AmygdalaScreen { DisplayMode::Current => DisplayMode::MeanRecent, DisplayMode::MeanRecent => DisplayMode::Current, }; + // Re-pin on mode change; the relative + // magnitudes between current-token and + // mean-recent differ substantially. + self.display_indices.clear(); + } + KeyCode::Char('z') => { + self.normalize = !self.normalize; + self.display_indices.clear(); } _ => {} } @@ -97,15 +123,33 @@ impl ScreenView for AmygdalaScreen { self.selected_layer = 0; } - // Compute the values to display: either the latest token's row - // for the selected layer, or the mean across recent tokens. - let values: Option> = match self.mode { + // Compute the raw values for the selected layer: either the + // latest token's row, or the mean across recent tokens. Raw + // means un-normalized dot products — their absolute scale is + // dominated by residual-stream norm, not concept alignment. + let raw: Option> = match self.mode { DisplayMode::Current => entries .last() .and_then(|e| e.readout.get(self.selected_layer).cloned()), DisplayMode::MeanRecent => mean_layer(&entries, self.selected_layer), }; + // Optional z-score normalization: remove the per-layer mean, + // scale by std. Result is "σ above/below the concept-vector + // average at this layer" — the loud-residual-stream scaling + // factor cancels out, values become comparable across frames. + let display_values = raw.as_ref().map(|v| { + if self.normalize { z_score(v) } else { v.clone() } + }); + + // Update the pinned display set with hysteresis: a concept + // stays pinned while it remains in the top (TOP_K + HYSTERESIS) + // by |value|; falls out only when it drops below that band. + // Keeps rows stable while values update in place. + if let Some(v) = display_values.as_ref() { + self.refresh_display_indices(v); + } + let layout = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -116,9 +160,12 @@ impl ScreenView for AmygdalaScreen { .split(area); render_header(frame, layout[0], &manifest, self.selected_layer, - self.mode, entries.len()); - match values { - Some(v) => render_bars(frame, layout[1], &manifest.concepts, &v), + self.mode, entries.len(), self.normalize); + match display_values { + Some(v) => render_bars( + frame, layout[1], &manifest.concepts, &v, + &self.display_indices, self.normalize, + ), None => render_empty_bars(frame, layout[1]), } render_recent(frame, layout[2], &entries, self.selected_layer, @@ -126,6 +173,38 @@ impl ScreenView for AmygdalaScreen { } } +impl AmygdalaScreen { + /// Add concepts entering the hysteresis band; evict concepts that + /// dropped out. Preserves existing order for concepts that stay. + fn refresh_display_indices(&mut self, values: &[f32]) { + let n = values.len(); + if n == 0 { + return; + } + // Rank all concepts by |value| desc so we can check both "in + // strict top-K" and "in hysteresis band (top K + H)" cheaply. + let mut rank: Vec<(usize, f32)> = values.iter() + .enumerate().map(|(i, v)| (i, v.abs())).collect(); + rank.sort_by(|a, b| b.1.partial_cmp(&a.1) + .unwrap_or(std::cmp::Ordering::Equal)); + let hyst_cutoff = (TOP_K + HYSTERESIS).min(n); + let in_band: std::collections::HashSet = + rank.iter().take(hyst_cutoff).map(|(i, _)| *i).collect(); + // Drop anything that left the band. + self.display_indices.retain(|i| in_band.contains(i)); + // Fill up to TOP_K by walking the top-K-by-|value| and adding + // any concept not already displayed. + for (i, _) in rank.iter().take(TOP_K) { + if self.display_indices.len() >= TOP_K { + break; + } + if !self.display_indices.contains(i) { + self.display_indices.push(*i); + } + } + } +} + fn render_disabled(frame: &mut Frame, area: Rect) { let text = Paragraph::new(Line::from(vec![ Span::raw("readout disabled — server did not return a manifest. "), @@ -141,13 +220,15 @@ fn render_disabled(frame: &mut Frame, area: Rect) { } fn render_header(frame: &mut Frame, area: Rect, manifest: &ReadoutManifest, - selected: usize, mode: DisplayMode, n_tokens: usize) { + selected: usize, mode: DisplayMode, n_tokens: usize, + normalize: bool) { let mode_str = match mode { DisplayMode::Current => "current", DisplayMode::MeanRecent => "mean(recent)", }; + let scale_str = if normalize { "z-score" } else { "raw" }; let layer = manifest.layers.get(selected).copied().unwrap_or(0); - let mut spans = vec![ + let spans = vec![ Span::styled("layer ", Style::default().fg(Color::DarkGray)), Span::styled( format!("{}/{} ", selected + 1, manifest.layers.len()), @@ -158,46 +239,53 @@ fn render_header(frame: &mut Frame, area: Rect, manifest: &ReadoutManifest, Span::styled(") ", Style::default().fg(Color::DarkGray)), Span::styled("mode ", Style::default().fg(Color::DarkGray)), Span::styled(mode_str, Style::default().fg(Color::Yellow)), + Span::styled(" scale ", Style::default().fg(Color::DarkGray)), + Span::styled(scale_str, Style::default().fg(Color::Yellow)), Span::styled(" ", Style::default()), Span::styled( format!("{} toks in ring", n_tokens), Style::default().fg(Color::DarkGray), ), + Span::raw(" "), + Span::styled( + format!("[1-{}] layer [t] mode [z] z-score/raw", + manifest.layers.len().min(9)), + Style::default().fg(Color::DarkGray), + ), ]; - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("[1-{}] layer [t] toggle mode", manifest.layers.len().min(9)), - Style::default().fg(Color::DarkGray), - )); let para = Paragraph::new(Line::from(spans)) .block(Block::default().borders(Borders::ALL).title("amygdala")); frame.render_widget(para, area); } fn render_bars(frame: &mut Frame, area: Rect, - concepts: &[String], values: &[f32]) { - // Sort indices by |value| descending, take top K. - let mut indexed: Vec<(usize, f32)> = values.iter() - .enumerate().map(|(i, v)| (i, *v)).collect(); - indexed.sort_by(|a, b| b.1.abs().partial_cmp(&a.1.abs()) - .unwrap_or(std::cmp::Ordering::Equal)); - indexed.truncate(TOP_K.min(concepts.len())); - + concepts: &[String], values: &[f32], + display_indices: &[usize], normalize: bool) { let inner = Block::default().borders(Borders::ALL) .title("top concepts"); let inner_area = inner.inner(area); frame.render_widget(inner, area); - if inner_area.height == 0 || indexed.is_empty() { + if inner_area.height == 0 || display_indices.is_empty() { return; } - // Find the max absolute value so bars are comparable. - let max_abs = indexed.iter().map(|(_, v)| v.abs()) - .fold(0.0_f32, f32::max) - .max(1e-6); + // Bar-scale normalization. For z-score mode, pin the bar to a + // fixed reference (|z| = 3 = full bar) so the visual magnitude + // has a meaningful interpretation ("3σ from baseline"). For raw + // mode, fall back to the old behavior (scale to the loudest + // concept on-screen). + let scale_ref: f32 = if normalize { + 3.0 + } else { + display_indices.iter() + .filter_map(|&i| values.get(i)) + .map(|v| v.abs()) + .fold(0.0_f32, f32::max) + .max(1e-6) + }; - let rows = (inner_area.height as usize).min(indexed.len()); + let rows = (inner_area.height as usize).min(display_indices.len()); let row_constraints: Vec = std::iter::repeat(Constraint::Length(1)).take(rows).collect(); let chunks = Layout::default() @@ -205,16 +293,22 @@ fn render_bars(frame: &mut Frame, area: Rect, .constraints(row_constraints) .split(inner_area); - for (i, (c_idx, v)) in indexed.iter().take(rows).enumerate() { - let label = concepts.get(*c_idx).cloned() + for (row, &c_idx) in display_indices.iter().take(rows).enumerate() { + let v = values.get(c_idx).copied().unwrap_or(0.0); + let label = concepts.get(c_idx).cloned() .unwrap_or_else(|| format!("c{}", c_idx)); - let ratio = (v.abs() / max_abs).clamp(0.0, 1.0); - let color = if *v >= 0.0 { Color::Green } else { Color::Red }; + let ratio = (v.abs() / scale_ref).clamp(0.0, 1.0); + let color = if v >= 0.0 { Color::Green } else { Color::Red }; + let display_num = if normalize { + format!("{:+.2}σ", v) + } else { + format!("{:+.3}", v) + }; let gauge = Gauge::default() .ratio(ratio as f64) .gauge_style(Style::default().fg(color).bg(Color::Reset)) - .label(format!("{:<26} {:+.3}", truncate_name(&label, 26), v)); - frame.render_widget(gauge, chunks[i]); + .label(format!("{:<26} {}", truncate_name(&label, 26), display_num)); + frame.render_widget(gauge, chunks[row]); } } @@ -263,6 +357,23 @@ fn render_recent(frame: &mut Frame, area: Rect, entries: &[ReadoutEntry], frame.render_widget(para, area); } +/// Z-score normalize: `(v - mean) / std` across the concept axis. +/// Result is comparable across frames and layers (the residual-stream +/// magnitude factors out) and has the nice property that "this is +/// ≥2σ elevated" has a concrete meaning regardless of scale. +fn z_score(values: &[f32]) -> Vec { + let n = values.len() as f32; + if n == 0.0 { + return Vec::new(); + } + let mean = values.iter().sum::() / n; + let var = values.iter() + .map(|v| (v - mean) * (v - mean)) + .sum::() / n; + let std = var.sqrt().max(1e-6); + values.iter().map(|v| (v - mean) / std).collect() +} + fn mean_layer(entries: &[ReadoutEntry], layer: usize) -> Option> { let rows: Vec<&Vec> = entries.iter() .filter_map(|e| e.readout.get(layer))