CLAUDE.md#
Tips for future agents working in this repo.
What this is#
mri-scanner is a companion to 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·dtand 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
.wasmasset. 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
.tssources directly (Node ≥ 22.6 type stripping), which is why src imports use explicit.tsextensions (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.