brain-zero / tests /test_pipeline.py
rfick's picture
Squash history: one commit holding the current files
6bc44d6
Raw History Blame Contribute Delete
24.2 kB
"""The pipeline's pieces that need no GPU and no layout: protocols and their measurements, DiSCo's own table as
a protocol, the noise, the score on the ground truth itself, the config's consistency."""
import numpy as np
import pytest
from space import pipeline as P
from space.sources import disco as D
CFG = P.config()
def test_the_config_names_every_shell_and_preset_shape():
shapes = set(CFG["shapes"])
assert {s["shape"] for s in CFG["disco"]["shells"]} <= shapes
for name, preset in CFG["presets"].items():
assert {s["shape"] for s in preset["shells"]} <= shapes, name
assert P.preset_protocol(CFG, name).name == name
for s in CFG["shapes"].values():
assert 0 < s["delta"] < s["Delta"] < s["TE"]
def test_a_protocols_measurements_are_its_rows_in_order():
p = P.Protocol((P.Shell("d12-D24", 1000, 30), P.Shell("d8-D20", 6000, 12)), n_b0=2)
m = P.measurements(p, CFG["shapes"])
assert p.n_meas == 44 and m.bvals.shape == (44,) and m.dirs.shape == (44, 3)
assert m.b0.sum() == 2 and list(m.shape[:2]) == ["d12-D24"] * 2 # the b = 0 rows take the first shell's timing
assert (m.bvals[2:32] == 1000).all() and (m.bvals[32:] == 6000).all()
np.testing.assert_allclose(np.linalg.norm(m.dirs, axis=1), 1.0, atol=1e-12)
assert (m.dirs[2:, 2] > 0).all() # a hemisphere: no antipodal pairs
np.testing.assert_allclose(m.delta[32:], CFG["shapes"]["d8-D20"]["delta"])
np.testing.assert_allclose(m.Delta[:32], CFG["shapes"]["d12-D24"]["Delta"])
def test_protocol_refusals():
with pytest.raises(ValueError, match="at least one shell"):
P.Protocol(())
with pytest.raises(ValueError, match="b = 0"):
P.Protocol((P.Shell("d12-D24", 1000, 30),), n_b0=0)
with pytest.raises(ValueError, match="b > 0"):
P.Protocol((P.Shell("d12-D24", 0, 30),))
with pytest.raises(ValueError, match="directions must be"):
P.Protocol((P.Shell("d12-D24", 1000, 3),), directions=np.zeros((2, 3)))
with pytest.raises(KeyError, match="no timing class"):
P.measurements(P.Protocol((P.Shell("d99-D99", 1000, 3),)), CFG["shapes"])
def test_discos_table_is_a_protocol_of_its_own_rows():
"""The dataset's 364 rows: 4 shells on 3 timing classes, every row's direction its own, b = 0 rows first, the
rows' b-values those of the table."""
p, idx = D.disco_protocol(CFG)
m = P.measurements(p, CFG["shapes"])
bv = np.loadtxt(P.DATA_DIR + "/DiSCo_gradients.bvals").ravel()[idx] # the table's rows in the protocol's order
assert p.name == "DiSCo 364" and p.n_meas == 364 == len(idx) and sorted(idx) == list(range(364))
assert [s.b for s in p.shells] == [1000, 1925, 3094, 13192] # the table's shells, rounded means
assert [s.shape for s in p.shells] == ["d10.2-D16.7", "d10.2-D16.7", "d7.6-D45.9", "d17.7-D35.8"]
assert m.b0.sum() == p.n_b0 == 4 and m.b0[:p.n_b0].all()
np.testing.assert_array_equal(m.bvals, bv) # every row's own b-value, exactly
assert sorted(np.unique(np.round(bv)).tolist()) == [0, 1000, 1925, 3094, 13192]
np.testing.assert_allclose(np.linalg.norm(m.dirs, axis=1), 1.0, atol=1e-6)
np.testing.assert_allclose(m.delta[m.shape == "d7.6-D45.9"], 7.6e-3)
def test_noise_is_rician_at_the_given_snr_and_leaves_nan_alone():
dwi = np.full((5, 5, 5, 40), 0.5); dwi[0, 0, 0] = np.nan
out = P.add_noise(dwi, 20.0, seed=1)
assert np.isnan(out[0, 0, 0]).all() and np.isfinite(out[1:]).all()
assert abs(out[1:].std() - 0.05) < 0.005 and (out[1:] >= 0).all() # sigma = 1 / SNR, a magnitude
assert P.add_noise(dwi, None) is dwi
with pytest.raises(ValueError, match="positive"):
P.add_noise(dwi, 0.0)
out_t = P.add_noise(dwi, 20.0, seed=1, backend="torch") # numpy's stream: same distribution, another realisation
assert np.isnan(out_t[0, 0, 0]).all() and abs(out_t[1:].std() - 0.05) < 0.005 and not np.array_equal(out_t[1:], out[1:])
def test_the_snr_is_at_m0_and_each_voxels_b0_snr_follows_its_s0_factor():
"""A voxel whose b = 0 signal is half of M0 gets twice the sigma on its normalised signal (SNR 20 at M0 is SNR 10
there); the factor map must cover the voxels and be positive; the b = 0 SNR summary reads it back."""
dwi = np.full((2, 1, 1, 4000), 0.5); factor = np.array([[[1.0]], [[0.5]]])
out = P.add_noise(dwi, 20.0, factor, seed=3)
assert abs(out[0].std() - 0.05) < 0.004 and abs(out[1].std() - 0.10) < 0.008
with pytest.raises(ValueError, match="positive map"):
P.add_noise(dwi, 20.0, np.array([[[1.0]], [[0.0]]]))
with pytest.raises(ValueError, match="positive map"):
P.add_noise(dwi, 20.0, np.ones((3, 1, 1)))
S = np.ones((2, 1, 1, 3)); S[1] *= 0.4; M0 = np.array([[[1.0]], [[1.0]]])
norm, f = P.s0_normalised(S, np.array([True, False, False]), M0)
np.testing.assert_allclose(norm, 1.0); np.testing.assert_allclose(f.ravel(), [1.0, 0.4])
res = P.Result(None, None, norm, None, f, None, None, None, None, {}, snr=30.0)
assert P.b0_snr(res) == dict(median=21.0, min=12.0, max=30.0)
assert P.b0_snr(P.Result(None, None, norm, None, f, None, None, None, None, {})) is None
class _Source(D.Disco):
"""The ground truth and the config without any replay data (D.Disco loads the files, nothing else)."""
mode = "test"
def test_the_score_of_the_ground_truth_is_one_and_the_pairs_add_up():
gt = _Source(CFG)
s = gt.score(gt.gt_count, gt.reference(None, None)[0])
assert s["pearson_count"] == pytest.approx(1.0) and s["false_pairs"] == 0 == s["missed_pairs"]
assert s["connected_pairs"] == s["gt_pairs"] == 25 == P.connected_pairs(gt.gt_count)
empty = gt.score(np.zeros((16, 16)), gt.reference(None, None)[0])
assert empty["missed_pairs"] == 25 and empty["connected_pairs"] == 0 and np.isnan(empty["pearson_count"]) # a constant matrix: NaN, no warning
assert np.isnan(P.pearson([1, 1, 1], [1, 2, 3])) and P.pearson([1, 2, 3], [2, 4, 6]) == pytest.approx(1.0)
def test_the_volumes_round_trip(tmp_path):
"""The DWI and FOD written as NIfTI read back with the table beside them."""
import nibabel as nib
p = P.Protocol((P.Shell("d12-D24", 1000, 6),), n_b0=1)
m = P.measurements(p, CFG["shapes"])
dwi = np.random.default_rng(0).random((4, 4, 4, 7)); dwi[0, 0, 0] = np.nan
sh = np.random.default_rng(1).random((4, 4, 4, 45))
res = P.Result(p, m, dwi, np.zeros((4, 4, 4)), np.ones((4, 4, 4)), sh, None, None, None, {}, {})
paths = P.write_volumes(res, str(tmp_path), prefix="t", affine=np.eye(4))
back = np.asarray(nib.load(paths["dwi"]).dataobj)
np.testing.assert_allclose(back, np.nan_to_num(dwi).astype(np.float32))
np.testing.assert_allclose(np.loadtxt(paths["bvals"]), m.bvals); np.testing.assert_allclose(np.loadtxt(paths["bvecs"]).T, m.dirs, atol=1e-8)
assert np.asarray(nib.load(paths["fod"]).dataobj).shape == (4, 4, 4, 45)
def test_the_backend_and_residency_come_from_the_config_or_the_environment(monkeypatch):
monkeypatch.delenv("DISCO_BACKEND", raising=False); monkeypatch.delenv("DISCO_RESIDENT", raising=False)
assert P.backend(CFG) == CFG["compute"]["backend"] == "jax" and P.resident(CFG) is True
monkeypatch.setenv("DISCO_BACKEND", "torch"); monkeypatch.setenv("DISCO_RESIDENT", "0")
assert P.backend(CFG) == "torch" and P.resident(CFG) is False
monkeypatch.setenv("DISCO_BACKEND", "numpy")
with pytest.raises(ValueError, match="backend"):
P.backend(CFG)
def test_the_mode_and_the_columns_come_from_the_config_or_the_environment(monkeypatch):
monkeypatch.delenv("DISCO_MODE", raising=False); monkeypatch.delenv("DISCO_COLUMNS", raising=False)
assert P.mode(CFG) == "demo" and P.columns_uri(CFG).startswith("hf://")
monkeypatch.setenv("DISCO_MODE", "full"); monkeypatch.setenv("DISCO_COLUMNS", "/columns")
assert P.mode(CFG) == "full" and P.columns_uri(CFG) == "/columns"
monkeypatch.setenv("DISCO_MODE", "fast")
with pytest.raises(ValueError, match="mode"):
P.mode(CFG)
def test_a_free_pulse_timing_is_the_shells_own_in_full_mode():
p = P.Protocol((P.Shell.free(2000, 12, 0.02, 0.05, 0.09),), n_b0=1)
assert p.shells[0].shape == "d20-D50-TE90" and p.shells[0].free_timing
m = P.measurements(p, CFG["shapes"])
np.testing.assert_allclose(m.delta, 0.02); np.testing.assert_allclose(m.Delta, 0.05); np.testing.assert_allclose(m.TE, 0.09)
with pytest.raises(ValueError, match="delta < Delta < TE"):
P.Shell.free(2000, 12, 0.06, 0.05, 0.09)
def test_a_table_of_rows_becomes_a_protocol_b0_first_then_each_group_in_order():
"""protocol_from_rows: the one row-grouping used by DiSCo's table and by an uploaded scheme; its refusals."""
b = np.array([1000, 0, 2000, 1000, 0]); dirs = np.array([[1, 0, 0], [0, 0, 0], [0, 2, 0], [0, 0, 1], [0, 0, 0]], float)
group = np.array([0, 0, 1, 0, 0])
p, idx = P.protocol_from_rows(b, dirs, group, lambda g, rows: P.Shell("d12-D24", float(b[rows[0]]), len(rows)), name="t")
assert list(idx) == [1, 4, 0, 3, 2] and p.n_b0 == 2 and [(s.b, s.n_dirs) for s in p.shells] == [(1000.0, 2), (2000.0, 1)]
np.testing.assert_allclose(np.linalg.norm(p.directions, axis=1), 1.0)
np.testing.assert_array_equal(p.directions[:2], [[0, 0, 1], [0, 0, 1]])
with pytest.raises(ValueError, match="b = 0"):
P.protocol_from_rows(b[[0, 2]], dirs[[0, 2]], group[[0, 2]], None, name="t")
with pytest.raises(ValueError, match="zero direction"):
P.protocol_from_rows(b, np.zeros((5, 3)), group, None, name="t")
with pytest.raises(ValueError, match="dirs must be"):
P.protocol_from_rows(b, dirs[:3], group, None, name="t")
def test_a_camino_scheme_becomes_a_protocol_with_every_rows_timing(tmp_path):
"""Two shells with their own delta / Delta at one TE plus a b = 0 row, written as Camino STEJSKALTANNER rows
(|G| in T/m, times in s): one free shell per distinct timing, the rows' own b-values and directions, b = 0 first."""
rows = ["VERSION: STEJSKALTANNER"]
rng = np.random.default_rng(0)
rows.append("0 0 0 0 0.0300 0.0100 0.0800")
for delta, Delta, G in ((0.0100, 0.0300, 0.0400), (0.0150, 0.0400, 0.0500)):
for _ in range(4):
u = rng.normal(size=3); u /= np.linalg.norm(u)
rows.append(f"{u[0]:.6f} {u[1]:.6f} {u[2]:.6f} {G:.4f} {Delta:.4f} {delta:.4f} 0.0800")
path = tmp_path / "t.scheme"; path.write_text("\n".join(rows) + "\n")
p = P.protocol_from_scheme(str(path))
assert p.n_meas == 9 and p.n_b0 == 1 and len(p.shells) == 2 and all(s.free_timing for s in p.shells)
m = P.measurements(p, CFG["shapes"])
assert m.b0[0] and (m.delta[1:5] == 0.0100).all() and (m.delta[5:] == 0.0150).all() and np.allclose(m.TE, 0.08)
assert np.allclose(np.linalg.norm(m.dirs, axis=1), 1.0)
np.testing.assert_allclose(sorted(s.b for s in p.shells), [305, 1409], atol=1) # Stejskal-Tanner b of 40 and 50 mT/m
assert p.shells[0].shape == "d10-D30-TE80"
def test_the_physics_is_the_catalogue_at_the_nearest_field_with_the_tiers_as_switches():
"""3 T is cited; 0.064 T and 11.7 T take the nearest cited field (1.5 T, 7 T) and say so; a tier off leaves that
part out of the tissue; all off is bare (no tissue); the refusals."""
c3 = P.catalogue(3.0); assert c3["catalogue_field"] == 3.0 and set(c3["T2"]) == set(P.CATALOGUE_POOLS) and c3["T2"]["myelin"] < c3["T2"]["intra"]
two = P.catalogue(3.0, ("intra", "extra")); assert set(two["T2"]) == {"intra", "extra"}
with pytest.raises(ValueError, match="no relaxation for the pools"):
P.catalogue(3.0, ("intra", "csf"))
assert P.catalogue(0.064)["catalogue_field"] == 1.5 and P.catalogue(11.7)["catalogue_field"] == 7.0
ph = P.Physics.at(3.0)
assert ph.pools == ("intra", "extra")
tis = ph.tissue()
assert tis.T2 == two["T2"] and tis.T1 == two["T1"] and tis.rho2 == c3["rho2"] and tis.chi_iso == c3["chi_iso"] and tis.chi_aniso == c3["chi_aniso"]
filled = ph.tissue(unseeded=("myelin",)) # the unseeded spec pool takes the first pool's value
assert set(filled.T2) == {"intra", "extra", "myelin"} and filled.T2["myelin"] == two["T2"]["intra"]
assert not ph.bare and "relaxation+contact+field" in ph.label()
no_field = P.Physics.at(7.0, field=False, b0_direction=P.b0_direction(90, 0))
assert no_field.tissue().chi_iso is None and no_field.tissue().chi_aniso == 0.0 and no_field.tissue().rho2 == c3["rho2"]
np.testing.assert_allclose(no_field.b0_direction, (1, 0, 0), atol=1e-12)
bare = P.Physics.at(3.0, relaxation=False, contact=False, field=False)
assert bare.bare and bare.tissue() is None and bare.label() == "bare diffusion"
assert P.tissue_and_scanner(bare) == (None, None) and P.tissue_and_scanner(None) == (None, None)
assert P.tissue_and_scanner(no_field)[1] is None and P.tissue_and_scanner(ph)[1] == 3.0 # the field goes with the field tier
only_relax = P.Physics.at(3.0, contact=False, field=False, T2={"intra": 0.08, "extra": 0.08})
assert only_relax.tissue().T2["intra"] == 0.08 and only_relax.tissue().rho2 is None
with pytest.raises(ValueError, match="tesla"):
P.Physics.at(3000.0)
with pytest.raises(ValueError, match="same pools"):
P.Physics.at(3.0, T2={"intra": 0.05})
with pytest.raises(ValueError, match="per pool"):
P.Physics.at(3.0, T2={"intra": -0.05, "extra": 0.05})
with pytest.raises(ValueError, match="unit vector"):
P.Physics.at(3.0, b0_direction=(0, 0, 2))
np.testing.assert_allclose(np.linalg.norm(P.B0_TRANSVERSE), 1.0)
assert P.Physics.at(3.0).along_z and not no_field.along_z
def test_a_tractogram_sample_keeps_whole_streamlines_in_order():
from dmipy_tract.tractogram import Tractogram
rng = np.random.default_rng(1)
counts = rng.integers(2, 6, size=50)
tg = Tractogram(rng.normal(size=(counts.sum(), 3)), np.concatenate([[0], np.cumsum(counts)]), np.arange(50), np.zeros((50, 2), np.int8))
s = P.sample_tractogram(tg, 10, seed=0)
assert len(s) == 10 and np.all(np.diff(s.seed_index) > 0)
for k, i in enumerate(s.seed_index):
np.testing.assert_array_equal(s[k], tg[int(i)])
assert P.sample_tractogram(tg, 100) is tg
def test_the_gradient_a_shell_needs_and_the_scanners_that_can_play_it():
"""b = 1000 at δ 12 / Δ 24 ms needs 70 mT/m (γ² G² δ² (Δ − δ/3)); the Prisma (80 mT/m) plays it, the Swoop (23
mT/m) does not; DiSCo's b = 13183 shell needs more than either; None means no limit."""
G = P.gradient_needed(1000, 0.012, 0.024)
assert 0.069 < G < 0.071
assert P.limits("siemens_magnetom_prisma_3T").G_max == 0.08 and P.limits("hyperfine_swoop_64mT").G_max < 0.03
p = P.Protocol((P.Shell("d12-D24", 1000, 30),), n_b0=1)
assert P.playable(p, CFG["shapes"], "siemens_magnetom_prisma_3T")[0][-1] and not P.playable(p, CFG["shapes"], "hyperfine_swoop_64mT")[0][-1]
assert P.playable(p, CFG["shapes"], None)[0][5] is None and P.playable(p, CFG["shapes"], None)[0][-1]
disco, _ = D.disco_protocol(CFG)
assert all(r[-1] for r in P.playable(disco, CFG["shapes"], None))
assert not all(r[-1] for r in P.playable(disco, CFG["shapes"], "siemens_magnetom_prisma_3T"))
free = P.Protocol((P.Shell.free(1000, 30, 0.012, 0.024, 0.06),), n_b0=1)
assert abs(P.playable(free, CFG["shapes"], None)[0][4] - G) < 1e-12
def test_the_pair_spread_over_repeated_runs():
M1 = np.zeros((16, 16)); M1[0, 1] = M1[1, 0] = 10; M1[2, 3] = M1[3, 2] = 4
M2 = M1.copy(); M2[0, 1] = M2[1, 0] = 14; M2[2, 3] = M2[3, 2] = 0; M2[4, 5] = M2[5, 4] = 2
sp = P.pair_spread([M1, M2], [dict(pearson_count=0.8), dict(pearson_count=0.9)], key="pearson_count")
assert sp["n"] == 2 and abs(sp["pearson_mean"] - 0.85) < 1e-12 and abs(sp["pearson_std"] - np.std([0.8, 0.9], ddof=1)) < 1e-12
assert sp["pairs_any"] == 3 and sp["pairs_always"] == 1 and sp["mean"][0, 1] == 12 and abs(sp["std"][0, 1] - np.std([10, 14], ddof=1)) < 1e-12
one = P.pair_spread([M1], [dict(pearson_count=0.8)], key="pearson_count")
assert one["pearson_std"] == 0.0 and one["std"].max() == 0.0
def test_floor_stats_are_over_the_voxels_with_signal():
dwi = np.ones((3, 3, 3, 2)); dwi[0, 0, 0] = np.nan
floor = np.arange(27, dtype=float).reshape(3, 3, 3) / 27
res = P.Result(None, None, dwi, floor, None, None, None, None, None, {})
f = P.floor_stats(res)
assert f["n"] == 26 and f["max"] == floor[2, 2, 2] and f["median"] == np.median(floor.ravel()[1:])
def test_the_layout_at_the_pinned_revision_holds_the_configs_classes_and_tiers():
"""The manifest the Space will download: every [shapes] class with the config's timing, and the tiers when the
tissue panel starts on (needs the Hub; skipped offline)."""
import json
hf = pytest.importorskip("huggingface_hub")
d = CFG["data"]
import requests
from huggingface_hub.errors import HfHubHTTPError, LocalEntryNotFoundError, OfflineModeIsEnabled
try:
path = hf.hf_hub_download(d["repo"], f"{d['moments']}/manifest.json", repo_type="dataset", revision=d["revision"])
except (requests.ConnectionError, HfHubHTTPError, LocalEntryNotFoundError, OfflineModeIsEnabled) as e: # offline, or the Hub unreachable
pytest.skip(f"the Hub is not reachable: {e!r}"[:120])
with open(path) as f:
m = json.load(f)
assert m["source"]["manifest_sha256"] == d["source_manifest_sha256"]
missing = [c for c in CFG["shapes"] if c not in m["columns"]]
assert not missing, f"the layout at {d['revision'][:12]} lacks {missing}"
for name, t in CFG["shapes"].items():
enc = m["shapes"][name].get("encoding") or {}
for k in ("delta", "Delta", "TE"):
v = enc.get(k); v = v[0] if isinstance(v, list) else v
if v is not None:
assert abs(v - t[k]) < 1e-9, (name, k, v, t[k])
if CFG["physics"]["default_on"]:
assert m.get("tiers"), "the tissue panel starts on but the layout has no tiers"
def test_the_tensor_fit_recovers_a_synthetic_tensor_and_refuses_too_few_rows():
"""A voxel with D = diag(1.7, 0.3, 0.3) um^2/ms at b = 0 and 1000 over 30 directions: MD 0.767, FA 0.799; a voxel
outside the mask is NaN; fewer than six directions below b_max refuse."""
p = P.Protocol((P.Shell("d12-D24", 1000, 30), P.Shell("d8-D20", 3000, 12)), n_b0=1)
m = P.measurements(p, CFG["shapes"])
D = np.diag([1.7e-3, 0.3e-3, 0.3e-3])
S = np.exp(-m.bvals * np.einsum("ni,ij,nj->n", m.dirs, D, m.dirs))
dwi = np.tile(S, (2, 2, 1, 1)); mask = np.ones((2, 2, 1), bool); mask[0, 0, 0] = False
md, fa = P.dti(dwi, m, mask)
assert np.isnan(md[0, 0, 0]) and abs(md[1, 1, 0] - 0.7667) < 1e-3 and abs(fa[1, 1, 0] - 0.7990) < 1e-3
with pytest.raises(ValueError, match="six or more"):
P.dti(dwi, m, mask, b_max=500)
def test_layer_differences_are_per_shell_between_consecutive_layers():
p = P.Protocol((P.Shell("d12-D24", 1000, 6), P.Shell("d8-D20", 3000, 6)), n_b0=1)
m = P.measurements(p, CFG["shapes"])
a = np.ones((2, 1, 1, 13)); b = a.copy(); b[..., 1:7] += 0.01; b[..., 7:] += 0.05; c = b.copy(); c[..., 7:] -= 0.02
rows = P.layer_differences([("bare", a), ("+ relaxation", b), ("A", c)], m, np.ones((2, 1, 1), bool))
assert [(r[0], r[1], r[2]) for r in rows] == [("bare", "+ relaxation", 1000.0), ("bare", "+ relaxation", 3000.0), ("+ relaxation", "A", 1000.0), ("+ relaxation", "A", 3000.0)]
np.testing.assert_allclose([r[3] for r in rows], [0.01, 0.05, 0.0, 0.02], atol=1e-12)
def test_the_ingredients_are_the_layouts_tier_maps_in_the_pages_units():
"""The ingredient maps on a stand-in layout: the intra fraction as is, the wall contact as +l in micrometres from
the stored -l, the survival and the field spread only with their tiers on, the maps asked at the run's tissue,
field and direction on the layout's backend."""
calls = []
class _M:
manifest = {"tiers": {"D_walk": 6e-10}}
def tier_maps(self, shape, tissue=None, scanner=None, *, backend, resident, b0_direction):
calls.append((shape, tissue is not None, scanner, backend, resident, tuple(b0_direction)))
return dict(pool={"intra": np.full((2, 1, 1), 0.4), "extra": np.full((2, 1, 1), 0.6)}, contact=np.array([[[-2e-5]], [[np.nan]]]),
contact_weight=np.full((2, 1, 1), 0.9), phase=np.zeros((2, 1, 1)), phase_std=np.full((2, 1, 1), 0.1))
lay = D.Layout.__new__(D.Layout); lay.moments = _M(); lay.tiers = True; lay.backend = "torch"; lay.unseeded = ("myelin",)
meas = P.measurements(P.Protocol((P.Shell("d12-D24", 1000, 6),), n_b0=1), P.config()["shapes"])
ph = P.Physics.at(3.0)
out = D.Layout.ingredients(lay, meas, ph)
assert out["D_walk"] == 6e-10 and out["intra_fraction"][0, 0, 0] == 0.4
np.testing.assert_allclose(out["wall_contact_um"].ravel(), [20.0, np.nan])
assert out["contact_survival"][0, 0, 0] == 0.9 and out["field_rad"][0, 0, 0] == 0.1
assert calls[-1] == ("d12-D24", True, 3.0, "torch", True, (0.0, 0.0, 1.0))
bare = D.Layout.ingredients(lay, meas, None)
assert bare["contact_survival"] is None and bare["field_rad"] is None and bare["wall_contact_um"][0, 0, 0] == 20.0
assert calls[-1][1:3] == (False, None)
lay.tiers = False
assert D.Layout.ingredients(lay, meas, ph) is None
def test_a_machine_reaches_the_layout_as_a_delivery_at_the_placement():
"""On a stand-in layout: the ideal scanner asks the image for the commanded acquisition (no delivery); a machine
asks the layout's delivery for every class at the machine's catalogue entry and the grid centred at the physics'
offset, and hands it to that class's image; a machine with its own gradient on a layout without background moments,
and with a transmit law on one without RF schedules, is refused by name."""
from dmipy_sim.phantom import Grid
seen = []
class _M:
grid = Grid(shape=(2, 1, 1), voxel_size_m=(25e-6,) * 3, origin_m=(0.0, 0.0, 0.0))
background = None
manifest = {"shapes": {"d12-D24": {}, "d8-D20": {}}}
def delivery(self, shape, b, dirs, scanner, grid):
seen.append(("delivery", shape, scanner.name, tuple(np.round(np.asarray(grid.isocenter_m) - np.asarray(self.grid.isocenter_m), 6))))
return ("delivered", shape)
def image(self, shape, b, dirs, *, backend, resident, tissue, scanner, b0_direction, delivered=None):
seen.append(("image", shape, delivered, scanner, tuple(b0_direction)))
return np.ones((2, 1, 1, len(b))), np.zeros((2, 1, 1))
lay = D.Layout.__new__(D.Layout); lay.moments = _M(); lay.tiers = True; lay.backend = "jax"; lay.unseeded = ("myelin",)
lay.M0 = np.ones((2, 1, 1)); lay.mask = np.ones((2, 1, 1), bool); lay.pools = ("intra", "extra"); lay.shapes = CFG["shapes"]
meas = P.measurements(P.Protocol((P.Shell("d12-D24", 1000, 3), P.Shell("d8-D20", 2000, 3)), n_b0=1), CFG["shapes"])
D.Layout.replay(lay, meas, P.Physics.at(3.0))
assert [s[2] for s in seen if s[0] == "image"] == [None, None]
seen.clear()
ph = P.Physics.at(3.0, scanner="siemens_magnetom_prisma_3T", offset_m=(0.0, 0.0, 0.05))
D.Layout.validate(lay, P.Protocol((P.Shell("d12-D24", 1000, 3),), n_b0=1), ph)
D.Layout.replay(lay, meas, ph)
assert [s for s in seen if s[0] == "delivery"] == [("delivery", "d12-D24", "siemens_magnetom_prisma_3T", (0.0, 0.0, -0.05)),
("delivery", "d8-D20", "siemens_magnetom_prisma_3T", (0.0, 0.0, -0.05))]
assert [s[2] for s in seen if s[0] == "image"] == [("delivered", "d12-D24"), ("delivered", "d8-D20")]
swoop = P.Physics.at(0.064, scanner="hyperfine_swoop_64mT", b0_direction=(0.0, 1.0, 0.0))
with pytest.raises(ValueError, match="background moments"):
D.Layout.validate(lay, P.Protocol((P.Shell("d12-D24", 100, 3),), n_b0=1), swoop)
lay.moments.background = {"amplitude": 5e-3}
with pytest.raises(ValueError, match="RF schedules"):
D.Layout.validate(lay, P.Protocol((P.Shell("d12-D24", 100, 3),), n_b0=1), swoop)