"""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)