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.

Momentum visualization on a 2D ravine loss.
Optimizer
Step 0. Loss: 243. Optimizer SGD. Learning rate 0.020. Waiting.

Three optimizers, one ravine. Step forward to see how each navigates the elongated loss surface.

Customize
Optimizer
SGD
0.020
0.90
Start
6.0
3.0

Installation

npx shadcn@latest add https://craftbits.dev/r/momentum-viz.json

Usage

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

  1. 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 a min label.
  2. 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's animate() driving raw SVG attributes — no re-render per frame.
  3. Optimizer toggle. A three-button radio group switches between SGD, Momentum, and Adam. Changing the optimizer resets the trail because the math is path-dependent (the velocity / second-moment buffers reset to zero).
  4. Learning rate + β sliders. Native range inputs with a value pill above each thumb. The β slider auto-disables outside Momentum since SGD ignores β entirely and Adam manages its own β₁ / β₂ internally.
  5. Phase machine. derivePhase(history, isExploded) returns observe while no steps have been taken (the dot pulses softly), running while the loss is still being driven down, converged once the loss drops below 0.01, and diverged when |x| or |y| escapes the explode threshold. Each phase has its own narration prose and palette.
  6. Auto + Step. Step advances the simulation one iteration. Auto polls the same step function on an 80 ms interval, capped at maxSteps. Auto auto-stops on divergence, on reaching the cap, or when the user toggles it off.
  7. 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

PropTypeDefaultDescription
defaultOptimizer"SGD" | "Momentum" | "Adam""SGD"Which optimizer is selected initially.
defaultLearningRatenumber0.02Initial slider value. Clamped to [0.001, 0.1].
defaultBetanumber0.9Initial 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.
explodeThresholdnumber50|x| or |y| above which the walker is considered diverged.
maxStepsnumber200Hard cap on the simulation length under Auto.
transitionTransitionSPRINGS.smoothOverride the step animation transition.
onStep(history) => voidFires after each step, post-animation, with the full trajectory.
onReset() => voidFires when the user clicks Reset.
classNamestringMerged onto the root via cn().

Accessibility

  • The plot SVG is role="img" with an aria-label summarising the active optimizer, learning rate, β, and step count.
  • The optimizer toggle is a role="radiogroup" of three buttons with aria-checked and visible focus rings. Keyboard users can reach every control with Tab.
  • 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-round usePredictRounds quiz pool with reveal prose and FeedbackBadge/ScoreDots/DoneCard chrome, and ChallengeBtn / SvgLabel / TogglePill / LabeledSlider imports from the lesson chrome — plus per-track palette tokens and an inline SPRINGS.gentle reference. The viz extract drops the predict mode entirely so the primitive is a pure interactive simulation, remaps every colour to var(--cb-*) semantic tokens, re-keys the dot animation to the canonical SPRINGS.smooth, replaces the lesson chrome with bare radio buttons and a thin local labeled-range, and exposes defaultOptimizer / defaultLearningRate / defaultBeta / defaultStart / explodeThreshold / maxSteps / transition / onStep / onReset props so the simulation is fully reusable outside any lesson chrome.