Skip to content

Port vfa_t1 from qMRLab - #13

Merged
agahkarakuzu merged 10 commits into
mainfrom
feat/vfa-t1
Aug 1, 2026
Merged

Port vfa_t1 from qMRLab#13
agahkarakuzu merged 10 commits into
mainfrom
feat/vfa-t1

Conversation

@agahkarakuzu

@agahkarakuzu agahkarakuzu commented Aug 1, 2026

Copy link
Copy Markdown
Member

Ports qMRLab's vfa_t1 (variable flip angle T1) into qmrust, plus the fixes the port surfaced.

The model

Fram et al. (1987) linearized SPGR fit. Dividing the steady-state signal by sin(α·B1) and tan(α·B1) linearizes it, so T1 and M0 come from an ordinary least-squares line — closed form, no iteration. A transmit map scales the nominal flip angles when present.

  • BIDS suffix VFA, per-volume flip entity, FlipAngle per volume and RepetitionTimeExcitation global.
  • Optional B1mapTB1map.
  • Outputs T1map (s) and M0map.
  • sequential_set: { by: [flip] } grouping; one registry line; recipes for both the BIDS and non-BIDS paths.

Validation against qMRLab

Fitted qMRLab's own OSF dataset (osf.io/7wcvh, FA = [3°, 20°], TR = 15 ms, with the shipped B1 map) and compared voxelwise to its FitResults:

vfa_t1-T1: 4668 voxels | within 1e-12: 100.000% | corr: 1.0000  OK
vfa_t1-M0: 4668 voxels | within 1e-12: 100.000% | corr: 1.0000  OK

Both maps agree to the float32 precision they are stored in. Wired into ci/integration_osf.sh alongside the existing model comparisons.

Fixes this surfaced

.bidsignore no longer hides a model's own volumes. is_custom_suffix returned true for every registered suffix, including canonical ones, so bidsify wrote *IRT1*/*MESE*/*MTR*/*MTS* lines into datasets it produced. Two separate questions were conflated:

question answer
write: does this dataset need a .bidsignore line? non-canonical suffixes only (QMTSPGR)
read: may a .bidsignore hide this file from qmrust? never, for any registered suffix

is_declared_suffix now answers the read question and is_custom_suffix the write one. bidsify also removes a stale line, so roots written before the fix self-heal. Verified against all six published archives, every one of which still carries a stale line:

inversion_recovery  ds-irt1.zip     bidsignore=*IRT1*      OK  9 volumes
mono_t2             ds-mese.zip     bidsignore=*MESE*      OK  30 volumes
mt_ratio            ds-mtr.zip      bidsignore=*MTR*       OK  2 volumes
mt_sat              ds-mts.zip      bidsignore=*MTS*       OK  3 volumes
qmt_spgr            ds-qmtspgr.zip  bidsignore=*QMTSPGR*   OK  10 volumes
vfa_t1              ds-vfa.zip      bidsignore=(none)      OK  2 volumes

Aux maps filed under fmap/ were invisible to the docs/playground generator, which globbed only anat/. qmt_spgr had been shipping its playground payload without B1map and B0map — the app fitted uncorrected while the CLI corrected, with no symptom to point at.

The playground landed on the file list after a successful dataset load instead of the viewer.

Dataset hosting

Adding ds-vfa.zip to Zenodo minted a new version, so docs/playground/data/sources.json moves to record 21696048. All six archives were re-verified at the new record: HTTP 200 with Access-Control-Allow-Origin: *. The concept id can't replace this — it 404s on the /api/ files route, so publishing a dataset will always mean touching this file.

Also

  • scripts/make_bids_examples.sh builds ds-vfa; ci/integration_osf.sh fetches, bidsifies, fits and compares it.
  • The porting skill gains an app-integration phase, the wasm-rebuild trap, and the Series volume-identity trap (see PR 2 for what that one cost).

Gates

cargo test --workspace, cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, both wasm32-unknown-unknown builds, node --test scripts/tests/*.test.mjs, python3 -m unittest discover -s scripts/tests.

Summary by CodeRabbit

  • New Features
    • Added variable flip-angle T1 mapping with linear and nonlinear fitting, optional B1 correction, and M0/T1 outputs.
    • Added native and BIDS workflows, recipes, playground support, and example dataset generation.
  • Bug Fixes
    • Improved BIDS ignore handling so declared outputs and sidecars remain discoverable.
    • Improved auxiliary-input discovery across supported data directories.
    • Improved playground loading behavior after BIDS processing.
  • Documentation
    • Added model documentation, gallery entries, figures, and supporting reference information.
  • Tests
    • Added fitting, protocol, integration, dataset, and regression coverage.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2631c943-fb70-4d2a-bded-517c203c042c

📥 Commits

Reviewing files that changed from the base of the PR and between d653dd1 and 4f67f4f.

📒 Files selected for processing (1)
  • .claude/skills/porting-qmrlab-models/SKILL.md

📝 Walkthrough

Walkthrough

This PR adds a variable flip angle T1 model with linear and nonlinear fitting, B1 correction, BIDS integration, OSF validation, example-dataset generation, playground data, and model documentation.

Changes

VFA T1 model and fitting

Layer / File(s) Summary
Configuration, fitting, and model adapter
crates/qmrust-core/src/models/..., crates/qmrust-core/src/registry.rs, recipes/{bids,non-bids}/vfa_t1_config.yaml
Adds VFA T1 configuration validation, Fram linear fitting, optional nonlinear fitting, B1-scaled modeling, protocol ingestion, output metadata, registry wiring, recipes, and tests.
BIDS suffix handling
crates/rust-bids/src/{default_grouping.yaml,table.rs,vocab.rs}, crates/qmrust-cli/src/bidsify.rs
Registers VFA, distinguishes declared and custom suffixes, preserves canonical suffix files, and synchronizes .bidsignore entries.
Dataset and integration validation
ci/integration_osf.sh, scripts/make_bids_examples.sh, scripts/docsfig/dataset.py, scripts/tests/test_dataset.py
Builds and fits the VFA example dataset with B1 and mask inputs, validates output maps against references, and discovers auxiliary files across datatype directories.
Documentation and playground integration
docs/index.md, docs/models/..., docs/playground/..., docs/figures/..., docs/references.bib, .claude/skills/...
Adds VFA documentation, figures, playground metadata and probes, loading behavior updates, the Fram reference, and app-integration workflow requirements.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Bidsify
  participant VfaT1Model
  participant VfaT1Fitter
  participant Playground
  User->>Bidsify: provide VFA volumes, mask, and B1 map
  Bidsify->>VfaT1Model: create BIDS dataset
  VfaT1Model->>VfaT1Fitter: assemble flip-angle samples
  VfaT1Fitter->>Playground: return M0 and T1 outputs
  Playground->>User: display maps and probe results
Loading

Possibly related PRs

  • qMRLab/qmrust#5: Provides the generic model, registry, and interface pipeline used by this model.
  • qMRLab/qmrust#7: Uses a similar model integration, documentation, recipe, and validation structure.
  • qMRLab/qmrust#9: Introduced related BIDS, playground, and example-dataset infrastructure.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: porting the vfa_t1 model from qMRLab.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vfa-t1

Comment @coderabbitai help to get the list of available commands.

@agahkarakuzu agahkarakuzu changed the title feat/vfa t1 Port vfa_t1 from qMRLab Aug 1, 2026
@agahkarakuzu
agahkarakuzu marked this pull request as ready for review August 1, 2026 04:13
@agahkarakuzu
agahkarakuzu marked this pull request as draft August 1, 2026 04:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
crates/qmrust-core/src/models/vfa_t1/fit.rs (2)

64-73: 🚀 Performance & Scalability | 🔵 Trivial

Consider a shared helper for the SPGR signal equation.

forward() and VfaProblem::residual_for() both compute m0 * a.sin() * (1.0 - e) / (1.0 - e * a.cos()) independently. Extracting a single function for the closed-form signal would remove the duplication and prevent the two from drifting if the equation changes.

Also applies to: 152-164

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/qmrust-core/src/models/vfa_t1/fit.rs` around lines 64 - 73, Extract
the duplicated SPGR signal equation from VfaT1::forward and
VfaProblem::residual_for into one shared helper, then update both callers to use
it with the same m0, angle, and relaxation-factor inputs. Preserve the existing
numerical behavior while ensuring future equation changes are made in only the
helper.

89-108: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Document intentional qMRLab parity for slope >= 1.0. Compute_M0_T1_OnSPGR uses the same guard and permits this case, which can produce a finite non-positive T1. Update the fit_linear comment to document this behavior instead of suggesting that every non-physical solve returns NaN.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/qmrust-core/src/models/vfa_t1/fit.rs` around lines 89 - 108, Update
the documentation comment for fit_linear to state that qMRLab parity permits
slope >= 1.0 after the existing guard, which may produce a finite non-positive
T1; do not change the implementation or imply that every non-physical solve
returns NaN.
crates/rust-bids/src/vocab.rs (1)

216-238: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider renaming custom_suffixes to reflect its actual contents.

is_custom_suffix and is_declared_suffix both read the same underlying custom_suffixes set, but now answer different questions ("is this non-canonical" vs. "is this declared at all"). The field holds canonical and non-canonical suffixes alike since bids() inserts every registered model's suffix into it. The name custom_suffixes no longer reflects that, which is why two long doc comments are needed to keep the two predicates straight.

Rename the field (e.g. to declared_suffixes) so its name matches is_declared_suffix's semantics directly, and is_custom_suffix reads as the derived, narrower predicate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rust-bids/src/vocab.rs` around lines 216 - 238, Rename the underlying
custom_suffixes field to declared_suffixes throughout its definition,
initialization, and all references, including bids(), is_custom_suffix(), and
is_declared_suffix(). Preserve the existing set contents and predicate behavior,
with is_custom_suffix remaining the canonical-excluding derived check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/porting-qmrlab-models/SKILL.md:
- Around line 69-81: Add the promised executable identity check in the
Series-model validation example: after constructing ids with
engine::build_volume_ids, compare each VolumeId::Params entry against the
corresponding m.forward(..).series()[i].params using assert_eq! or an equivalent
loop. Keep the comparison aligned by volume index so any drift between
protocol-derived identities and forward sample parameters fails immediately.

In `@crates/qmrust-core/src/models/vfa_t1/config.rs`:
- Line 54: Update the flip-angle validation in the configuration validation
method to reject non-finite values, including NaN, before sorting. Ensure
invalid angles produce the existing validation error path so sort_by’s
partial_cmp unwrap in the same method cannot receive NaN.
- Around line 47-67: Update validate_protocol to reject duplicate flip angles
after validating and sorting self.flip_angles, preserving the existing
acceptance of distinct angles and returning a clear validation error when any
adjacent sorted values are equal. Ensure this uniqueness check runs before
returning Ok so fit() never receives duplicate expected angles.

In `@scripts/docsfig/dataset.py`:
- Around line 141-143: Update the auxiliary matching logic around found and
aux[spec["name"]] so ambiguous matches cannot be silently accepted: require
exactly one matching derivative before assignment, and fail clearly when
multiple matches are found. Preserve the existing assignment for a unique match
and the current behavior when no match exists.

---

Nitpick comments:
In `@crates/qmrust-core/src/models/vfa_t1/fit.rs`:
- Around line 64-73: Extract the duplicated SPGR signal equation from
VfaT1::forward and VfaProblem::residual_for into one shared helper, then update
both callers to use it with the same m0, angle, and relaxation-factor inputs.
Preserve the existing numerical behavior while ensuring future equation changes
are made in only the helper.
- Around line 89-108: Update the documentation comment for fit_linear to state
that qMRLab parity permits slope >= 1.0 after the existing guard, which may
produce a finite non-positive T1; do not change the implementation or imply that
every non-physical solve returns NaN.

In `@crates/rust-bids/src/vocab.rs`:
- Around line 216-238: Rename the underlying custom_suffixes field to
declared_suffixes throughout its definition, initialization, and all references,
including bids(), is_custom_suffix(), and is_declared_suffix(). Preserve the
existing set contents and predicate behavior, with is_custom_suffix remaining
the canonical-excluding derived check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f7351284-4ce3-4548-88da-426fbc356d40

📥 Commits

Reviewing files that changed from the base of the PR and between d2b303b and fc15364.

⛔ Files ignored due to path filters (15)
  • docs/playground/data/inversion_recovery.nii.gz is excluded by !**/*.gz
  • docs/playground/data/inversion_recovery_mask.nii.gz is excluded by !**/*.gz
  • docs/playground/data/mono_t2.nii.gz is excluded by !**/*.gz
  • docs/playground/data/mono_t2_mask.nii.gz is excluded by !**/*.gz
  • docs/playground/data/mt_ratio.nii.gz is excluded by !**/*.gz
  • docs/playground/data/mt_ratio_mask.nii.gz is excluded by !**/*.gz
  • docs/playground/data/mt_sat.nii.gz is excluded by !**/*.gz
  • docs/playground/data/qmt_spgr.nii.gz is excluded by !**/*.gz
  • docs/playground/data/qmt_spgr_B0map.nii.gz is excluded by !**/*.gz
  • docs/playground/data/qmt_spgr_B1map.nii.gz is excluded by !**/*.gz
  • docs/playground/data/qmt_spgr_R1map.nii.gz is excluded by !**/*.gz
  • docs/playground/data/qmt_spgr_mask.nii.gz is excluded by !**/*.gz
  • docs/playground/data/vfa_t1.nii.gz is excluded by !**/*.gz
  • docs/playground/data/vfa_t1_B1map.nii.gz is excluded by !**/*.gz
  • docs/playground/data/vfa_t1_mask.nii.gz is excluded by !**/*.gz
📒 Files selected for processing (33)
  • .claude/skills/porting-qmrlab-models/SKILL.md
  • ci/integration_osf.sh
  • crates/qmrust-cli/src/bidsify.rs
  • crates/qmrust-core/src/models/mod.rs
  • crates/qmrust-core/src/models/vfa_t1/config.rs
  • crates/qmrust-core/src/models/vfa_t1/fit.rs
  • crates/qmrust-core/src/models/vfa_t1/mod.rs
  • crates/qmrust-core/src/models/vfa_t1/model.rs
  • crates/qmrust-core/src/registry.rs
  • crates/rust-bids/src/default_grouping.yaml
  • crates/rust-bids/src/table.rs
  • crates/rust-bids/src/vocab.rs
  • docs/figures/qmt_spgr/aux.webp
  • docs/figures/qmt_spgr/curve.webp
  • docs/figures/vfa_t1/aux.webp
  • docs/figures/vfa_t1/curve.webp
  • docs/figures/vfa_t1/inputs.webp
  • docs/figures/vfa_t1/outputs.webp
  • docs/index.md
  • docs/models/index.md
  • docs/models/relaxometry/vfa_t1.md
  • docs/playground/data/index.json
  • docs/playground/data/qmt_spgr.json
  • docs/playground/data/sources.json
  • docs/playground/data/vfa_t1.json
  • docs/playground/inputs.js
  • docs/playground/model.js
  • docs/references.bib
  • recipes/bids/vfa_t1_config.yaml
  • recipes/non-bids/vfa_t1_config.yaml
  • scripts/docsfig/dataset.py
  • scripts/make_bids_examples.sh
  • scripts/tests/test_dataset.py

Comment thread .claude/skills/porting-qmrlab-models/SKILL.md
Comment thread crates/qmrust-core/src/models/vfa_t1/config.rs
Comment thread crates/qmrust-core/src/models/vfa_t1/config.rs Outdated
Comment thread scripts/docsfig/dataset.py
@agahkarakuzu
agahkarakuzu marked this pull request as ready for review August 1, 2026 04:27
@agahkarakuzu
agahkarakuzu merged commit eeb522b into main Aug 1, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant