Skip to content
2 changes: 1 addition & 1 deletion python/MRzeroCore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .phantom.custom_voxel_phantom import CustomVoxelPhantom
from .phantom.sim_data import SimData
from .phantom.brainweb import generate_brainweb_phantoms
from .phantom.nifti_phantom import NiftiPhantom, NiftiTissue, NiftiRef, NiftiMapping
from .phantom.nifti_phantom import NiftiPhantom, NiftiTissue, NiftiRef, NiftiMapping, ResliceConfig
from .phantom.tissue_dict import TissueDict
from .simulation.isochromat_sim import isochromat_sim
from .simulation.pre_pass import compute_graph, compute_graph_ext, Graph
Expand Down
3 changes: 3 additions & 0 deletions python/MRzeroCore/phantom/custom_voxel_phantom.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def build(self) -> SimData:
# TODO: until the dephasing func fix is here, this only works on the
# device self.voxel_size happens to be on
size = self.voxel_pos.max(0).values - self.voxel_pos.min(0).values

affine = torch.eye(3,4)

return SimData(
self.PD,
Expand All @@ -140,6 +142,7 @@ def build(self) -> SimData:
self.B1[None, :],
torch.ones(1, self.PD.numel()),
size,
affine,
self.voxel_pos,
torch.tensor([float('inf'), float('inf'), float('inf')]),
build_dephasing_func(self.voxel_shape, self.voxel_size),
Expand Down
37 changes: 32 additions & 5 deletions python/MRzeroCore/phantom/nifti_phantom.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Literal, Any
from pathlib import Path

Expand Down Expand Up @@ -65,6 +65,20 @@ def to_dict(self) -> dict[str, float]:
return {"gyro": self.gyro, "B0": self.B0}


@dataclass
class ResliceConfig:
"""Target resampling grid declared inside the phantom JSON."""
resolution: list # [nx, ny, nz] — output voxel count
affine: list # 3×4 NIfTI-style affine in mm (rotation×voxel_size | origin)

@classmethod
def from_dict(cls, config: dict):
return cls(resolution=config["resolution"], affine=config["affine"])

def to_dict(self) -> dict:
return {"resolution": self.resolution, "affine": self.affine}


@dataclass
class NiftiRef:
file_name: Path
Expand Down Expand Up @@ -162,6 +176,7 @@ class NiftiPhantom:
units: PhantomUnits
system: PhantomSystem
tissues: dict[str, NiftiTissue]
reslice_to: ResliceConfig | None = field(default=None)

@classmethod
def default(cls, gyro=42.5764, B0=3.0):
Expand All @@ -177,32 +192,44 @@ def load(cls, path: Path | str):

def save(self, path: Path | str):
import json
import re
import os

path = Path(path)

os.makedirs(path.parent, exist_ok=True)
text = json.dumps(self.to_dict(), indent=2)
# Compact arrays of numbers onto a single line
text = re.sub(
r'\[(\n\s+[-\d.eE+]+,?)+\n\s*\]',
lambda m: '[' + ', '.join(re.findall(r'[-\d.eE+]+', m.group(0))) + ']',
text
)
with open(path, "w") as f:
json.dump(self.to_dict(), f, indent=2)
f.write(text)

@classmethod
def from_dict(cls, config: dict):
assert config["file_type"] == "nifti_phantom_v1"
units = PhantomUnits.from_dict(config["units"])
system = PhantomSystem.from_dict(config["system"])
tissues = {
name: NiftiTissue.from_dict(tissue)
for name, tissue in config["tissues"].items()
}
reslice_to = (ResliceConfig.from_dict(config["reslice_to"])
if "reslice_to" in config else None)

return cls(units, system, tissues)
return cls(units, system, tissues, reslice_to)

def to_dict(self) -> dict:
return {
d = {
"file_type": self.file_type,
"units": self.units.to_dict(),
"system": self.system.to_dict(),
"tissues": {
name: tissue.to_dict() for name, tissue in self.tissues.items()
},
}
if self.reslice_to is not None:
d["reslice_to"] = self.reslice_to.to_dict()
return d
6 changes: 6 additions & 0 deletions python/MRzeroCore/phantom/sim_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class are nothing but the data needed for simulation, so it can describe
size : torch.Tensor
Physical size of the phantom. If a sequence with normalized gradients
is simulated, size is used to scale them to match the phantom.
affine : torch.Tensor
Affine matrix of the phantom data, in millimeters.
avg_B1_trig : torch.Tensor
(361, 3) values containing the PD-weighted avg of sin/cos/sin²(B1*flip)
voxel_pos : torch.Tensor
Expand All @@ -61,6 +63,7 @@ def __init__(
B1: torch.Tensor,
coil_sens: torch.Tensor,
size: torch.Tensor,
affine: torch.Tensor,
voxel_pos: torch.Tensor,
nyquist: torch.Tensor,
dephasing_func: Callable[[torch.Tensor, torch.Tensor], torch.Tensor],
Expand Down Expand Up @@ -100,6 +103,7 @@ def __init__(
self.tissue_masks = {}
self.coil_sens = coil_sens.clone()
self.size = size.clone()
self.affine = affine.clone()
self.voxel_pos = voxel_pos.clone()
self.avg_B1_trig = calc_avg_B1_trig(B1, PD)
self.nyquist = nyquist.clone()
Expand All @@ -125,6 +129,7 @@ def cuda(self) -> SimData:
self.B1.cuda(),
self.coil_sens.cuda(),
self.size.cuda(),
self.affine.cuda(),
self.voxel_pos.cuda(),
self.nyquist.cuda(),
self.dephasing_func,
Expand Down Expand Up @@ -152,6 +157,7 @@ def cpu(self) -> SimData:
self.B1.cpu(),
self.coil_sens.cpu(),
self.size.cpu(),
self.affine.cpu(),
self.voxel_pos.cpu(),
self.nyquist.cpu(),
self.dephasing_func,
Expand Down
Loading
Loading