This page gets you from a fresh clone to a fitted map or a simulated signal. If you just want the shape of the project first, read index or architecture instead.
Build¶
cargo build --workspaceThis builds all four crates. To get an optimized qmrust binary:
cargo build --release # binary at target/release/qmrust
cargo install --path crates/qmrust-cli # optional: onto your PATHRun a fit¶
Every fit needs a config (protocol + fitting parameters) and some data.
Example configs live in recipes/, grouped by input type. The
non-BIDS configs (recipes/non-bids/) are fully explicit YAML that list the
whole acquisition protocol, so a run is self-documenting. BIDS configs
(recipes/bids/) leave the protocol out; those values come from the dataset’s
JSON sidecars.
cargo run -p qmrust-cli -- fit \
--mat-dir <dir-with-MTdata/R1map/B1map/B0map/Mask.mat> \
--config recipes/non-bids/qmt_config_sledpikerp.yaml \
--output-dir <out>Or fit inversion-recovery T1 from a single 4D NIfTI:
cargo run -p qmrust-cli -- fit \
--data ir_data.nii.gz \
--config recipes/non-bids/irt1_config.yaml \
--output-dir <out>Print the fully-resolved config (defaults applied, validated) before running:
cargo run -p qmrust-cli -- dump-config --config recipes/non-bids/qmt_config_sledpikerp.yamlFit a BIDS dataset¶
If your data is already laid out as a BIDS dataset, point at the dataset root instead of individual files:
cargo run -p qmrust-cli -- fit \
--bids-dir <path-to-bids-dataset> \
--config recipes/bids/irt1_config.yaml \
--output-dir <out>This scans the dataset, groups files into collections per the config’s model
(e.g. an inversion-time series for IRT1, or an Angle/Offset series for
QMTSPGR), and fits each subject (and session, if present), writing
<out>/qmrust/<subject>[/<session>]/anat/<subject>[_<session>]_<suffix>.nii.gz.
Each model composes its
acquisition protocol from the sidecars and resolves any auxiliary maps it
declares (B1/B0/R1) from the dataset by suffix — so aux-requiring models like
qMT fit through --bids-dir too. Named collections (fixed role slots, e.g.
MTR’s mt-on/mt-off) fit as well: their role-labeled volumes are mapped onto the
model’s declared roles, so the grouping’s named_set role names must match the
model’s measurement() roles. See BIDS for how rust-bids resolves
the dataset layout.
Notice --config above points at recipes/bids/irt1_config.yaml, not the
non-BIDS one — a BIDS fit’s config doesn’t carry the inversion times: the
model declares which acquisition parameters it needs from the JSON sidecars,
so --config is just algorithm options (fit bounds, etc.) plus the mask:
block that disambiguates which mask to apply. See
From sidecar metadata to Protocol
for how that mapping works.
Fitted maps are written under --output-dir as a BIDS-derivatives tree:
<output-dir>/qmrust/<subject>[/<session>]/anat/<subject>[_<session>]_<Suffix>.nii.gz
(+ a JSON sidecar per map, and a derivatives dataset_description.json).
Only the maps a model declares via bids_outputs() are written (e.g. IRT1’s
T1 → T1map) — diagnostic outputs like res/idx are not. See
DATA-PIPELINE.md for the full output contract.
Create a BIDS example from qMRLab data¶
qmrust bidsify converts a qMRLab .mat dataset into a BIDS layout whose
voxel data is byte-identical to the source .mat (no rescale, no dtype
narrowing — every volume round-trips as f64):
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-irt1This writes ds-irt1/sub-01/anat/sub-01_inv-<i>_IRT1.nii.gz (+
{InversionTime} sidecars), dataset_description.json, participants.tsv,
and the mask under ds-irt1/derivatives/preprocessed/sub-01/anat/ sub-01_desc-brain_mask.nii.gz. Each --out is a complete, self-contained BIDS
dataset root — one per model, rather than many models sharing one tree.
bidsify also supports qmt_spgr, whose BIDS identity is the custom,
non-official suffix QMTSPGR (see BIDS). Point it at a directory
of qMRLab’s qMT .mat files instead of a single file, and give it its own root:
cargo run -p qmrust-cli -- bidsify \
--model qmt_spgr \
--mat-dir <dir-with-MTdata/R1map/B1map/B0map/Mask.mat> \
--config recipes/non-bids/qmt_config_ramani.yaml \
--subject 01 --out ds-qmtspgrThis writes ds-qmtspgr/sub-01/anat/sub-01_flip-<f>_mt-<m>_QMTSPGR.nii.gz for
each of the 10 MT-weighted volumes (2 flip angles × 5 offsets), each with a
sidecar carrying the acquisition metadata the fit reads back by identity:
{"Angle": 142.0, "Offset": 443.0, "RepetitionTime": 0.03, "MTPulseDuration": 0.008}and a root .bidsignore containing *QMTSPGR* (so general BIDS validators
skip the non-official suffix; qmrust’s own layout resolver discovers it
regardless — see BIDS). Any computed inputs present in --mat-dir
are written byte-identical to a preprocessed derivatives pipeline: B1/B0
field maps under ds-qmtspgr/derivatives/preprocessed/sub-01/fmap/
(_TB1map/_B0map), the R1 map and brain mask under that pipeline’s anat/
(_R1map/_desc-brain_mask).
A model’s auxiliary inputs are auto-discovered as <name>.mat under --mat-dir
(as above). A NIfTI source has no such convention, so pass them explicitly —
--aux <name>=<path>, repeatable, where <name> is one the model declares:
cargo run -p qmrust-cli -- bidsify \
--model mt_sat --nii-dir <dir-with-MTw/PDw/T1w.nii.gz> \
--aux B1map=<TB1map.nii.gz> \
--config recipes/non-bids/mt_sat_config.yaml \
--subject 01 --out ds-mtsFitting the resulting QMTSPGR collection the same way as IRT1:
cargo run -p qmrust-cli -- fit \
--bids-dir ds-qmtspgr \
--config recipes/bids/qmt_config_ramani.yaml \
--output-dir ds-qmtspgr/derivativeswrites the six qMT maps qmt_spgr declares via bids_outputs() —
sub-01_Fmap.nii.gz, _kRmap, _R1Fmap, _R1Rmap, _T2Fmap, _T2Rmap —
under ds-qmtspgr/derivatives/qmrust/sub-01/anat/.
scripts/make_bids_examples.sh automates this for every model: it fetches
qMRLab’s OSF demo datasets and builds one self-contained single-subject BIDS
dataset per model — ds-irt1, ds-mese, ds-mtr, ds-mts, ds-qmtspgr,
named ds-<lowercased BIDS suffix> — fits each via qmrust fit --bids-dir, and
asserts each model’s declared maps were produced. Because each root carries its
own raw acquisitions and its own derivatives/preprocessed inputs, any one of
them is a single self-sufficient unit: --zip writes ds-<slug>.zip per root,
excluding the derivatives/qmrust reference outputs (those check a fit; they
aren’t an input to one). The datasets themselves are not committed (large data
stays out of the repo); re-run the script to regenerate them.
Voxelwise agreement with qMRLab’s own FitResults is checked separately by
ci/integration_osf.sh.
Run a simulation¶
qmrust sim generates a forward signal from ground-truth parameters,
optionally adds noise, and fits it back — useful for sanity-checking a
protocol without any real data.
cargo run -p qmrust-cli -- sim single-voxel \
--config recipes/sim/qmt_sim_ramani.yaml \
--output sv.jsonOther sim subcommands: signal (forward only), sensitivity (parameter
sweep), montecarlo (fit over parameter distributions). See the top-level
README.md for the full flag reference and per-model input
requirements.
Verify before you push¶
cargo test --workspace
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo build -p qmrust-core --target wasm32-unknown-unknown # core must stay wasm-cleanUnits¶
qmrust works in BIDS/SI units end to end: protocol times (inversion_times,
repetition_time in config; InversionTime, RepetitionTime in sidecars) are in
seconds, and fitted time-constant maps (e.g. IRT1’s T1map) are in seconds too —
so a fitted T1 of 0.9 means 900 ms, not 0.9 ms. This is a deliberate divergence from
qMRLab (which uses milliseconds): a qMRLab FitResults/T1.nii.gz reference differs from
qmrust’s T1map by a factor of 1000. See the “Units — BIDS-native (SI)” principle in
CLAUDE.md for the full rule.
Next steps¶
Adding your own model? See Adding a model.
Working from a BIDS dataset instead of individual files? See BIDS.