bifti 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bifti-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MRX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
bifti-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: bifti
3
+ Version: 0.1.0
4
+ Summary: Load, save and resample BIfTI MRI simulation phantoms (JSON + NIfTI)
5
+ Keywords: mri,phantom,simulation,nifti,bifti
6
+ Author: J-Endres
7
+ Author-email: J-Endres <jonathan.endres01@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
13
+ Classifier: Topic :: Scientific/Engineering :: Physics
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: nibabel>=5.4.2
16
+ Requires-Dist: numpy>=2.5.1
17
+ Requires-Dist: requests>=2.34.2
18
+ Requires-Dist: torch>=2.13.0 ; extra == 'torch'
19
+ Requires-Python: >=3.12
20
+ Project-URL: Homepage, https://github.com/mrx-org/bifti-phantoms
21
+ Project-URL: Documentation, https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/README.md
22
+ Project-URL: Repository, https://github.com/mrx-org/bifti-phantoms
23
+ Project-URL: Issues, https://github.com/mrx-org/bifti-phantoms/issues
24
+ Project-URL: Registry, https://mrx-org.github.io/bifti-phantoms/
25
+ Provides-Extra: torch
26
+ Description-Content-Type: text/markdown
27
+
28
+ # bifti
29
+
30
+ A Python package for the [BIfTI phantom format](https://github.com/mrx-org/bifti-phantoms/blob/main/SPEC.md): parse/serialize
31
+ the phantom JSON, load a phantom into plain NumPy arrays, and fetch phantoms
32
+ from the public [registry](https://github.com/mrx-org/bifti-phantoms/blob/main/REGISTRY.md).
33
+
34
+ ```python
35
+ from bifti import NumpyPhantom
36
+
37
+ phantom = NumpyPhantom.load("subj42-3T.json")
38
+ tissue = phantom.tissues["gm"] # a NumpyTissue: density, T1, T2, ... as np.ndarray
39
+ print(tissue.shape, tissue.T1.mean())
40
+ ```
41
+
42
+ `phantom.config` is the parsed `BiftiPhantom` (the raw JSON structure: units,
43
+ system, tissue definitions) — see [phantom.py](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/phantom.py) for the
44
+ full data model, and [loader.py](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py) for how each property
45
+ resolves to an array.
46
+
47
+ ## API at a glance
48
+
49
+ Everything below is importable from the top-level `bifti` package (`from
50
+ bifti import ...`):
51
+
52
+ | Name | What it is |
53
+ |------|------------|
54
+ | `NumpyPhantom.load(path)` | Load a phantom JSON + its NIfTIs into `NumpyPhantom(config, tissues)`. |
55
+ | `NumpyTissue` | One tissue as NumPy arrays: `density`, `T1`, `T2`, `T2dash`, `ADC`, `dB0`, `B1_tx`, `B1_rx`, plus `.shape`/`.affine`. |
56
+ | `NumpyPhantom.save(dir, json_name="phantom.json")` | Save a loaded phantom as a self-contained folder (float64 NIfTIs + JSON, no `reslice_to`, no `func`); see [Saving](https://github.com/mrx-org/bifti-phantoms/blob/main/README.md#saving). Returns the JSON path. |
57
+ | `BiftiPhantom.load(path)` / `.save(path)` | Parse/serialize just the JSON side (no NIfTI I/O) — `BiftiPhantom(units, system, tissues, reslice_to, schema)`. |
58
+ | `load_catalog()` | Fetch and parse the public [catalog.json](https://github.com/mrx-org/bifti-phantoms/blob/main/catalog.json) — the discovery list, mapping a label to an immutable registry name. |
59
+ | `load_registry()` | Fetch and parse the public [registry.json](https://github.com/mrx-org/bifti-phantoms/blob/main/registry.json) — the immutable archive of every collection. |
60
+ | `load_registry_phantom(collection, name)` | Download one phantom's JSON + NIfTIs from Zenodo into a local cache; returns the JSON path. `collection` is an immutable registry name. |
61
+
62
+ `NumpyPhantom.load` is what you want for simulation/analysis (arrays); use
63
+ `BiftiPhantom` directly only if you're generating or editing phantom JSONs
64
+ without touching pixel data (e.g. [`examples/make_numerical_brain_cropped_bifti.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/make_numerical_brain_cropped_bifti.py)).
65
+
66
+ ## Installation
67
+
68
+ From [PyPI](https://pypi.org/project/bifti/):
69
+
70
+ ```sh
71
+ # using uv (recommended):
72
+ uv add bifti
73
+ # or using pip:
74
+ pip install bifti
75
+ ```
76
+
77
+ Or the latest unreleased version from git:
78
+
79
+ ```sh
80
+ uv add --git https://github.com/mrx-org/bifti-phantoms --subdirectory python/bifti bifti
81
+ pip install "git+https://github.com/mrx-org/bifti-phantoms.git#subdirectory=python/bifti"
82
+ ```
83
+
84
+ ## Examples
85
+
86
+ [`examples/`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/) has a small, self-contained demo that **loads a BIfTI
87
+ phantom and plots every tissue's data** — a starting point for seeing the
88
+ package in action.
89
+
90
+ ```sh
91
+ git clone https://github.com/mrx-org/bifti-phantoms
92
+ uv add --path bifti-phantoms/python/bifti bifti
93
+ ```
94
+
95
+ | File | Purpose |
96
+ |------|---------|
97
+ | [`examples/demo.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/demo.py) | Load a phantom and plot one figure per tissue. |
98
+ | [`examples/nifti_to_koma.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/nifti_to_koma.py) | Convert a phantom to a KomaMRI `.phantom` HDF5 file. |
99
+ | [`examples/mrzero_sim.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/mrzero_sim.py) | Simulate a Pulseq sequence on a phantom with MR-zero. |
100
+ | [`examples/make_numerical_brain_cropped_bifti.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/make_numerical_brain_cropped_bifti.py) | Port the legacy MRzero test phantom to BIfTI. |
101
+ | [`examples/data/`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/) | Phantom JSONs + the NIfTIs they reference. |
102
+ | [`examples/data/generate.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/generate.py) | Regenerates the example data in `examples/data/` (reproducible). |
103
+
104
+ ### Requirements
105
+
106
+ The core package only needs `bifti`'s own dependencies (numpy, nibabel,
107
+ requests). Resampling uses `torch` when it is importable — on CPU as well as
108
+ CUDA — and NumPy otherwise; install it with the `torch` extra
109
+ (`pip install "bifti[torch]"`) or force a backend with
110
+ `BIFTI_RESAMPLE_BACKEND=numpy|torch`. The examples pull in extra,
111
+ examples-only dependencies
112
+ (`matplotlib`, `h5py`, `MRzeroCore`, `torch`) declared as a
113
+ [dependency group](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups)
114
+ rather than real package dependencies:
115
+
116
+ ```sh
117
+ uv sync --group examples
118
+ ```
119
+
120
+ ### Running
121
+
122
+ From this `python/bifti` directory:
123
+
124
+ ```sh
125
+ # list the registry, pick one, download + plot
126
+ uv run --group examples examples/demo.py
127
+ # ...or plot a local phantom JSON directly
128
+ uv run --group examples examples/demo.py examples/data/shapes.json
129
+ uv run --group examples examples/demo.py examples/data/shapes_resliced.json
130
+ uv run --group examples examples/demo.py examples/data/shapes_downsampled.json
131
+ ```
132
+
133
+ With no argument, `demo.py` fetches the live [`catalog.json`](https://github.com/mrx-org/bifti-phantoms/blob/main/catalog.json),
134
+ resolves each label to its [`registry.json`](https://github.com/mrx-org/bifti-phantoms/blob/main/registry.json) collection,
135
+ prints their phantoms as a numbered list, and downloads the one you pick (its
136
+ JSON and every NIfTI it references) into `examples/cache/` via `bifti.registry`
137
+ before plotting. Passing a local JSON path skips the catalog and plots that
138
+ file directly (the example data is committed in `examples/data/`).
139
+
140
+ `demo.py` saves one PNG per tissue into `examples/figures/` and, on a GUI
141
+ backend, also shows them. Each figure tiles the tissue's maps — `density`,
142
+ `T1`, `T2`, `T2'`, `ADC`, `dB0`, and every `B1+`/`B1-` channel — at the volume's
143
+ middle slice. Spatially uniform properties show as a flat field; properties
144
+ left at their default (e.g. `T1 = inf`) are blanked out.
145
+
146
+ ## The example data
147
+
148
+ The NIfTIs in `examples/data/` are committed, but they are all produced by
149
+ [`examples/data/generate.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/generate.py) — re-run it
150
+ (`uv run --group examples examples/data/generate.py`) to regenerate them. It
151
+ derives everything from a fixed seed and simple `meshgrid` functions (smooth
152
+ bumps, low-order polynomials, a little noise), so re-running reproduces
153
+ byte-identical files. The phantoms together exercise the whole format:
154
+
155
+ - **`subj42-3T.json`** (hand-written) — a brain-like single slice. Uses
156
+ `reslice_to`, so all maps are resampled from their native 64×64 grid onto a
157
+ 100×100 grid on load. Exercises: scalar properties, a shared `dB0` map, an
158
+ **8-channel `B1+`**, and a transformed reference (`fat.dB0 = "x - 420"`).
159
+ - **`shapes.json`** (generated) — a small 40×32×4 phantom with **no
160
+ `reslice_to`**, so it loads on its native grid. Exercises: multiple tissues
161
+ sharing one grid, a polynomial `dB0` map, a 2-channel `B1+`, a `func` mapping
162
+ (`"x * 0.5 + 10"`), property defaults, and a true 3D volume.
163
+ - **`shapes_resliced.json`** (generated) — the same NIfTIs as `shapes.json` but
164
+ with a `reslice_to` onto a finer grid (40×32×4 → **60×48×4**), so loading
165
+ genuinely resamples the 3D volumes.
166
+ - **`shapes_downsampled.json`** (generated) — the same NIfTIs again, resliced
167
+ onto a *coarser* grid (40×32×4 → **16×12×3**) whose FOV deliberately
168
+ overhangs the source. Exercises the case plain interpolation gets wrong:
169
+ output voxels that average many source voxels, and output voxels that
170
+ straddle the edge of the source FOV.
171
+
172
+ > **Note:** loading a tissue with a `func` mapping prints a warning — the loader
173
+ > evaluates `func` with `eval` for brevity, so only load phantoms you trust (see
174
+ > the note in [`bifti/loader.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py)).
175
+
176
+ ## Known limitations
177
+
178
+ - **Complex-valued NIfTI data isn't handled.** A complex `B1+`/`B1-` map gets
179
+ cast with `np.asarray(..., dtype=np.float64)` in
180
+ [`loader.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py), which silently discards the imaginary
181
+ part (numpy emits a `ComplexWarning`, easy to miss). The
182
+ [Rust crate](https://github.com/mrx-org/bifti-phantoms/blob/main/rust/bifti/) fails loudly instead in this case.
183
+ - Only the default [`units`](https://github.com/mrx-org/bifti-phantoms/blob/main/JSON.md#units) are accepted.
184
+
185
+ ## See also
186
+
187
+ - [../../README.md](https://github.com/mrx-org/bifti-phantoms/blob/main/README.md) — repo overview, format spec, and the
188
+ registry.
189
+ - [../../rust/bifti/](https://github.com/mrx-org/bifti-phantoms/blob/main/rust/bifti/) — the Rust crate, including how it
190
+ differs from this package.
bifti-0.1.0/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # bifti
2
+
3
+ A Python package for the [BIfTI phantom format](https://github.com/mrx-org/bifti-phantoms/blob/main/SPEC.md): parse/serialize
4
+ the phantom JSON, load a phantom into plain NumPy arrays, and fetch phantoms
5
+ from the public [registry](https://github.com/mrx-org/bifti-phantoms/blob/main/REGISTRY.md).
6
+
7
+ ```python
8
+ from bifti import NumpyPhantom
9
+
10
+ phantom = NumpyPhantom.load("subj42-3T.json")
11
+ tissue = phantom.tissues["gm"] # a NumpyTissue: density, T1, T2, ... as np.ndarray
12
+ print(tissue.shape, tissue.T1.mean())
13
+ ```
14
+
15
+ `phantom.config` is the parsed `BiftiPhantom` (the raw JSON structure: units,
16
+ system, tissue definitions) — see [phantom.py](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/phantom.py) for the
17
+ full data model, and [loader.py](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py) for how each property
18
+ resolves to an array.
19
+
20
+ ## API at a glance
21
+
22
+ Everything below is importable from the top-level `bifti` package (`from
23
+ bifti import ...`):
24
+
25
+ | Name | What it is |
26
+ |------|------------|
27
+ | `NumpyPhantom.load(path)` | Load a phantom JSON + its NIfTIs into `NumpyPhantom(config, tissues)`. |
28
+ | `NumpyTissue` | One tissue as NumPy arrays: `density`, `T1`, `T2`, `T2dash`, `ADC`, `dB0`, `B1_tx`, `B1_rx`, plus `.shape`/`.affine`. |
29
+ | `NumpyPhantom.save(dir, json_name="phantom.json")` | Save a loaded phantom as a self-contained folder (float64 NIfTIs + JSON, no `reslice_to`, no `func`); see [Saving](https://github.com/mrx-org/bifti-phantoms/blob/main/README.md#saving). Returns the JSON path. |
30
+ | `BiftiPhantom.load(path)` / `.save(path)` | Parse/serialize just the JSON side (no NIfTI I/O) — `BiftiPhantom(units, system, tissues, reslice_to, schema)`. |
31
+ | `load_catalog()` | Fetch and parse the public [catalog.json](https://github.com/mrx-org/bifti-phantoms/blob/main/catalog.json) — the discovery list, mapping a label to an immutable registry name. |
32
+ | `load_registry()` | Fetch and parse the public [registry.json](https://github.com/mrx-org/bifti-phantoms/blob/main/registry.json) — the immutable archive of every collection. |
33
+ | `load_registry_phantom(collection, name)` | Download one phantom's JSON + NIfTIs from Zenodo into a local cache; returns the JSON path. `collection` is an immutable registry name. |
34
+
35
+ `NumpyPhantom.load` is what you want for simulation/analysis (arrays); use
36
+ `BiftiPhantom` directly only if you're generating or editing phantom JSONs
37
+ without touching pixel data (e.g. [`examples/make_numerical_brain_cropped_bifti.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/make_numerical_brain_cropped_bifti.py)).
38
+
39
+ ## Installation
40
+
41
+ From [PyPI](https://pypi.org/project/bifti/):
42
+
43
+ ```sh
44
+ # using uv (recommended):
45
+ uv add bifti
46
+ # or using pip:
47
+ pip install bifti
48
+ ```
49
+
50
+ Or the latest unreleased version from git:
51
+
52
+ ```sh
53
+ uv add --git https://github.com/mrx-org/bifti-phantoms --subdirectory python/bifti bifti
54
+ pip install "git+https://github.com/mrx-org/bifti-phantoms.git#subdirectory=python/bifti"
55
+ ```
56
+
57
+ ## Examples
58
+
59
+ [`examples/`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/) has a small, self-contained demo that **loads a BIfTI
60
+ phantom and plots every tissue's data** — a starting point for seeing the
61
+ package in action.
62
+
63
+ ```sh
64
+ git clone https://github.com/mrx-org/bifti-phantoms
65
+ uv add --path bifti-phantoms/python/bifti bifti
66
+ ```
67
+
68
+ | File | Purpose |
69
+ |------|---------|
70
+ | [`examples/demo.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/demo.py) | Load a phantom and plot one figure per tissue. |
71
+ | [`examples/nifti_to_koma.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/nifti_to_koma.py) | Convert a phantom to a KomaMRI `.phantom` HDF5 file. |
72
+ | [`examples/mrzero_sim.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/mrzero_sim.py) | Simulate a Pulseq sequence on a phantom with MR-zero. |
73
+ | [`examples/make_numerical_brain_cropped_bifti.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/make_numerical_brain_cropped_bifti.py) | Port the legacy MRzero test phantom to BIfTI. |
74
+ | [`examples/data/`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/) | Phantom JSONs + the NIfTIs they reference. |
75
+ | [`examples/data/generate.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/generate.py) | Regenerates the example data in `examples/data/` (reproducible). |
76
+
77
+ ### Requirements
78
+
79
+ The core package only needs `bifti`'s own dependencies (numpy, nibabel,
80
+ requests). Resampling uses `torch` when it is importable — on CPU as well as
81
+ CUDA — and NumPy otherwise; install it with the `torch` extra
82
+ (`pip install "bifti[torch]"`) or force a backend with
83
+ `BIFTI_RESAMPLE_BACKEND=numpy|torch`. The examples pull in extra,
84
+ examples-only dependencies
85
+ (`matplotlib`, `h5py`, `MRzeroCore`, `torch`) declared as a
86
+ [dependency group](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups)
87
+ rather than real package dependencies:
88
+
89
+ ```sh
90
+ uv sync --group examples
91
+ ```
92
+
93
+ ### Running
94
+
95
+ From this `python/bifti` directory:
96
+
97
+ ```sh
98
+ # list the registry, pick one, download + plot
99
+ uv run --group examples examples/demo.py
100
+ # ...or plot a local phantom JSON directly
101
+ uv run --group examples examples/demo.py examples/data/shapes.json
102
+ uv run --group examples examples/demo.py examples/data/shapes_resliced.json
103
+ uv run --group examples examples/demo.py examples/data/shapes_downsampled.json
104
+ ```
105
+
106
+ With no argument, `demo.py` fetches the live [`catalog.json`](https://github.com/mrx-org/bifti-phantoms/blob/main/catalog.json),
107
+ resolves each label to its [`registry.json`](https://github.com/mrx-org/bifti-phantoms/blob/main/registry.json) collection,
108
+ prints their phantoms as a numbered list, and downloads the one you pick (its
109
+ JSON and every NIfTI it references) into `examples/cache/` via `bifti.registry`
110
+ before plotting. Passing a local JSON path skips the catalog and plots that
111
+ file directly (the example data is committed in `examples/data/`).
112
+
113
+ `demo.py` saves one PNG per tissue into `examples/figures/` and, on a GUI
114
+ backend, also shows them. Each figure tiles the tissue's maps — `density`,
115
+ `T1`, `T2`, `T2'`, `ADC`, `dB0`, and every `B1+`/`B1-` channel — at the volume's
116
+ middle slice. Spatially uniform properties show as a flat field; properties
117
+ left at their default (e.g. `T1 = inf`) are blanked out.
118
+
119
+ ## The example data
120
+
121
+ The NIfTIs in `examples/data/` are committed, but they are all produced by
122
+ [`examples/data/generate.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/examples/data/generate.py) — re-run it
123
+ (`uv run --group examples examples/data/generate.py`) to regenerate them. It
124
+ derives everything from a fixed seed and simple `meshgrid` functions (smooth
125
+ bumps, low-order polynomials, a little noise), so re-running reproduces
126
+ byte-identical files. The phantoms together exercise the whole format:
127
+
128
+ - **`subj42-3T.json`** (hand-written) — a brain-like single slice. Uses
129
+ `reslice_to`, so all maps are resampled from their native 64×64 grid onto a
130
+ 100×100 grid on load. Exercises: scalar properties, a shared `dB0` map, an
131
+ **8-channel `B1+`**, and a transformed reference (`fat.dB0 = "x - 420"`).
132
+ - **`shapes.json`** (generated) — a small 40×32×4 phantom with **no
133
+ `reslice_to`**, so it loads on its native grid. Exercises: multiple tissues
134
+ sharing one grid, a polynomial `dB0` map, a 2-channel `B1+`, a `func` mapping
135
+ (`"x * 0.5 + 10"`), property defaults, and a true 3D volume.
136
+ - **`shapes_resliced.json`** (generated) — the same NIfTIs as `shapes.json` but
137
+ with a `reslice_to` onto a finer grid (40×32×4 → **60×48×4**), so loading
138
+ genuinely resamples the 3D volumes.
139
+ - **`shapes_downsampled.json`** (generated) — the same NIfTIs again, resliced
140
+ onto a *coarser* grid (40×32×4 → **16×12×3**) whose FOV deliberately
141
+ overhangs the source. Exercises the case plain interpolation gets wrong:
142
+ output voxels that average many source voxels, and output voxels that
143
+ straddle the edge of the source FOV.
144
+
145
+ > **Note:** loading a tissue with a `func` mapping prints a warning — the loader
146
+ > evaluates `func` with `eval` for brevity, so only load phantoms you trust (see
147
+ > the note in [`bifti/loader.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py)).
148
+
149
+ ## Known limitations
150
+
151
+ - **Complex-valued NIfTI data isn't handled.** A complex `B1+`/`B1-` map gets
152
+ cast with `np.asarray(..., dtype=np.float64)` in
153
+ [`loader.py`](https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/src/bifti/loader.py), which silently discards the imaginary
154
+ part (numpy emits a `ComplexWarning`, easy to miss). The
155
+ [Rust crate](https://github.com/mrx-org/bifti-phantoms/blob/main/rust/bifti/) fails loudly instead in this case.
156
+ - Only the default [`units`](https://github.com/mrx-org/bifti-phantoms/blob/main/JSON.md#units) are accepted.
157
+
158
+ ## See also
159
+
160
+ - [../../README.md](https://github.com/mrx-org/bifti-phantoms/blob/main/README.md) — repo overview, format spec, and the
161
+ registry.
162
+ - [../../rust/bifti/](https://github.com/mrx-org/bifti-phantoms/blob/main/rust/bifti/) — the Rust crate, including how it
163
+ differs from this package.
@@ -0,0 +1,57 @@
1
+ [project]
2
+ name = "bifti"
3
+ version = "0.1.0"
4
+ description = "Load, save and resample BIfTI MRI simulation phantoms (JSON + NIfTI)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ keywords = [
9
+ "mri",
10
+ "phantom",
11
+ "simulation",
12
+ "nifti",
13
+ "bifti",
14
+ ]
15
+ classifiers = [
16
+ "Intended Audience :: Science/Research",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
19
+ "Topic :: Scientific/Engineering :: Physics",
20
+ "Typing :: Typed",
21
+ ]
22
+ requires-python = ">=3.12"
23
+ dependencies = [
24
+ "nibabel>=5.4.2",
25
+ "numpy>=2.5.1",
26
+ "requests>=2.34.2",
27
+ ]
28
+
29
+ [[project.authors]]
30
+ name = "J-Endres"
31
+ email = "jonathan.endres01@gmail.com"
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/mrx-org/bifti-phantoms"
35
+ Documentation = "https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/README.md"
36
+ Repository = "https://github.com/mrx-org/bifti-phantoms"
37
+ Issues = "https://github.com/mrx-org/bifti-phantoms/issues"
38
+ Registry = "https://mrx-org.github.io/bifti-phantoms/"
39
+
40
+ [project.optional-dependencies]
41
+ torch = ["torch>=2.13.0"]
42
+
43
+ [build-system]
44
+ requires = ["uv_build>=0.11.31,<0.12.0"]
45
+ build-backend = "uv_build"
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "pytest>=8.4.2",
50
+ "scipy>=1.18.0",
51
+ ]
52
+ examples = [
53
+ "h5py>=3.16.0",
54
+ "matplotlib>=3.11.1",
55
+ "mrzerocore>=1.0.4",
56
+ "torch>=2.13.0",
57
+ ]
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "bifti"
3
+ version = "0.1.0"
4
+ description = "Load, save and resample BIfTI MRI simulation phantoms (JSON + NIfTI)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "J-Endres", email = "jonathan.endres01@gmail.com" }
10
+ ]
11
+ keywords = ["mri", "phantom", "simulation", "nifti", "bifti"]
12
+ classifiers = [
13
+ "Intended Audience :: Science/Research",
14
+ "Programming Language :: Python :: 3",
15
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
16
+ "Topic :: Scientific/Engineering :: Physics",
17
+ "Typing :: Typed",
18
+ ]
19
+ requires-python = ">=3.12"
20
+ dependencies = [
21
+ "nibabel>=5.4.2",
22
+ "numpy>=2.5.1",
23
+ "requests>=2.34.2",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/mrx-org/bifti-phantoms"
28
+ Documentation = "https://github.com/mrx-org/bifti-phantoms/blob/main/python/bifti/README.md"
29
+ Repository = "https://github.com/mrx-org/bifti-phantoms"
30
+ Issues = "https://github.com/mrx-org/bifti-phantoms/issues"
31
+ Registry = "https://mrx-org.github.io/bifti-phantoms/"
32
+
33
+ [project.optional-dependencies]
34
+ # Resampling uses torch when it is importable and NumPy otherwise; see bifti._backend.
35
+ torch = ["torch>=2.13.0"]
36
+
37
+ [build-system]
38
+ requires = ["uv_build>=0.11.31,<0.12.0"]
39
+ build-backend = "uv_build"
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "pytest>=8.4.2",
44
+ # test-only: the reference trilinear implementation the upsample path is checked
45
+ # against. Resampling itself no longer needs scipy.
46
+ "scipy>=1.18.0",
47
+ ]
48
+ examples = [
49
+ "h5py>=3.16.0",
50
+ "matplotlib>=3.11.1",
51
+ "mrzerocore>=1.0.4",
52
+ "torch>=2.13.0",
53
+ ]
@@ -0,0 +1,43 @@
1
+ from .phantom import (
2
+ BiftiPhantom,
3
+ BiftiTissue,
4
+ NiftiMapping,
5
+ NiftiRef,
6
+ PATIENT_POSITIONS,
7
+ Patient,
8
+ PhantomSystem,
9
+ PhantomUnits,
10
+ ResliceTo,
11
+ patient_to_scanner,
12
+ )
13
+ from .loader import (
14
+ NumpyPhantom,
15
+ NumpyTissue,
16
+ to_scanner_affine,
17
+ )
18
+ from .registry import (
19
+ flatten_phantoms,
20
+ load_catalog,
21
+ load_registry,
22
+ load_registry_phantom,
23
+ )
24
+
25
+ __all__ = [
26
+ "BiftiPhantom",
27
+ "BiftiTissue",
28
+ "NiftiMapping",
29
+ "NiftiRef",
30
+ "PATIENT_POSITIONS",
31
+ "Patient",
32
+ "PhantomSystem",
33
+ "PhantomUnits",
34
+ "ResliceTo",
35
+ "patient_to_scanner",
36
+ "NumpyPhantom",
37
+ "NumpyTissue",
38
+ "to_scanner_affine",
39
+ "flatten_phantoms",
40
+ "load_catalog",
41
+ "load_registry",
42
+ "load_registry_phantom",
43
+ ]
@@ -0,0 +1,110 @@
1
+ """Array backend for resampling: torch when it is installed, NumPy otherwise.
2
+
3
+ The separable resampling path (see :mod:`bifti.resample`) is three matrix contractions, so
4
+ it maps straight onto whichever array library is available. Torch is used whenever it can be
5
+ imported - *not* only when CUDA is present, since torch is faster than NumPy on CPU too and
6
+ falls back to the same code path either way.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import warnings
13
+ from functools import lru_cache
14
+
15
+ import numpy as np
16
+
17
+
18
+ class Backend:
19
+ """The handful of array operations resampling needs."""
20
+
21
+ name = "numpy"
22
+
23
+ def asarray(self, x):
24
+ return np.asarray(x, dtype=np.float64)
25
+
26
+ def to_numpy(self, x) -> np.ndarray:
27
+ return np.asarray(x, dtype=np.float64)
28
+
29
+ def tensordot(self, a, b, axes):
30
+ return np.tensordot(a, b, axes=axes)
31
+
32
+ def moveaxis(self, x, source, destination):
33
+ return np.moveaxis(x, source, destination)
34
+
35
+ def transpose(self, x, axes):
36
+ return np.transpose(x, axes)
37
+
38
+
39
+ class TorchBackend(Backend):
40
+ """Torch-backed contractions, on CUDA when available.
41
+
42
+ Uses float64 on CPU so results match the NumPy backend exactly, and float32 on CUDA
43
+ where float64 throughput is heavily penalised. That makes the GPU path accurate to
44
+ roughly 1e-6 relative rather than to machine precision - far below the accuracy of the
45
+ phantom data itself.
46
+ """
47
+
48
+ name = "torch"
49
+
50
+ def __init__(self, torch):
51
+ self._torch = torch
52
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
53
+ self.dtype = torch.float32 if self.device.type == "cuda" else torch.float64
54
+
55
+ def asarray(self, x):
56
+ if self._torch.is_tensor(x):
57
+ return x.to(device=self.device, dtype=self.dtype)
58
+ return self._torch.as_tensor(
59
+ np.ascontiguousarray(x, dtype=np.float64),
60
+ dtype=self.dtype,
61
+ device=self.device,
62
+ )
63
+
64
+ def to_numpy(self, x) -> np.ndarray:
65
+ if self._torch.is_tensor(x):
66
+ return x.detach().to("cpu", self._torch.float64).numpy()
67
+ return np.asarray(x, dtype=np.float64)
68
+
69
+ def tensordot(self, a, b, axes):
70
+ return self._torch.tensordot(a, b, dims=axes)
71
+
72
+ def moveaxis(self, x, source, destination):
73
+ return self._torch.movedim(x, source, destination)
74
+
75
+ def transpose(self, x, axes):
76
+ return self._torch.permute(x, tuple(int(a) for a in axes))
77
+
78
+
79
+ def _load_torch_backend() -> Backend | None:
80
+ try:
81
+ import torch
82
+ except ImportError:
83
+ return None
84
+ try:
85
+ return TorchBackend(torch)
86
+ except Exception as exc: # pragma: no cover - torch present but unusable
87
+ warnings.warn(f"torch is installed but unusable ({exc}); falling back to NumPy")
88
+ return None
89
+
90
+
91
+ @lru_cache(maxsize=None)
92
+ def get_backend(name: str | None = None) -> Backend:
93
+ """The array backend to resample with.
94
+
95
+ ``name`` (or ``$BIFTI_RESAMPLE_BACKEND``) may be ``"auto"`` (the default), ``"numpy"``
96
+ or ``"torch"``. ``"torch"`` raises if torch is missing; ``"auto"`` quietly falls back.
97
+ """
98
+ name = (name or os.environ.get("BIFTI_RESAMPLE_BACKEND", "auto")).strip().lower()
99
+ if name == "numpy":
100
+ return Backend()
101
+ if name == "torch":
102
+ backend = _load_torch_backend()
103
+ if backend is None:
104
+ raise ImportError("BIFTI_RESAMPLE_BACKEND=torch but torch is not installed")
105
+ return backend
106
+ if name != "auto":
107
+ raise ValueError(
108
+ f"invalid resampling backend {name!r} (use auto, numpy or torch)"
109
+ )
110
+ return _load_torch_backend() or Backend()