Source code for mrsiprep.io.mrsinmrs

"""MRSinMRS (Lin et al. 2021, PMID 33559967) sequence-parameter reporting.

Reads an optional dataset-level ``mrsinmrs.json`` at the BIDS root, carrying
MRSI acquisition/hardware/reconstruction parameters so they can be surfaced
in each subject/session's QC report for reproducibility and transparency.
This file is never required for processing -- its absence just means the
report's MRSinMRS section is omitted.
"""

from __future__ import annotations

import json
from pathlib import Path

from mrsiprep.utils.misc import normalize_session, normalize_subject

MRSINMRS_FILENAME = "mrsinmrs.json"


[docs] def load_mrsinmrs(bids_dir: str | Path) -> dict | None: """Parse ``<bids_dir>/mrsinmrs.json`` if present. Returns None if the file does not exist (this is the expected default case). Raises ValueError on a malformed file, matching load_bids_filters()'s "loud on error, silent only when absent" style. """ path = Path(bids_dir) / MRSINMRS_FILENAME if not path.exists(): return None try: data = json.loads(path.read_text()) except (OSError, json.JSONDecodeError) as exc: raise ValueError(f"Could not parse {path}: {exc}") from exc if not isinstance(data, dict): raise ValueError(f"{path} must contain a JSON object at the top level.") unsupported = set(data) - {"CommonMetadata", "Recordings"} if unsupported: raise ValueError( f"{path} has unsupported top-level key(s) {sorted(unsupported)}; " "only 'CommonMetadata' and 'Recordings' are supported." ) return data
[docs] def resolve_mrsinmrs(parsed: dict | None, subject: str, session: str | None) -> dict | None: """Merge CommonMetadata with the matching Recordings entry. Recording match order: exact subject+session, then subject-only (a Recordings entry with no "ses" key applies to all of that subject's sessions). Returns None only if `parsed` itself is None or the file has no CommonMetadata and no matching Recordings entry. """ if not parsed: return None common = dict(parsed.get("CommonMetadata") or {}) recordings = parsed.get("Recordings") or [] sub_norm = normalize_subject(subject) ses_norm = normalize_session(session) match = None for entry in recordings: entry_sub = normalize_subject(entry.get("sub", "")) if entry_sub != sub_norm: continue entry_ses = normalize_session(entry.get("ses")) if entry_ses == ses_norm: match = entry break if entry_ses is None and match is None: match = entry merged = {**common, **{k: v for k, v in (match or {}).items() if k not in {"sub", "ses"}}} return merged or None