Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7e1b530
docs: fix R1map suffix, engine::run arg order, wasm API signatures, m…
Jul 21, 2026
a53c28b
rust-bids: accept short or full BIDS entity keys in grouping config
Jul 21, 2026
73a515b
Remove unused qMT timing/pulse config fields; gate sf test-only helpers
Jul 21, 2026
eac37a9
Drop history/process narrative from comments; annotate IR config BIDS…
Jul 21, 2026
68b8d71
rust-bids: numeric-aware entity matching (zero-padding-insensitive)
Jul 21, 2026
d53d177
rust-bids: load default grouping from bundled YAML via include_str!
Jul 21, 2026
db50de8
cli: fit --grouping to override the default BIDS grouping manifest
Jul 21, 2026
6c10637
test(cli): make --grouping test discriminate custom vs default resolu…
Jul 21, 2026
d95a335
core: split model describe (structural) from build (fit-ready, valida…
Jul 21, 2026
15926ab
cli: read BIDS model schema via describe, not an empty-protocol build
Jul 21, 2026
0239a22
Reorganize prots/ into recipes/{bids,non-bids,sim}; update refs and CI
Jul 21, 2026
56ac30b
fix(cli): dump-config tolerates BIDS configs without protocol; docs g…
Jul 21, 2026
0400c3f
core: shared ModelConfig build pipeline; qMT composes protocol from B…
Jul 22, 2026
3ed7845
recipes+docs: bids qMT recipes; document shared build pipeline; fix s…
Jul 22, 2026
92b5cb2
core+cli: dump-config as a generic registry capability (no match model)
Jul 22, 2026
c36a78a
core: BidsVolume write-seam (n_volumes/bids_volume) on Model, impl pe…
Jul 22, 2026
a20fed4
cli+core: generic bidsify + source-agnostic .mat; drop per-model bids…
Jul 22, 2026
8fe3ff1
docs: sync DATA-PIPELINE/ARCHITECTURE/ADDING-A-MODEL/CLAUDE to delete…
Jul 22, 2026
eb1d999
docs: drop residual 'v1 scope' temporal phrasing in ARCHITECTURE
Jul 22, 2026
0b1cc7a
Update readme
Jul 22, 2026
2e40293
Fix named-set desc filtering + entity_value_cmp total order; matfile …
Jul 22, 2026
3d890cd
docs+recipes: ModelConfig supertraits, BIDS output path, sidecar-sour…
Jul 22, 2026
3ac40dc
bidsify: locate measurement .mat from --mat-dir generically; add mask…
Jul 22, 2026
e8047db
Sanitize comments+docs: drop temporal/process narrative, fix stale Mo…
Jul 22, 2026
fc7363f
Update claude
Jul 22, 2026
cdc15a2
docs: replace behaviour-preserving TODO with the real verification pr…
Jul 22, 2026
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: 8 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Convert non-BIDS sources (e.g. a qMRLab `.mat` in ms) to BIDS units **at the she
1. New dir `crates/qmrust-core/src/models/<name>/`: `config.rs` (a `serde` struct +
`validate()`), the pure math, and `model.rs` (`impl Model` + `pub fn build`).
2. Register it in `models/mod.rs`.
3. Add **one** `ModelEntry` to `registry::all()` in `registry.rs` (name + BIDS suffix + `build`).
3. Add **one** `ModelEntry` to `registry::all()` in `registry.rs` (name + BIDS suffix + `build` + `describe` + `dump`).
4. Add tests (forward→fit round-trip; config parse/validate).

That's it — do **not** add `match cfg.model` branches in the CLI, engine, sim, or config.
Expand Down Expand Up @@ -143,10 +143,11 @@ cargo build --workspace
cargo test --workspace
cargo fmt --all --check # CI format gate
cargo clippy --workspace --all-targets -- -D warnings # CI lint gate (must be clean)
cargo run -p qmrust-cli -- fit --mat-dir <dir> --config prots/<cfg>.yaml --output-dir <out>
cargo run -p qmrust-cli -- fit --bids-dir <dir> --config prots/<cfg>.yaml --output-dir <out> # v1: no-aux, sequential (e.g. IRT1); writes derivatives/qmrust/...
cargo run -p qmrust-cli -- bidsify --model inversion_recovery --mat-data <IRData.mat> --mask <Mask.mat> --config prots/irt1_config.yaml --subject 01 --out <ds-root>
cargo run -p qmrust-cli -- sim single-voxel --config prots/<cfg>.yaml --output <out>.json
cargo run -p qmrust-cli -- fit --mat-dir <dir> --config recipes/non-bids/<cfg>.yaml --output-dir <out>
cargo run -p qmrust-cli -- fit --bids-dir <dir> --config recipes/bids/irt1_config.yaml --output-dir <out> # protocol from sidecars, aux by suffix; writes derivatives/qmrust/...
cargo run -p qmrust-cli -- fit --bids-dir <dir> --config recipes/bids/irt1_config.yaml --grouping <file> --output-dir <out> # override the built-in grouping manifest
cargo run -p qmrust-cli -- bidsify --model inversion_recovery --mat-data <IRData.mat> --mask <Mask.mat> --config recipes/non-bids/irt1_config.yaml --subject 01 --out <ds-root>
cargo run -p qmrust-cli -- sim single-voxel --config recipes/sim/qmt_sim_ramani.yaml --output <out>.json
cargo build -p qmrust-core --target wasm32-unknown-unknown # core must stay wasm-clean
cargo build -p rust-bids --target wasm32-unknown-unknown # rust-bids must stay wasm-clean too
cargo test -p qmrust-wasm --target wasm32-unknown-unknown --no-run # wasm bindings + browser tests must compile (CI runs them in a headless browser)
Expand All @@ -159,5 +160,5 @@ Before claiming work is done: `cargo test --workspace`, `cargo fmt --all --check

- Large test data is **not** committed; CI fetches qMRLab's datasets from OSF
(`ci/integration_osf.sh`). Locally you supply your own `--mat-dir`/`--mat-data`.
- Config files live in `prots/`; the browser build's API + build recipe are documented in
`crates/qmrust-wasm/README.md`.
- Config files live in `recipes/{bids,non-bids,sim}/` (see `recipes/README.md`); the
browser build's API + build recipe are documented in `crates/qmrust-wasm/README.md`.
139 changes: 40 additions & 99 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,124 +1,65 @@
# qmrust

Native-Rust quantitative MRI fitting — a fast port of selected [qMRLab](https://qmrlab.org) models.
It is qMRLab (https://qmrlab.org) written in Rust, so it is fast, and the fitting code is portable: the same routines run on the command line and, compiled to WebAssembly, inside a web browser with no server.

**Models available**
qmrust is a growing library of models, each one self-contained so a new model
can be added without disturbing the others. Today it fits:

| Config (`prots/`) | Model | Fits |
|---|---|---|
| `irt1_config.yaml` | Inversion Recovery T1 (Barral RD-NLS) | T1 mapping |
| `qmt_config_ramani.yaml` | qMT-SPGR · Ramani | F, kr, R1f, R1r, T2f, T2r |
| `qmt_config_sledpikerp.yaml` | qMT-SPGR · SledPikeRP | F, kr, R1f, R1r, T2f, T2r |
- Inversion recovery T1
- Quantitative magnetization transfer (qMT-SPGR), with the Ramani and
Sled-Pike sub-models

## Build
More are on the way, and the project is built to make adding them simple.

```bash
cargo build --release # binary at target/release/qmrust
```
Optional — install onto your PATH so you can call `qmrust` anywhere:
```bash
cargo install --path . # → ~/.cargo/bin/qmrust
```
Examples below use `./target/release/qmrust`; swap in `qmrust` if installed.

## Usage per config
## Getting started

### qMT-SPGR — SledPikeRP
Fits from `MTdata` + `R1map` + `B1map` + `B0map` + `Mask`. The `--mat-dir` mode
auto-loads all five `.mat` files from a folder by name.
Check out docs: qmrlab.org/qmrust

```bash
./target/release/qmrust fit \
--mat-dir ~/Desktop/qmrust_test/qmt_spgr \
--config prots/qmt_config_sledpikerp.yaml \
--output-dir ~/Desktop/qmrust_test/qmt_spgr/FitResults_rust
```
Build the tool:

### qMT-SPGR — Ramani
Same inputs; just swap the config:
```bash
./target/release/qmrust fit \
--mat-dir ~/Desktop/qmrust_test/qmt_spgr \
--config prots/qmt_config_ramani.yaml \
--output-dir ./FitResults_ramani
cargo build --release
```

Instead of `--mat-dir`, you can pass maps individually (`.mat` or NIfTI):
```bash
./target/release/qmrust fit \
--mat-data MTdata.mat --mask Mask.mat \
--r1map R1map.mat --b1map B1map.mat --b0map B0map.mat \
--config prots/qmt_config_sledpikerp.yaml --output-dir ./out
```
The binary lands at `target/release/qmrust`. Run `cargo install --path .` to
call `qmrust` from anywhere.

**qMT outputs** (8 maps): `F`, `kr`, `R1f`, `R1r`, `T2f`, `T2r`, `kf`, `resnorm`.
Fit a dataset:

### Inversion Recovery T1
Needs IR data: a 4D NIfTI, or a `.mat` containing `IRdata` (+ optional `TI`, `Mask`).
```bash
./target/release/qmrust fit \
--data ir_data.nii.gz \
--config prots/irt1_config.yaml \
--output-dir ./FitResults_t1
qmrust fit --bids-dir path/to/dataset \
--config recipes/bids/irt1_config.yaml \
--output-dir results
```
**T1 outputs**: `T1`, `b`, `a`, `res` (+ `idx` for the magnitude method).

## Configs
qmrust reads BIDS datasets directly, taking each scan's acquisition details
from its sidecar. When your data is not in BIDS, point it at plain NIfTI or
qMRLab `.mat` files instead and the acquisition details come from the config.
The `recipes/` folder holds ready-to-edit example configs, one per model,
grouped by how you feed in the data. Run `qmrust fit --help` for the full list
of inputs.

The files in `prots/` are **fully explicit** — every protocol, timing, pulse,
and fitting parameter is listed, so a run is self-documenting. Edit them to
match your acquisition (angles/offsets, timing table, bounds, etc.).
## Simulating

You can also generate a signal from known parameters, add noise, and fit it
back. This helps you check a model or see how reliably a parameter can be
recovered.

Print the fully-resolved config a run will use (defaults applied, validated):
```bash
./target/release/qmrust dump-config --config prots/qmt_config_sledpikerp.yaml
qmrust sim single-voxel --config recipes/sim/qmt_sim_ramani.yaml --output result.json
```

## Simulation
## Learning more

`qmrust sim` mirrors qMRLab's `Sim_*` tools: generate signal from ground-truth
parameters, optionally add noise, and fit it back. Parameters, noise, and sweep
ranges live in a `sim:` block in the same YAML.
- `recipes/README.md` walks through the example configs and how to run BIDS and
non-BIDS data.
- `docs/` covers the available models, the data pipeline, and how to add a
model of your own.

```bash
# forward signal only
qmrust sim signal --config prots/qmt_sim_ramani.yaml --output sig.json
# one voxel, N noisy trials, fit back (+ optional SVG)
qmrust sim single-voxel --config prots/qmt_sim_ramani.yaml --output sv.json --plot sv.svg
# sweep one parameter, report bias/std
qmrust sim sensitivity --config prots/qmt_sim_ramani.yaml --output sens.json --plot sens.svg
# Monte-Carlo over parameter distributions
qmrust sim montecarlo --config prots/qmt_sim_ramani.yaml --output mc.json
```
## Background

`sim:` block fields: `params` (ground truth), `b1`/`b0`/`r1`, `noise`
(`type: none|gaussian|rician`, `snr`), `seed`, `trials`, `sweep`
(sensitivity), `distributions` (montecarlo). Noise is `sigma = max(|signal|)/SNR`;
runs are reproducible for a fixed `seed`.

## Common options

| Flag | Meaning |
|---|---|
| `--config <file>` | protocol/fitting config (required) |
| `--mat-dir <dir>` | auto-load `MTdata/R1map/B1map/B0map/Mask.mat` |
| `--data` / `--mat-data` | single 4D NIfTI / `.mat` input |
| `--mask`, `--r1map`, `--b1map`, `--b0map` | individual maps (`.mat` or NIfTI) |
| `--output-dir <dir>` | where maps are written (default `./FitResults`) |
| `--threads <n>` | worker threads (default: all cores) |

Fitting shows a live progress bar (elapsed, throughput, ETA); it auto-hides when
output is redirected to a file.

## Notes on comparing against qMRLab

- For `.mat` inputs, output maps are written with a `make_nii`-compatible header
(2D, sform origin at voxel (1,1,1)) so they **overlay and subtract voxel-exactly**
against qMRLab's `FitResults`.
- Match the sub-model: comparing the wrong one dominates the differences.
- For qMT, `kf`/`T2f`/`T2r` agree tightly (~1–4%); `F`/`kr` are individually
ill-conditioned and best compared on averages/trends, not per voxel.
- Validate the Sf table (SledPikeRP) against qMRLab's:
```bash
./target/release/qmrust dump-sf --config prots/qmt_config_sledpikerp.yaml --output sf.bin
```
qmrust grows out of [qMRLab](https://qmrlab.org), the MATLAB toolbox for
quantitative MRI. It reimplements selected models natively so they run quickly
and in more places, and its results are made to line up with qMRLab's for
validation. Values follow BIDS conventions in SI units, so a T1 map is in
seconds rather than qMRLab's milliseconds.
6 changes: 3 additions & 3 deletions ci/integration_osf.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ IR_MASK="$(find "$DATA/ir" -name 'Mask.mat' | head -1)"

echo "Running IR fit..."
"$BIN" fit --mat-data "$IR_MAT" --mask "$IR_MASK" \
--config prots/irt1_config.yaml --output-dir "$DATA/out_ir"
--config recipes/non-bids/irt1_config.yaml --output-dir "$DATA/out_ir"

echo "Running qMT Ramani fit..."
"$BIN" fit --mat-dir "$QMT_DIR" \
--config prots/qmt_config_ramani.yaml --output-dir "$DATA/out_ramani"
--config recipes/non-bids/qmt_config_ramani.yaml --output-dir "$DATA/out_ramani"

echo "Running qMT SledPikeRP fit..."
"$BIN" fit --mat-dir "$QMT_DIR" \
--config prots/qmt_config_sledpikerp.yaml --output-dir "$DATA/out_srp"
--config recipes/non-bids/qmt_config_sledpikerp.yaml --output-dir "$DATA/out_srp"

echo "Asserting outputs..."
for f in "$DATA/out_ir/T1.nii.gz" "$DATA/out_ramani/F.nii.gz" "$DATA/out_srp/F.nii.gz"; do
Expand Down
Loading
Loading