Momentum Viz
A 1-D ball that rolls down a loss curve under heavy-ball momentum dynamics: v_{t+1} = β·v_t + ∇L(x_t), then x_{t+1} = x_t − α·v_{t+1}. Set the momentum coefficient β to zero and steps stay tiny — pure SGD. Slide β toward 1 and velocity compounds: the ball rolls past the minimum, overshoots, and oscillates back. The velocity vector is rendered as an arrow on top of the ball so the about-to-take step is always legible.
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.jsonUsage
import { MomentumViz } from "@craft-bits/core";
<MomentumViz />Drive the momentum coefficient and step from outside the component:
const [coeff, setCoeff] = useState(0.9);
const [step, setStep] = useState(0);
<MomentumViz
momentumCoeff={coeff}
onMomentumCoeffChange={setCoeff}
currentStep={step}
onCurrentStepChange={setStep}
/>Provide a different 1-D loss function:
// A double-well so the ball can get stuck on the wrong side.
<MomentumViz
loss={(x) => (x * x - 1) ** 2}
initialX={-1.5}
xRange={[-2, 2]}
/>Autoplay through the trajectory:
<MomentumViz playing playSpeed={120} />Understanding the component
- Two coupled scalar updates. Every step the optimizer evaluates the gradient at
x_t, blends it into the velocity (v_{t+1} = β·v_t + g_t), then takes a step proportional to the new velocity (x_{t+1} = x_t − α·v_{t+1}). Atβ = 0the velocity collapses to the current gradient — pure SGD. Asβrises, the velocity term remembers more of the past, so the ball builds momentum even when the gradient flattens. - The arrow shows the next step. The velocity vector drawn on top of the ball is
−α · v_{t+1}— the signed displacement the optimizer is about to apply. Watch it grow as the ball accelerates downhill, flip sign when the ball climbs past the minimum, and shrink as the system damps toward zero. - Trajectory is precomputed, then scrubbed. The component simulates
stepsiterations up-front each timemomentumCoeff,learningRate,loss,initialX, orxRangechanges.currentStepis a cursor into the precomputed trajectory — autoplay and the slider both shift the cursor without rerunning the math. The same inputs always yield the same trajectory, so server and client renders agree. - Numerical gradient. The default loss
f(x) = x^2is differentiated via a central difference(f(x+h) − f(x−h)) / 2hwithh = 1e-3— accurate enough that the rendered trajectory is indistinguishable from the analytic one, and zero-config for arbitrary user-suppliedlossfunctions. - Reduced-motion fallback. With
prefers-reduced-motion: reducethe ball snaps directly to its new position and autoplay is suppressed.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
loss | (x: number) => number | x => x * x | The 1-D loss function the ball rolls on. |
momentumCoeff | number | — | Controlled β. Pair with onMomentumCoeffChange. |
defaultMomentumCoeff | number | 0.9 | Uncontrolled initial momentum coefficient. |
onMomentumCoeffChange | (coeff) => void | — | Fires when the slider changes the coefficient. |
learningRate | number | 0.1 | Fixed step size α. |
currentStep | number | — | Controlled trajectory cursor. Pair with onCurrentStepChange. |
defaultCurrentStep | number | 0 | Uncontrolled initial step. |
onCurrentStepChange | (step) => void | — | Fires on autoplay tick and manual scrub. |
playing | boolean | false | When true, advances currentStep every playSpeed ms. |
playSpeed | number | 120 | Milliseconds between autoplay ticks. |
initialX | number | -1.5 | Starting position of the ball. |
steps | number | 120 | Number of optimizer steps to precompute. |
xRange | [number, number] | [-2, 2] | Visible math-space x-range. |
transition | Transition | SPRINGS.smooth | Spring for the ball's position transition. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The figure is
role="figure"with a labelled title and anaria-live="polite"summary that names the currentβ, learning rate, step, positionx, velocityv, and lossL(x)— every value the bar visualises is also exposed textually. - Both sliders are native
<input type="range">instances wrapped inLabeledSlider, so keyboard arrows nudge the value andaria-valuetextis read aloud by screen readers. - Color is never the only signal — the trail, ball, and arrow all share the same accent hue but the textual readouts below the curve repeat the same information.
prefers-reduced-motion: reducesnaps the ball directly to its target position and suppresses autoplay; the velocity arrow updates in lockstep.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/viz/MomentumViz.tsx). Stripped the multi-optimizer (SGD / Momentum / Adam) ravine racetrack, the predict-quiz mode, the 2-D contour plot, theusePredictRoundscurriculum integration, and the lesson-specific narration. Generalised to the textbook heavy-ball story on a 1-D curve: configurableloss, controlled / uncontrolledmomentumCoeffandcurrentSteppairs, autoplay, and a velocity arrow rendered as the next-step displacement.