# CLAUDE.md Tips for future agents working in this repo. ## What this is mri-scanner is a companion to [seqlab](https://github.com/concept-collection/seqlab). You upload a pulseq `.seq` file (e.g. one exported from seqlab), pick a digital phantom, and it runs a Bloch simulation **in the browser** (Web Worker) and shows the raw k-space (the acquired signal laid out one row per ADC readout). Reconstruction is intentionally deferred — this is the "raw data" stage. ## Architecture ``` src/seq/ .seq parser + reconstruction, COPIED from seqlab (parseSeq.ts, reconstruct.ts, types.ts, md5.ts). If you fix a parser bug, fix it in seqlab too. summary.ts wraps them for a display/validate summary. src/phantom/ phantomTypes.ts (Phantom = spin cloud + tissue props), loadPhantom.ts (read a KomaMRI .phantom HDF5 via h5wasm, in the browser), builtins.ts (the two bundled phantoms, imported with Vite ?url), data/{cube,sphere}.phantom (GENERATED — do not hand-edit). src/sim/ simulate.ts = the Bloch simulator (pure, no DOM — so it runs headlessly in Node too). simWorker.ts runs it off the main thread; useSimulation.ts is the React hook (progress streaming; cancel = terminate the worker). src/kspace/ kspace.ts (RawSignal -> RGBA image, DOM-free), KspaceView.tsx (canvas + mode selector). src/storage/ seqStore.ts — uploaded .seq files in IndexedDB. src/ui/ SequencePanel, PhantomPanel. src/App.tsx orchestration. scripts/gen-phantoms.mjs regenerate the two .phantom files (h5wasm/node). scripts/sim-test.mjs headless physics smoke test. test-data/ fid.seq, gre.seq goldens (from seqlab) for sim-test. ``` ## The simulator (src/sim/simulate.ts) Isochromat Bloch simulation in the rotating frame. The timeline is split at the union of gradient vertices, RF samples and ADC samples (`boundaries`); within a segment gradients are linear and RF is ~one raster. Two regimes: - **Free precession** (no RF): transverse magnetisation rotates about z by `dφ = 2π·(g·r)·dt + Δw·dt` and relaxes — exact for a linear gradient, so these segments can be long (a whole dwell/delay). This is what makes it fast. - **Excitation** (RF present): full 3-D Rodrigues rotation about `Ω = (2π·B1·cosθ, 2π·B1·sinθ, 2π·g·r + Δw)`. Signal at each ADC sample = `Σ_j ρ_j·(Mx+iMy)`, demodulated by the receiver phase (ADC phase + frequency offset). Units: pulseq gradients are **Hz/m**, RF amplitude **Hz**, positions **m**, so `g·r` and `B1` are already in Hz — no γ needed. Sign convention is internally consistent (excitation reduces to the free-precession z-rotation when B1=0) but not pinned to a physical handedness; that only matters once we add reconstruction. Relaxation has a uniform fast path (our built-in phantoms are single-tissue) and a per-spin fallback. Cost ≈ O(numSegments · numSpins). A 128×128 GRE on the 2197-spin cube is ~400k segments → ~20 s. FID/EPI are much cheaper. The sphere (~1000 spins) is ~2× faster than the cube. Progress is streamed; cancel terminates the worker. ## The .phantom format Genuine KomaMRI `.phantom` = HDF5 (see `../KomaMRI.jl` Phantom.jl): root attrs `Version`/`Name`/`Ns`/`Dims`; group `position` with `x`/`y`/`z` (metres); group `contrast` with `ρ`, `T1`, `T2`, `T2s` (s) and `Δw` (rad/s). Note the **Unicode** dataset names `ρ` and `Δw` — h5wasm reads/writes them fine. `loadPhantom.ts` maps aliases so it also opens arbitrary KomaMRI phantoms. The two built-ins are ~10 mm, 0.8 mm uniform spin spacing, single water-like tissue (T1 1000 ms, T2 100 ms, Δw 0). ## Gotchas - **h5wasm inlines its WASM** into the main JS bundle (~5 MB raw, ~1.1 MB gz). That's expected; there is no separate `.wasm` asset. It's used on the main thread (loadPhantom); the worker only needs parse+simulate, so the worker chunk stays tiny. - **The worker gets plain typed arrays**, not the h5wasm-decoded file — the Phantom is structured-cloned to the worker. Don't move HDF5 decoding into the worker. - **Node runs the `.ts` sources directly** (Node ≥ 22.6 type stripping), which is why src imports use explicit `.ts` extensions (inherited from seqlab). - **Regenerating phantoms**: `npm run gen-phantoms`. Change SIZE/SPACING there. Denser phantoms = slower sims; keep the balance. - **base: './'** in vite.config so it works from the Pages subpath. ## Testing - `npm run sim-test` — headless physics checks: FID |S(0)| ≈ Σρ and decays at exactly 1/T2; GRE echo peaks at the readout centre. Uses test-data goldens. - `npm run build` — tsc typecheck + vite build. - Browser verification (h5wasm load, worker run, canvas, drag & drop, IndexedDB persistence) is manual: `npm run dev`.