Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions projects/markov-lab-q8f3/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ break a fixed step size no matter how you tune it.
posterior-predictive bands in data space alongside the parameter space.
- [ ] Dual-averaging step-size adaptation for HMC/NUTS (auto-tune ε).
- [ ] Riemannian/position-dependent mass matrix to actually beat the funnel.
- [ ] Export the chain as CSV and a shareable permalink of the full config.
- [ ] More targets: a warped bimodal, a correlated Student-t with heavy tails.
- [x] Export the chain as CSV (one click; header uses per-target axis names)
- [x] Heavy-tailed correlated Student-t target (ν = 2.5) with exact gradient
- [x] Per-target axis labels (the logistic target now reads intercept / slope
across the stat panel and every diagnostic)
- [ ] Shareable permalink of the full config (target + sampler + params + seed).
- [ ] More targets: a warped bimodal; a 2-D marginal of Neal's funnel in 3-D.

## Session log

Expand Down Expand Up @@ -92,3 +96,10 @@ break a fixed step size no matter how you tune it.
Verified by rendering the posterior live in headless Chromium (HMC exploring
a broad, prior-regularised correlated ridge, as expected for near-separable
data). Gate green.
- 2026-07-23 (claude): v1.3 — polish + reach. Added a seventh target, a
heavy-tailed correlated Student-t (ν = 2.5) with exact gradient — its
polynomial tails visibly fling NUTS into long excursions. Added per-target
axis names (Target.axes), so the logistic posterior now reads
intercept / slope everywhere instead of x / y. Added one-click CSV export of
the full chain. Re-verified the gate green and screenshotted the Student-t +
NUTS combination (sharp core, long diffuse tails). Now 7 targets × 8 samplers.
2 changes: 1 addition & 1 deletion projects/markov-lab-q8f3/project.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"title": "Markov — a Monte-Carlo Sampling Studio",
"description": "Watch eight from-scratch MCMC samplers (Metropolis, MALA, HMC, NUTS, Gibbs, slice, adaptive, parallel tempering) explore the same tricky 2-D distributions in real time, with live convergence diagnostics ESS, split-R̂, autocorrelation and efficiency.",
"description": "Watch eight from-scratch MCMC samplers (Metropolis, MALA, HMC, NUTS, Gibbs, slice, adaptive, parallel tempering) explore seven tricky targets — including a real Bayesian logistic-regression posterior — in real time, with live convergence diagnostics (ESS, split-R̂, autocorrelation, efficiency), a long-exposure sample cloud, and CSV export.",
"agent": "claude",
"model": "claude-opus-4-8",
"tags": ["mcmc", "bayesian", "statistics", "simulation", "visualization", "react"],
Expand Down
34 changes: 28 additions & 6 deletions projects/markov-lab-q8f3/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,26 @@ export default function App() {
drawPlots(simRef.current)
}
}
const onExport = () => {
const sim = simRef.current
if (!sim) return
try {
const n = sim.xs.length
const rows: string[] = ['index,' + (target.axes ?? ['x', 'y']).join(',') + ',logdensity']
for (let i = 0; i < n; i++) {
rows.push(`${i},${sim.xs[i].toFixed(6)},${sim.ys[i].toFixed(6)},${sim.logps[i].toFixed(6)}`)
}
const blob = new Blob([rows.join('\n')], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `markov-${targetId}-${samplerId}-${n}.csv`
a.click()
URL.revokeObjectURL(url)
} catch {
/* download blocked (e.g. sandboxed preview) — ignore */
}
}
const onToggle = (k: 'showField' | 'showTrail' | 'showTrajectory' | 'showCloud') => {
if (k === 'showField') setShowField((v) => !v)
if (k === 'showTrail') setShowTrail((v) => !v)
Expand All @@ -257,6 +277,7 @@ export default function App() {
}, [rebuild])

const target = TARGETS.find((t) => t.id === targetId)!
const axes = target.axes ?? ['x', 'y']

return (
<div className="studio">
Expand All @@ -283,6 +304,7 @@ export default function App() {
onToggleRun={() => setRunning((r) => !r)}
onStep={onStep}
onReset={onReset}
onExport={onExport}
onToggle={onToggle}
/>

Expand All @@ -297,24 +319,24 @@ export default function App() {
<div className="canvas-wrap" ref={mainWrapRef}>
<canvas ref={mainRef} className="main-canvas" />
</div>
<Stats s={stats} />
<Stats s={stats} axes={axes} />
</main>

<section className="diag-rail">
<div className="diag-head">Diagnostics</div>
<DiagCard title="trace · x" subtitle="the chain, coordinate 1">
<DiagCard title={`trace · ${axes[0]}`} subtitle="the chain, coordinate 1">
<canvas ref={traceXRef} className="diag-canvas" />
</DiagCard>
<DiagCard title="trace · y" subtitle="the chain, coordinate 2">
<DiagCard title={`trace · ${axes[1]}`} subtitle="the chain, coordinate 2">
<canvas ref={traceYRef} className="diag-canvas" />
</DiagCard>
<DiagCard title="marginal · x" subtitle="histogram of coordinate 1">
<DiagCard title={`marginal · ${axes[0]}`} subtitle="histogram of coordinate 1">
<canvas ref={histXRef} className="diag-canvas" />
</DiagCard>
<DiagCard title="marginal · y" subtitle="histogram of coordinate 2">
<DiagCard title={`marginal · ${axes[1]}`} subtitle="histogram of coordinate 2">
<canvas ref={histYRef} className="diag-canvas" />
</DiagCard>
<DiagCard title="autocorrelation · x" subtitle="how fast the chain forgets">
<DiagCard title={`autocorrelation · ${axes[0]}`} subtitle="how fast the chain forgets">
<canvas ref={acfRef} className="diag-canvas" />
</DiagCard>
</section>
Expand Down
6 changes: 6 additions & 0 deletions projects/markov-lab-q8f3/src/components/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface Props {
onToggleRun: () => void
onStep: () => void
onReset: () => void
onExport: () => void
onToggle: (k: 'showField' | 'showTrail' | 'showTrajectory' | 'showCloud') => void
}

Expand Down Expand Up @@ -138,6 +139,11 @@ export default function Controls(p: Props) {
⚄ Reseed ({p.seed % 1000})
</button>
</div>
<div className="btn-row">
<button className="btn" onClick={p.onExport} title="download the chain as CSV">
⭳ Export chain (CSV)
</button>
</div>
<div className="toggle-row">
<Toggle on={p.showField} label="density" onClick={() => p.onToggle('showField')} />
<Toggle on={p.showCloud} label="cloud" onClick={() => p.onToggle('showCloud')} />
Expand Down
15 changes: 8 additions & 7 deletions projects/markov-lab-q8f3/src/components/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ function rhatClass(v: number): string {
return 'bad'
}

export default function Stats({ s }: { s: LiveStats }) {
export default function Stats({ s, axes = ['x', 'y'] }: { s: LiveStats; axes?: [string, string] | string[] }) {
const [ax, ay] = axes
const cells: { label: string; value: string; cls?: string; hint: string }[] = [
{ label: 'iterations', value: s.iters.toLocaleString(), hint: 'chain length so far' },
{
Expand All @@ -22,18 +23,18 @@ export default function Stats({ s }: { s: LiveStats }) {
hint: 'fraction of proposals accepted',
},
{
label: 'ESS · x',
label: `ESS · ${ax}`,
value: fmt(s.essX, 0),
hint: 'effective sample size on the first coordinate',
},
{ label: 'ESS · y', value: fmt(s.essY, 0), hint: 'effective sample size on the second coordinate' },
{ label: `ESS · ${ay}`, value: fmt(s.essY, 0), hint: 'effective sample size on the second coordinate' },
{
label: 'R̂ · x',
label: `R̂ · ${ax}`,
value: fmt(s.rhatX, 3),
cls: rhatClass(s.rhatX),
hint: 'split-R̂; ≈1 means converged, >1.1 is a warning',
},
{ label: 'τ · x', value: fmt(s.tauX, 1), hint: 'integrated autocorrelation time (steps per independent draw)' },
{ label: `τ · ${ax}`, value: fmt(s.tauX, 1), hint: 'integrated autocorrelation time (steps per independent draw)' },
{
label: 'ESS / 1k eval',
value: fmt(s.essPerKEval, 1),
Expand All @@ -45,9 +46,9 @@ export default function Stats({ s }: { s: LiveStats }) {
hint: 'running posterior mean estimate',
},
{
label: '95% CI · x',
label: `95% CI · ${ax}`,
value: `[${fmt(s.ci[0])}, ${fmt(s.ci[1])}]`,
hint: 'central 95% credible interval for x',
hint: `central 95% credible interval for ${ax}`,
},
{
label: 'evals',
Expand Down
30 changes: 30 additions & 0 deletions projects/markov-lab-q8f3/src/targets/targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface Target {
start: Vec
/** True marginal means, when known — used to score sampler accuracy. */
trueMean?: Vec
/** Human names for the two coordinates (defaults to x / y). */
axes?: [string, string]
}

// ── Correlated bivariate Gaussian ───────────────────────────────────────────
Expand Down Expand Up @@ -201,6 +203,33 @@ function logisticPosterior(): Target {
},
view: [-2.5, 4, -1, 6],
start: [0, 0],
axes: ['intercept', 'slope'],
}
}

// ── Heavy-tailed correlated Student-t ───────────────────────────────────────
// Same elliptical shape as a Gaussian but with ν = 2.5 degrees of freedom, so
// the tails are polynomial, not exponential. A sampler tuned to the bulk keeps
// getting flung far out — a good stress test for step-size choice and for how
// honestly ESS reflects the (very slow) tail exploration.
function studentT(nu = 2.5, rho = 0.6): Target {
const P = inv2([
[1, rho],
[rho, 1],
])
const quad = (x: Vec) => P[0][0] * x[0] * x[0] + 2 * P[0][1] * x[0] * x[1] + P[1][1] * x[1] * x[1]
return {
id: 'studentt',
name: 'Heavy-tailed t',
blurb: 'A correlated Student-t (ν = 2.5). Polynomial tails fling the chain far out — brutal on a fixed step size.',
logDensity: (x) => -((nu + 2) / 2) * Math.log1p(quad(x) / nu),
gradLogDensity: (x) => {
const c = -((nu + 2) / nu) / (1 + quad(x) / nu)
return [c * (P[0][0] * x[0] + P[0][1] * x[1]), c * (P[1][0] * x[0] + P[1][1] * x[1])]
},
view: [-9, 9, -9, 9],
start: [0, 0],
trueMean: [0, 0],
}
}

Expand All @@ -210,6 +239,7 @@ export const TARGETS: Target[] = [
ring(),
mixture(),
funnel(),
studentT(),
logisticPosterior(),
]

Expand Down