Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Adding a model

qmrust fits several qMRI models (inversion-recovery T1, qMT-SPGR, ...) and is designed so that adding another one touches exactly two places: a new module, and one line in a registry. Nothing else — not the CLI, not the config parser, not the fitting engine, not the simulator — needs to know a new model exists.

Why: two seams, not scattered branches

Checklist: add a model

  1. New directory crates/qmrust-core/src/models/<name>/: a config.rs (a serde struct implementing ModelConfig), the pure math, and a model.rs (impl Model + the one-line build/describe/dump entry points that delegate to the shared pipeline).

  2. Register the module in models/mod.rs.

  3. Add one ModelEntry to registry::all() in registry.rs (name + BIDS suffix + build + describe + dump).

  4. Fill in that entry’s doc: ModelDoc — title, category, a one-paragraph summary, the LaTeX equation, symbols matching param_names(), citation keys added to docs/references.bib, and the recipe paths. Then regenerate the site’s model pages:

    cargo build --release -p qmrust-cli
    ./target/release/qmrust catalog > catalog.json
    python3 scripts/gen_model_docs.py --catalog catalog.json

    The model’s page, its gallery card, and its sidebar entry are generated from that metadata — there is no page to write by hand. CI fails the build if the committed pages are stale, so run this before committing.

  5. Add tests: a forward → fit round-trip, and config parse/validate.

That’s it. Use models/inversion_recovery/ as the minimal reference model (its protocol_schema() maps InversionTime off the BIDS sidecar); models/qmt_spgr/ shows a nested-config model that also declares auxiliary inputs (B1/B0/R1 maps) and a Series measurement keyed by (Angle, Offset).

Inputs are BIDS

qmrust fits BIDS or BIDS-like layouts only — qmrust fit --bids-dir <dir>, or --mat-dir/--mat-data for qMRLab .mat data converted to BIDS via qmrust bidsify. The rust-bids crate parses the whole dataset — the raw tree and every derivatives/<pipeline>/ — into one flat table (parse_to_table), then groups matching files into Collections per a declarative grouping config (BidsConfig). Everything downstream (grouping, auxiliary-input resolution, mask resolution) is just a query over that table via table_filter(rows, &[(column, value)]) — nothing model- or dataset-specific. See docs/agents/DATA-PIPELINE.md for the full walkthrough.

Declaring your model’s BIDS contract

A model doesn’t read files or JSON; it declares what it needs, and the shell (rust-bids + the CLI) fulfills the declaration:

Customizing the layout for non-official BIDS

Most qMRI protocols (qMT-SPGR, MTsat, ...) aren’t in the official BIDS-MRI suffix list. Rather than hardcoding exceptions, non-official layout facts are declared in the grouping config (BidsConfig, rust-bids/src/config.rs):

A registered model’s own bids_suffix (e.g. QMTSPGR) is already known at compile time via qmrust_core::registry::all()Vocabulary::bids() builds that in with no config; Vocabulary::from_config(cfg) layers a dataset’s declared customs on top. An unrecognized suffix is still included in the table (never dropped), just warned about — permissive but loud.

Grouping itself is one of three declarative shapes under BidsConfig:

loop_over: [sub, ses, run, task]

custom_entities:
  - key: cest
    name: cestPool
custom_suffixes: [QMTSPGR]

IRT1:
  sequential_set:
    by: [inv]

QMTSPGR:
  sequential_set:
    by: [mt, flip]

MTS:
  named_set:
    PDw: { flip: "1", mt: "off" }
    MTw: { flip: "1", mt: "on" }
    T1w: { flip: "2", mt: "off" }
    required: [PDw, MTw, T1w]

See docs/agents/DATA-PIPELINE.md for the full Vocabulary/grouping mechanics.

Auxiliary maps and the mask

A fit resolves each of a model’s required_inputs() entries from the dataset’s flat table by the collection’s full identity (subject/session/run/ ...) plus the declared BIDS suffix (and entity, if any) — found in the raw tree or in any derivatives/<pipeline>/. A missing required: true input is a hard build-time error; a missing optional one just leaves the model to its own default.

The brain mask works the same way but is declared separately, in --config under a mask: block, because a dataset can carry more than one mask (brain, tissue, lesion, ...) and auto-picking one would be a silent guess:

mask:
  desc: brain

suffix defaults to mask; every other key is an entity constraint (short keys like desc are normalized to their full BIDS name). An under-specified mask: that still matches several files is a hard error, never a silent pick; an absent mask: block means no masking at all.

Units

qmrust works natively in BIDS/SI units, end to end: time (RepetitionTime, EchoTime, InversionTime, and any fitted time constant such as T1/T2) in seconds, frequency in Hz, field in tesla, angle in radians except BIDS-MRI’s FlipAngle, which is degrees. There is no internal ms↔s round-tripping — a non-BIDS source (a qMRLab .mat in milliseconds) is converted once, at the shell boundary (bidsify / .mat load), so qmrust-core only ever sees BIDS units. Fitted maps therefore differ from qMRLab’s own FitResults by the unit factor (qMRLab T1 in ms = qmrust T1 in s × 1000) — validation against qMRLab references must reconcile that factor, never expect raw equality. bids_outputs() records each map’s SI unit explicitly.

Going deeper

For the full trait definition, the supporting value types (Aux, InputSpec, FitStrategy, Protocol, MeasurementKind, BidsSpec), and a worked line-by-line example, see docs/agents/ARCHITECTURE.md. For the BIDS layout/sidecar/protocol machinery in depth, see docs/agents/DATA-PIPELINE.md.