Studies and Observables#

The mrax.data_struct.study.Study class coordinates multiple experiments, models, and datasets. Observables are explicit: if a model needs a map from another fit (for example a calibration or baseline map), insert it into the target experiment before calling run_fit.

Dimension handling#

Experiments use named dimensions to describe how non-spatial axes map to observables. Spatial axes are always x/y/z. Non-spatial axes are inferred from observable lengths when importing data. Temporal repeats use t only when another observable axis explains part of the frame grid. If the observable layout cannot be inferred, importers fall back to a single frame axis.

You can inspect the current layout via experiment.describe_dims() and reshape arrays manually using mrax.data_struct.reshape_frames_to_dims() or mrax.data_struct.reshape_observables_to_dims(). To go back to frame-major layouts, use mrax.data_struct.reshape_dims_to_frames(). To choose an observable that matches the frame axis, use mrax.data_struct.pick_observable_for_frames().

ROI masks from external tools sometimes arrive transposed relative to MRax spatial ordering. Use mrax.analysis.match_spatial_mask() to validate or transpose masks before ROI extraction.

By default, any non-spatial dimension without a mapped observable is treated like an additional parameter axis. Fits will return fields with that dimension up front, for example t, x, y or frame, x, y. This enables per-repeat regularizers and parameter_constant_dims along the repeat axis without splitting the dataset. If you want parameters shared across a repeat dimension, map it to an explicit observable or set parameter_constant_dims to collapse that axis.

Multi-axis editing example#

When an imported scan has multiple acquisition axes and temporal repeats, you can override the inferred layout and reshape the data explicitly:

from mrax.data_struct import reshape_frames_to_dims, reshape_observables_to_dims

exp = study.experiments[idx]
ds = study.datasets[idx]
print(exp.describe_dims())

# Override dims: 12 settings along axis_a, 3 along axis_b, 2 repeats.
exp.dims = {
    "axis_a": 12,
    "axis_b": 3,
    "t": 2,
    "x": exp.dims["x"],
    "y": exp.dims["y"],
}
dim_map = getattr(exp, "_mrax_dim_observables", None)
exp.observables = reshape_observables_to_dims(exp.observables, exp.dims, dim_map)
ds.data = reshape_frames_to_dims(ds.data, exp.dims)

Joint experiments and partial fits#

When multiple scans measure the same subject or phantom with related acquisition settings, you can combine them into a single frame-major experiment for a joint fit:

from mrax.data_struct import join_experiments
from mrax.core.loss_builder import run_fit

joint = join_experiments(
    [exp_15, exp_16, exp_17],
    [ds_15, ds_16, ds_17],
    names=["15", "16", "17"],
    joint_name="joint_protocol",
)
joint_exp, joint_ds = joint.experiment, joint.dataset
fields, losses = run_fit(joint_exp, model, joint_ds, auto_mask_nan_observables=True)

Missing observables are filled with NaN by default; auto_mask_nan_observables=True ensures those frames do not contribute to the loss. By default the join helper adds a joint_source observable (integer index per frame) so side models can condition on scan/timepoint membership.

To fit only a subset (e.g., a single scan or timepoint), select frames by source name:

sub_exp, sub_ds = joint.select("16")
fields_sub, _ = run_fit(sub_exp, model, sub_ds)

Side models that explain parameter drift or time dependence can be expressed by fitting a separate experiment that outputs the needed map, then inserting it into the joint experiment with Experiment.insert_observables.

Defining a Study#

from mrax.data_struct import Quantity, Study
from mrax.core.synthetic import make_example_suite

examples = make_example_suite(noise_level=0.05, seed=0)
study_globals = {"field_strength_mhz": Quantity(400.0, "MHz")}
study = Study.from_examples(examples, study_globals=study_globals)

Synthetic builders (mrax.core.synthetic.make_synthetic_study()) and importers (mrax.io.bruker_import.load_bruker_study()) both emit Study objects, so the downstream fitting and storage flows are identical regardless of the data source. Study-wide globals, such as scanner or protocol constants, belong in study_globals and are merged into every experiment when fitting.

Inserting observables from prior fits#

If a downstream model needs maps produced elsewhere, insert them explicitly:

from mrax.data_struct import Quantity

target_exp = study.experiments[target_idx]
target_exp = target_exp.insert_observables(
    {
        "baseline_map": Quantity(baseline_vals, "a.u."),
        "calibration_map": Quantity(calibration_vals, "1"),
    }
)

Sanity checks#

Use the built-in sanity checks to confirm that run_fit has everything it needs:

report = study.experiments[idx].sanity_check(model, dataset)
print(report.explain())

Study-level helpers are also available:

print(study.explain_sanity())

Planning and Execution#

Use mrax.core.study_runner.plan_study_fits() to run fits in the order provided by the study, optionally filtering with target_names or reusing cached results via precomputed_fields.

If an experiment is missing required observables, execute_study_plan skips it by default and returns a result entry marked skipped. Insert the missing observables and re-run to fit the skipped experiments.

Shared run_fit options belong in run_fit_kwargs. Use run_fit_overrides for per-experiment settings; dict-valued run_fit options such as regularizer_weights are passed through unchanged.

To re-fit only the skipped experiments, collect them from the plan/results:

from mrax.core.study_runner import collect_skipped_experiments

plan = plan_study_fits(study)
results, _ = execute_study_plan(plan, run_fit_kwargs={"num_steps": 50})
skipped = collect_skipped_experiments(plan, results)
refit_plan = list(skipped.values())
results_refit, _ = execute_study_plan(refit_plan, run_fit_kwargs={"num_steps": 50})

Visualization#

Human-readable descriptions are available:

print(study.describe())          # text summary

Add save_nifti=True to save_study_results to export NIfTI volumes alongside the MRD outputs.