# week2 checker — 2D Lennard-Jones MD energy conservation + Maxwell-Boltzmann

The physics gate: a correctly integrated **NVE** molecular-dynamics run conserves
total energy over 10^4 steps, and its velocities settle into a Maxwell-Boltzmann
distribution at the target temperature. A high-order but *non-symplectic*
integrator (classic RK4) looks accurate step-to-step yet dissipates total energy
secularly over the run — that is the check with teeth.

Reduced units: `eps = sigma = m = kB = 1`. Cutoff `rc = 2.5`, potential **shifted**
so `U(rc) = 0`:  `U(r) = 4(r^-12 - r^-6) - U_shift`, `U_shift = 4(2.5^-12 - 2.5^-6)`.
Minimum-image periodic boundaries with the box from `run.json`. System: `N = 100`
atoms, `rho = 0.8`, target `T = 0.5`, `dt = 0.01`.

The checker is **stdlib-only** (the gate CI job installs no numpy for the checker).

## Required raw artifacts (student side)

`artifacts/run.json` — a JSON object with keys:

| Key | Type / value |
|---|---|
| `n` | `100` |
| `rho` | `0.8` |
| `box` | `[Lx, Ly]` floats |
| `dt` | `0.01` |
| `temperature` | `0.5` |
| `eq_steps` | int `>= 2000` |
| `steps` | int `>= 10000` (production steps) |
| `sample_every` | int in `[1, 100]` |
| `seed` | int |
| `integrator` | `"velocity-verlet"` |

`artifacts/traj.jsonl` — one JSON object per **production** sample frame, keys:

| Key | Type |
|---|---|
| `step` | int (production step index at the sample) |
| `t` | float (`step * dt`) |
| `pos` | `[[x, y] * n]`, positions wrapped into the box |
| `vel` | `[[vx, vy] * n]` |
| `E_pot` | float (shifted-LJ potential energy) |
| `E_kin` | float (kinetic energy) |

The checker reads only the raw per-frame `pos`/`vel` and **recomputes** every LJ
energy, the secular drift, and the speed histogram itself. It checks that every
value is finite, every position is wrapped, and frames occur exactly at
`sample_every, 2*sample_every, ...` with `t = step*dt`. It uses the logged
`E_pot`/`E_kin` only to establish self-consistency of the sampled states, never as
the gate quantity.

## Checker-supplied input

None. The checker establishes an ordered, self-consistent sampled trajectory and
recomputes the claimed physics. Sparse snapshots alone cannot prove every
unrecorded integration step, so the fresh-clone `make reproduce`, source review,
and live crit remain part of the evidence contract.

## Checks and tolerances (all printed in output)

| Check | Quantity | Tolerance |
|---|---|---|
| schema | artifacts parse; all values are finite 2D data; positions wrapped into the box; `len(pos)==len(vel)==n`; `>= 100` frames | exact |
| config | declared and derived density, `T`, `dt`, equilibration, production integrator and duration | `n == 100`; `\|rho − 0.8\| <= 0.008`; declared `T == 0.5`; `dt == 0.01`; `eq_steps >= 2000`; `integrator == "velocity-verlet"`; `steps >= 10000`; `sample_every` in `[1,100]` |
| chronology | complete ordered production samples | exactly `floor(steps/sample_every)` frames at consecutive multiples of `sample_every`; `t == step*dt` ± 1e-9 |
| epot-consistency | recomputed `E_pot(pos)`/`E_kin(vel)` vs logged (25 frames) | rel err `< 1e-6` ← anti-fabrication teeth |
| **energy-secular-drift** | `\|mean(E[-k:]) − mean(E[:k])\| / \|E_0\|`, `k = max(1, nframes//10)`, recomputed for ALL frames | **`< 2e-3`** ← money check |
| mb-chi2 | speed histogram vs 2D Maxwell-Boltzmann (Rayleigh) at `T_meas`; and `T_meas` | `chi2/dof < 2.0` (24 equal-prob bins, dof = 22); `\|T_meas − 0.5\| < 0.05` ← equilibrium teeth |

`energy-secular-drift` is the headline gate: it isolates *secular* (monotone) drift
from bounded oscillation (the max oscillation is printed but not gated). A symplectic
integrator keeps energy bounded; a non-symplectic RK step dissipates it. `mb-chi2`
is the equilibrium teeth: a trajectory can conserve energy yet not be a thermal
equilibrium sample.

## Verdict conventions

`PASS: …` and exit 0 on success; `FAIL: <check>: <measured vs required>` and exit 1
on a failed physics/config check; exit 2 on a contract error (missing artifact or
bad schema). Every numeric check prints `measured = … (required …)`.

## Negative-control fixture

`fixtures/week2-broken` — same MD and seed, but the production run secretly uses
classic RK4 while declaring the required velocity-Verlet metadata. It passes
schema, config, chronology, and epot-consistency, then **FAILs on
`energy-secular-drift`**. Metadata cannot rescue wrong dynamics; a checker that
PASSes this fixture has no teeth.

## Calibration (measured over 8 seeds at this exact contract)

| Integrator | Secular drift | Max oscillation | chi2/dof (correct runs) |
|---|---|---|---|
| velocity-Verlet (reference) | 1.7e-5 … 5.5e-4 | up to 1.3e-3 | 0.58 … 1.34 |
| classic RK4 (broken) | 4.4e-3 … 6.3e-3 | — | — |

The `2e-3` threshold sits **> 3.6×** above the worst Verlet secular shift and
**> 2.2×** below the best RK4 shift — a clean separation. The shipped fixtures
(seed 42) measure: reference secular ~4.4e-5, chi2/dof ~1.03; broken secular ~5.9e-3.

## Fixtures

- `fixtures/week2-reference` — velocity-Verlet → **PASS** (secular ~4e-5, chi2/dof ~1.0)
- `fixtures/week2-broken` — classic RK4 → **FAIL** on `energy-secular-drift` (secular ~6e-3)
