I/O and format support¶
The reader contract¶
class Reader(ABC):
format_name: str
extensions: tuple[str, ...]
def can_read(self, path) -> bool: ... # cheap sniff — extension + magic bytes
def read(self, path, **options) -> MRIData: ... # full parse, validated, provenance recorded
can_readmust be cheap and side-effect-free: check the extension, and if needed read a few magic bytes. Never parse the whole file.readmust return aMRIDatathat passes.validate()and has at least oneprovenancestep describing the read (source path, reader, key options).- A reader that raises inside
can_readis skipped, not fatal.
Dispatch¶
Readers register with register_reader(MyReader()) (optionally
prepend=True for priority). unimri.read(path) calls find_reader, which
returns the first reader whose can_read accepts the file, else raises
UnsupportedFormatError. Built-in readers are registered at import time.
Format status¶
| Format | Extension | Reader | Status | Backend |
|---|---|---|---|---|
| ISMRMRD | .mrd, .h5 |
ISMRMRDReader |
✅ Cartesian — see scope below | ismrmrd |
| UniMRI HDF5 | .h5, .hdf5 |
HDF5Reader |
interface only — Milestone 1 | h5py |
| Siemens TWIX | .dat |
TwixReader |
interface only — Milestone 2 | twixtools |
| GE | .7, ScanArchive |
— | planned — Milestone 7 | TBD (Orchestra / pfile) |
| Philips | .raw/.lab/.sin |
— | planned — Milestone 7 | TBD |
| Bruker | fid/2dseq |
— | later | brukerapi |
"Interface only" means can_read is implemented (so detection works) but
read raises NotImplementedError pointing at the roadmap.
ISMRMRDReader scope (as of 0.1.0a1)¶
Wraps the ismrmrd package. Handles a single Cartesian encoding space
with exactly one slice / average / contrast / repetition / set / segment —
validated end-to-end (write a real ISMRMRD file → read → reconstruct →
match ground truth) in tests/test_io_ismrmrd.py. Noise-measurement
acquisitions are skipped (no pre-whitening yet). Not yet supported, and
rejected with a clear ReaderError rather than silently mishandled:
- Non-Cartesian ISMRMRD trajectories (radial, spiral, ...) — Milestone 2/5.
- Multi-slice / multi-average / multi-repetition / multi-set / multi-segment acquisitions — tracked for a future milestone.
- Nucleus label: ISMRMRD's header has no standard field for it, so
AcquisitionInfo.nucleusstays at its"1H"default; set it manually for multinuclear files.
Why ISMRMRD first¶
ISMRMRD already solved the hard part of the data model — it standardises
acquisitions, encoding spaces, and trajectories, and there is a large body of
public test data and vendor converters (siemens_to_ismrmrd, ge_to_ismrmrd,
philips_to_ismrmrd). Making ISMRMRDReader the first real reader means the
MRIData design is validated against a mature schema and real datasets before
the vendor-specific readers are written.
Trajectory extraction¶
For non-Cartesian vendor data the reader is responsible for producing a
Trajectory in NORMALIZED units:
- ISMRMRD — use the stored
trajarray on each acquisition when present. - Siemens TWIX — either the analytic trajectory computed from sequence special-card parameters (radial views, density-adapted readout profile, etc.), or a measured trajectory stored as a separate MDB category. Gradient-delay / GIRF correction is a preprocessing step, not the reader's job.
Writing your own reader¶
Subclass Reader, implement the two methods, and register it:
from unimri.io import Reader, register_reader
class MyLabReader(Reader):
format_name = "MyLab binary"
extensions = (".mlb",)
def can_read(self, path): ...
def read(self, path, **opts): ...
register_reader(MyLabReader())
Contributions of readers for additional formats are very welcome — see
CONTRIBUTING.md.