Momentum Viz
A 2D contour plot of the ravine L(x, y) = ½(x² + 50y²) with a three-way optimizer toggle (SGD / Momentum / Adam) and live learning-rate and momentum-β sliders. The visualization simulates the chosen optimizer from a fixed start (defaults to (6, 3)), animates the dot to its next position with a spring, and draws the full trajectory as a polyline behind it. The phase auto-classifies as observe / running / converged / diverged from the trajectory and recolours every element to match.
The three update rules at play:
SGD: x ← x − lr · ∇L(x)
Momentum: v ← β v + ∇L(x); x ← x − lr · v
Adam: m ← β₁ m + (1−β₁) ∇L
s ← β₂ s + (1−β₂) ∇L²
x ← x − lr · m̂ / (√ŝ + ε)
The ravine is intentionally elongated (B = 50 on the y-axis vs A = 1 on x), so the gradient at the start points almost entirely across the floor — exactly the situation where SGD bounces, Momentum builds along-floor velocity, and Adam reads its own second moment.
Three optimizers, one ravine. Step forward to see how each navigates the elongated loss surface.
Installation
npx shadcn@latest add https://craftbits.dev/r/momentum-viz.jsonUsage
import { MomentumViz } from "@craft-bits/viz/momentum-viz";
<MomentumViz />Start on Momentum so the visitor sees the along-floor acceleration immediately:
<MomentumViz defaultOptimizer="Momentum" defaultBeta={0.9} />Subscribe to step events for an external loss-vs-step chart:
<MomentumViz
onStep={(history) => {
/* read history.map((h) => h.loss) */
}}
/>Understanding the component
- The plot. A 480 × 360 SVG renders the contour map of
L = ½(x² + 50y²)as concentric ellipses — the level sets of the ravine. The x and y axes cross at the origin where the global minimum lives, marked with a small green disc and aminlabel. - The trajectory. Every optimizer step is computed in pure math (no DOM), then plotted as a polyline. The most recent vertex is the live dot, animated via
motion'sanimate()driving raw SVG attributes — no re-render per frame. - Optimizer toggle. A three-button radio group switches between
SGD,Momentum, andAdam. Changing the optimizer resets the trail because the math is path-dependent (the velocity / second-moment buffers reset to zero). - Learning rate + β sliders. Native range inputs with a value pill above each thumb. The β slider auto-disables outside
Momentumsince SGD ignores β entirely and Adam manages its ownβ₁/β₂internally. - Phase machine.
derivePhase(history, isExploded)returnsobservewhile no steps have been taken (the dot pulses softly),runningwhile the loss is still being driven down,convergedonce the loss drops below0.01, anddivergedwhen|x|or|y|escapes the explode threshold. Each phase has its own narration prose and palette. - Auto + Step.
Stepadvances the simulation one iteration.Autopolls the same step function on an 80 ms interval, capped atmaxSteps. Auto auto-stops on divergence, on reaching the cap, or when the user toggles it off. - Reduced motion. Under
prefers-reduced-motion: reduce, the dot snaps instantly to its target each step, the observe-phase idle pulse stops, and the converged celebration ring at the origin disables. The simulation itself is identical — only the visual smoothing is dropped.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOptimizer | "SGD" | "Momentum" | "Adam" | "SGD" | Which optimizer is selected initially. |
defaultLearningRate | number | 0.02 | Initial slider value. Clamped to [0.001, 0.1]. |
defaultBeta | number | 0.9 | Initial momentum β slider value. Clamped to [0, 0.99]. Only meaningful for the Momentum optimizer. |
defaultStart | { x: number; y: number } | { x: 6, y: 3 } | Starting point on the contour map. |
explodeThreshold | number | 50 | |x| or |y| above which the walker is considered diverged. |
maxSteps | number | 200 | Hard cap on the simulation length under Auto. |
transition | Transition | SPRINGS.smooth | Override the step animation transition. |
onStep | (history) => void | — | Fires after each step, post-animation, with the full trajectory. |
onReset | () => void | — | Fires when the user clicks Reset. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The plot SVG is
role="img"with anaria-labelsummarising the active optimizer, learning rate, β, and step count. - The optimizer toggle is a
role="radiogroup"of three buttons witharia-checkedand visible focus rings. Keyboard users can reach every control withTab. - The learning-rate and β sliders are native
<input type="range">, so they inherit the platform's keyboard and screen-reader behaviour (arrow keys to nudge, Home/End for bounds) and labelling. - A live region below the controls announces the current step, loss, optimizer, learning rate, and phase.
- The narration paragraph also has
aria-live="polite"and reads as plain prose; it is the canonical explanation for each phase. - Colour is never the only signal — the optimizer name is in the radio label and the live-region status, and the phase is encoded in the prose and live-region text.
- Motion respects
prefers-reduced-motion: reduce— the dot snaps instantly, the observe-phase idle pulse stops, and the converged celebration ring disables.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/viz/MomentumViz.tsx). The source bundled an Explore/Predict ModeStrip, a six-roundusePredictRoundsquiz pool with reveal prose and FeedbackBadge/ScoreDots/DoneCard chrome, andChallengeBtn/SvgLabel/TogglePill/LabeledSliderimports from the lesson chrome — plus per-track palette tokens and an inlineSPRINGS.gentlereference. The viz extract drops the predict mode entirely so the primitive is a pure interactive simulation, remaps every colour tovar(--cb-*)semantic tokens, re-keys the dot animation to the canonicalSPRINGS.smooth, replaces the lesson chrome with bare radio buttons and a thin local labeled-range, and exposesdefaultOptimizer/defaultLearningRate/defaultBeta/defaultStart/explodeThreshold/maxSteps/transition/onStep/onResetprops so the simulation is fully reusable outside any lesson chrome.