File size: 18,699 Bytes
6bc44d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
"""The page's acquisition choice without a browser: DiSCo's table, a config preset, the shell rows (a stored class in
demo mode, the row's own delta / Delta / TE in full mode), a scheme upload only in full mode; an unknown choice
refused (no Gradio needed: the mapping lives beside the page, not in a widget)."""
import types

import numpy as np
import pytest

from space import app as A
from space import pipeline as P
from space.sources import disco as D

CFG = P.config()
S = D.Disco
ROWS = [True, "d12-D24", 1000, 30, 10.0, 20.0, 60.0, True, "d8-D20", 3000, 45, 8.0, 20.0, 60.0,
        False, "d17-D30", 3000, 90, 17.0, 30.0, 60.0, False, "d17-D30", 6000, 60, 17.0, 30.0, 60.0]


def test_the_three_kinds_of_acquisition():
    disco = A._protocol_from_inputs(S, CFG, "DiSCo 364", 5, *ROWS)
    assert disco.name == "DiSCo 364" and disco.n_meas == 364 and disco.n_b0 == 4        # the table's own, not the slider's
    name = next(iter(CFG["presets"]))
    preset = A._protocol_from_inputs(S, CFG, name, 5, *ROWS)
    assert preset.name == name and preset.n_b0 == CFG["presets"][name]["n_b0"]
    assert [s.shape for s in preset.shells] == [s["shape"] for s in CFG["presets"][name]["shells"]]
    custom = A._protocol_from_inputs(S, CFG, A.CUSTOM, 2, *ROWS)
    assert custom.n_b0 == 2 and [(s.shape, s.b, s.n_dirs) for s in custom.shells] == [("d12-D24", 1000.0, 30), ("d8-D20", 3000.0, 45)]
    assert not any(s.free_timing for s in custom.shells)
    with pytest.raises(ValueError, match="unknown acquisition"):
        A._protocol_from_inputs(S, CFG, "something else", 2, *ROWS)
    with pytest.raises(ValueError, match="at least one shell"):
        A._protocol_from_inputs(S, CFG, A.CUSTOM, 2, *([False] + ROWS[1:7] + [False] + ROWS[8:14] + ROWS[14:]))


def test_full_mode_takes_the_rows_own_timing_and_demo_mode_refuses_the_upload():
    full = A._protocol_from_inputs(S, CFG, A.CUSTOM, 2, *ROWS, full=True)
    assert all(s.free_timing for s in full.shells)
    assert [(s.delta, s.Delta, s.TE) for s in full.shells] == [(0.010, 0.020, 0.060), (0.008, 0.020, 0.060)]
    with pytest.raises(ValueError, match="full mode"):
        A._protocol_from_inputs(S, CFG, A.UPLOADED, 2, *ROWS, scheme="x.scheme")
    with pytest.raises(ValueError, match="upload"):
        A._protocol_from_inputs(S, CFG, A.UPLOADED, 2, *ROWS, scheme=None, full=True)


def _values(on=True, field_T=3.0, **over):
    nums = S.catalogue_numbers(CFG, field_T)[:-1]
    v = dict(zip(D.physics_fields(CFG), [on, field_T, next(iter(D.B0_MODES)), 0, 0] + nums + [True, True, True, 7.9, next(iter(D.OFFSET_AXES))]))
    v.update(over)
    return v


def test_the_physics_panel_is_a_physics_in_si_or_none_when_off():
    nums = S.catalogue_numbers(CFG, 3.0)
    assert nums[-1].startswith("catalogue values at 3 T") and S.catalogue_numbers(CFG, 0.064)[-1].startswith("the catalogue has no cited")
    ph = S.physics_from(CFG, _values(field=False))
    assert ph.field_T == 3.0 and ph.T2["intra"] == nums[0] * 1e-3 and ph.rho2 == nums[4] * 1e-6 and not ph.field and ph.relaxation and ph.pools == D.pools(CFG)
    assert S.physics_from(CFG, _values(on=False)) is None
    free = S.physics_from(CFG, _values(field_T=7.0, b0_mode=D.FREE_B0, theta=90, phi=90))
    assert abs(free.b0_direction[1] - 1.0) < 1e-12
    with pytest.raises(ValueError, match="17 inputs"):
        A.physics_values(S, CFG, 1, 2, 3)


def test_a_knob_changes_one_thing_of_a():
    """Every knob maps to B = A with that one change: the field takes the catalogue tissue with it, a tier goes off,
    the tissue goes off, the noise changes, every shell is retimed; A's other settings survive; a panel knob on an
    A whose panel is off is refused (B would differ in two things)."""
    v = _values()
    prot = A._protocol_from_inputs(S, CFG, A.CUSTOM, 2, *ROWS)
    ks = S.knobs(CFG)
    assert ks[A.NO_KNOB] is None and len(ks) == 1 + 5 + 2 + 3 + 1 + 3 + len(CFG["shapes"])
    for name, change in ks.items():
        if change is None:
            continue
        pb, on, snr, vb = S.apply_knob(CFG, change, prot, True, 30.0, v)
        diffs = [k for k in D.physics_fields(CFG) if vb[k] != v[k]]
        kind = change[0]
        if kind == "field":
            assert vb["field_T"] == change[1] and [vb[k] for k in D.tissue_numbers(CFG)] == S.catalogue_numbers(CFG, change[1])[:-1] and pb is prot
        elif kind == "b0":
            assert diffs == ["b0_mode"] or (diffs == [] and change[1] == v["b0_mode"])
        elif kind == "tier":
            assert diffs == [change[1]] and vb[change[1]] is False
        elif kind == "bare":
            assert diffs == ["on"] and S.physics_from(CFG, vb) is None
        elif kind == "snr":
            assert diffs == [] and (on, snr) == ((False, 30.0) if change[1] is None else (True, change[1]))
        elif kind == "shape":
            assert diffs == [] and all(s.shape == change[1] for s in pb.shells) and [s.b for s in pb.shells] == [s.b for s in prot.shells]
        if kind in ("field", "b0", "tier"):
            with pytest.raises(ValueError, match="which is off for A"):
                S.apply_knob(CFG, change, prot, True, 30.0, _values(on=False))
    with pytest.raises(ValueError, match="unknown knob"):
        S.apply_knob(CFG, ("what", 1), prot, True, 30.0, v)


def test_compare_is_symmetric_in_its_pairs_and_zero_for_the_same_run():
    M = np.zeros((16, 16)); M[0, 1] = M[1, 0] = 5; M[2, 3] = M[3, 2] = 2
    N = M.copy(); N[4, 5] = N[5, 4] = 1; N[2, 3] = N[3, 2] = 0
    a = types.SimpleNamespace(matrix=M, score=dict(pearson_count=0.9, pearson_area=0.8))
    b = types.SimpleNamespace(matrix=N, score=dict(pearson_count=0.85, pearson_area=0.8))
    c = D.compare(a, b)
    assert c["only_a"] == 1 and c["only_b"] == 1 and abs(c["delta_count"] + 0.05) < 1e-12 and c["delta_area"] == 0
    same = D.compare(a, a)
    assert same["only_a"] == 0 and same["pearson_ab"] == 1.0


PRISMA = "Siemens Prisma 3 T"
SWOOP = next(k for k in P.machines(CFG) if k.startswith("Hyperfine Swoop"))


def test_the_gradient_table_names_the_shell_the_scanner_cannot_play():
    prot = A._protocol_from_inputs(S, CFG, A.CUSTOM, 2, *ROWS)
    table, ok = A.gradient_text(CFG, prot, CFG["shapes"], PRISMA)
    assert "70 mT/m" in table and "194 mT/m" in table and "cannot play" in table and not ok      # shell 2: b 3000 at δ 8 / Δ 20
    assert A.gradient_text(CFG, prot, CFG["shapes"], A.IDEAL)[1]
    with pytest.raises(ValueError, match="no scanner"):
        A.gradient_text(CFG, prot, CFG["shapes"], "a magnet nobody built")


def test_the_scanner_menu_names_every_term_and_where_it_comes_from():
    """The term table of each machine: the Swoop applies every term (its field law measured, its nonlinearity's
    diagonal derived from a measurement), the cylinders their class-model nonlinearity and the Maxwell term, with
    their own gradient and transmit map absent and said so; the ideal scanner none."""
    assert list(P.machines(CFG).values()) == ["hyperfine_swoop_64mT", "siemens_magnetom_prisma_3T", "siemens_magnetom_terra_7T"]
    sw = {t: (on, src) for t, on, src in P.scanner_terms("hyperfine_swoop_64mT")}
    assert all(on for on, _ in sw.values()) and "0.064 T along (0, 1, 0)" in sw["field strength and direction"][1]
    assert "measured" in sw["transmit scale B1"][1] and "derived from a measurement" in sw["gradient nonlinearity L"][1]
    pr = {t: (on, src) for t, on, src in P.scanner_terms("siemens_magnetom_prisma_3T")}
    assert pr["gradient nonlinearity L"][0] and "inferred from the class" in pr["gradient nonlinearity L"][1]
    assert not pr["field law: its gradient g0 (and its value, a phase per voxel)"][0] and "absent" in pr["field law: its gradient g0 (and its value, a phase per voxel)"][1]
    assert not pr["transmit scale B1"][0] and "0.7-1.2" in pr["transmit scale B1"][1] and pr["Maxwell (concomitant) gradient"][0]
    assert "ideal" in P.scanner_text(CFG, A.IDEAL) and "| transmit scale B1 | no |" in P.scanner_text(CFG, PRISMA)


def test_a_machine_sets_the_field_its_direction_and_the_placement():
    """DiSCo's physics on a machine: the machine's field and direction whatever the panel says, the phantom's centre at
    the panel's distance along its axis, the bare diffusion on the machine when the tissue is off; the ideal scanner
    keeps the panel's field and no placement."""
    v = dict(_values(), field_T=1.5, offset_cm=7.9, offset_axis="A-P (y)")
    ph = S.physics_from(CFG, v, scanner="hyperfine_swoop_64mT")
    assert ph.field_T == 0.064 and ph.b0_direction == (0.0, 1.0, 0.0) and np.allclose(ph.offset_m, (0.0, 0.079, 0.0))
    assert ph.scanner == "hyperfine_swoop_64mT" and "hyperfine_swoop_64mT" in ph.label()
    off = S.physics_from(CFG, dict(v, on=False), scanner="siemens_magnetom_prisma_3T")
    assert off.bare and off.scanner == "siemens_magnetom_prisma_3T" and off.field_T == 3.0
    assert S.physics_from(CFG, dict(v, on=False)) is None
    ideal = S.physics_from(CFG, v)
    assert ideal.field_T == 1.5 and ideal.scanner is None and ideal.offset_m == (0.0, 0.0, 0.0)
    with pytest.raises(ValueError, match="T magnet"):
        P.Physics.at(3.0, scanner="hyperfine_swoop_64mT")


class _Demo(D.Disco):
    """A demo source without replay data: validates as a tiered layout would."""
    mode = "demo"
    tiers = True

    def validate(self, protocol, physics):
        if physics and not physics.bare and not self.tiers:
            raise ValueError("bare only")
        if physics and set(physics.pools) != set(self.pools):
            raise ValueError("pools")
        return P.measurements(protocol, self.shapes)


def test_the_runs_are_planned_and_refused_before_any_work():
    """plan_runs: A alone, A + B with a knob, the scanner's refusal naming the run, the source's refusal, and the
    upload refused in demo mode."""
    src = _Demo(CFG)
    v = _values()
    runs = A.plan_runs(CFG, src, A.CUSTOM, 2, True, 30.0, None, A.NO_KNOB, A.IDEAL, v, ROWS)
    assert [r[0] for r in runs] == ["A"] and runs[0][4].field_T == 3.0
    runs = A.plan_runs(CFG, src, A.CUSTOM, 2, True, 30.0, None, "SNR → 10", A.IDEAL, v, ROWS)
    assert [r[0] for r in runs] == ["A", "B"] and runs[1][3] == 10.0 and runs[1][1] is runs[0][1]
    with pytest.raises(ValueError, match="cannot play run A"):
        A.plan_runs(CFG, src, A.CUSTOM, 2, True, 30.0, None, A.NO_KNOB, SWOOP, v, ROWS)
    with pytest.raises(ValueError, match="cannot play run B"):
        A.plan_runs(CFG, src, "clinical b1000 x 30", 1, True, 30.0, None, "every shell's pulse timing → d8-D20 (Connectome 2.0 δ 8 / Δ 20 ms)", PRISMA, v, ROWS)
    with pytest.raises(ValueError, match="fixes the field"):
        A.plan_runs(CFG, src, "clinical b1000 x 30", 1, True, 30.0, None, "field → 7 T (catalogue tissue at that field)", PRISMA, v, ROWS)
    runs = A.plan_runs(CFG, src, "clinical b1000 x 30", 1, True, 30.0, None, A.NO_KNOB, PRISMA, v, ROWS)
    assert runs[0][4].scanner == "siemens_magnetom_prisma_3T" and runs[0][4].field_T == 3.0
    src.tiers = False
    with pytest.raises(ValueError, match="bare only"):
        A.plan_runs(CFG, src, A.CUSTOM, 2, True, 30.0, None, A.NO_KNOB, A.IDEAL, v, ROWS)
    with pytest.raises(ValueError, match="full mode"):
        A.plan_runs(CFG, src, A.UPLOADED, 2, True, 30.0, "x.scheme", A.NO_KNOB, A.IDEAL, _values(on=False), ROWS)
    with pytest.raises(ValueError, match="unknown knob"):
        A.plan_runs(CFG, src, A.CUSTOM, 2, True, 30.0, None, "twist", A.IDEAL, v, ROWS)


def test_disco_prepares_nothing_so_the_lookup_and_the_worker_have_nothing_to_do(monkeypatch):
    """DiSCo has no share outside the device: prepare_runs gives None for every entry, the worker computes nothing and
    says nothing, the payload has no response seconds and the reservation has no response part."""
    src = _Demo(CFG)
    monkeypatch.setattr(A, "_load", lambda: dict(error=None, source=src, cfg=CFG))
    v = _values()
    args = [A.CUSTOM, 2, True, 30.0, 2, 30.0, 0.5, 0, None, "SNR → 10", A.IDEAL, 1, True, *v.values(), *ROWS]
    prepared = A.prepare_runs(*args)
    assert prepared["runs"] == {"A": None, "B": None} and all(p is None for p in prepared["ladder"])
    runs = A.plan_runs(CFG, src, A.CUSTOM, 2, True, 30.0, None, "SNR → 10", A.IDEAL, v, ROWS)
    gen = A._fill(src, runs, True, prepared)
    with pytest.raises(StopIteration) as stop:
        next(gen)
    assert stop.value.value == (prepared, {}, None)
    assert src.responses([(m, ph) for _, m, ph in A.response_entries(src, runs, True)]) == []


def test_the_estimated_seconds_grow_with_the_run_and_stay_in_the_pools_window():
    v = list(_values().values())
    one = A.estimated_seconds("DiSCo 364", 1, True, 30, 2, 30.0, 0.5, 0, None, A.NO_KNOB, A.IDEAL, 1, False, *v, *ROWS)
    two = A.estimated_seconds("DiSCo 364", 1, True, 30, 2, 30.0, 0.5, 0, None, "SNR → 10", A.IDEAL, 1, False, *v, *ROWS)
    keys = A.estimated_seconds("DiSCo 364", 1, True, 30, 2, 30.0, 0.5, 0, None, A.NO_KNOB, A.IDEAL, 8, False, *v, *ROWS)
    ladder = A.estimated_seconds("DiSCo 364", 1, True, 30, 2, 30.0, 0.5, 0, None, A.NO_KNOB, A.IDEAL, 1, True, *v, *ROWS)
    small = A.estimated_seconds("clinical b1000 x 30", 1, True, 30, 2, 30.0, 0.5, 0, None, A.NO_KNOB, A.IDEAL, 1, False, *v, *ROWS)
    assert 30 <= small < one < two <= 480 and one < keys <= 480 and one < ladder <= 480
    both = A.estimated_seconds("DiSCo 364", 1, True, 30, 2, 30.0, 0.5, 0, None, "SNR → 10", A.IDEAL, 1, True, *v, *ROWS)
    assert one < ladder < both <= 120          # DiSCo alone, with the ladder, and A + B + ladder all fit a logged-out visitor's quota
    assert A.estimated_seconds("nonsense", 1, True, 30, 2, 30.0, 0.5, 0, None, A.NO_KNOB, A.IDEAL, 1, True, *v, *ROWS) == 480
    text = A.gpu_seconds_text("DiSCo 364", 1, True, 30, 2, 30.0, 0.5, 0, None, A.NO_KNOB, A.IDEAL, 1, True, *v, *ROWS)
    assert f"reserves {ladder} s" in text and "logged out" in text.split("started by")[1].split(".")[0]
    assert len(A.OUTPUTS) == 30


def test_the_explorer_draws_layers_differences_and_metrics_from_its_state():
    """A two-layer explorer state (bare, A) with B: every mode and quantity returns an image; the table has one row
    per layer step per shell plus the floor; an empty state draws nothing."""
    p = P.Protocol((P.Shell("d12-D24", 1000, 12),), n_b0=1)
    m = P.measurements(p, CFG["shapes"])
    rng = np.random.default_rng(0)
    bare = rng.uniform(0.3, 1.0, (4, 4, 2, 13)).astype(np.float16); a = (bare * 0.98).astype(np.float16); b = (bare * 0.97).astype(np.float16)
    md = rng.random((4, 4, 2)).astype(np.float16); fa = rng.random((4, 4, 2)).astype(np.float16)
    ex = dict(layers=[("bare diffusion", bare), ("A = bare + field", a), ("B = A with field → 7 T", b)], metrics={k: (md, fa) for k in ("bare diffusion", "A = bare + field", "B = A with field → 7 T")},
              floor=np.full((4, 4, 2), 0.01, np.float32), meas=m, mask=np.ones((4, 4, 2), bool),
              ingredients=S.ingredient_layers(dict(intra_fraction=rng.random((4, 4, 2)), wall_contact_um=rng.random((4, 4, 2)), contact_survival=None, field_rad=None, D_walk=6e-10)),
              differences=P.layer_differences([("bare diffusion", bare.astype(np.float32)), ("A", a.astype(np.float32)), ("B", b.astype(np.float32))], m, np.ones((4, 4, 2), bool)),
              snr=None, physics=None)
    for mode in A.EXPLORE_MODES:
        for metric in A.METRICS:
            img = A.explore(ex, "A = bare + field", mode, metric, 1, 5)
            assert img is not None and img.size[0] > 100, (mode, metric)
    assert A.explore(ex, "bare diffusion", "minus the previous layer", A.METRICS[0], 0, 0) is not None
    pool, contact, fld = A.ingredient_views(ex, 1)
    assert pool is not None and contact is not None and fld is None
    rows = A.layer_table(ex)
    assert len(rows) == 2 + 1 and rows[-1][0] == "replay floor"
    assert A._layer_labels(P.Physics.at(3.0, contact=False), "SNR → 10") == ("A = bare + relaxation + field", "B = A with SNR → 10")
    assert A._layer_labels(None, A.NO_KNOB)[0] == "A = bare diffusion"
    assert A.explore(None, "A", "signal", A.METRICS[0], 0, 0) is None and A.ingredient_views(None, 0) == (None, None, None) and A.layer_table(None) == []


def test_the_run_button_chains_the_devices_texts_into_the_headline_then_the_pages_outputs(monkeypatch):
    """run_pipeline: every (text, fraction) of the compute function becomes a headline-only update (the progress bar
    following), the payload goes to present once the device part has ended, and its outputs are the last yield."""
    pytest.importorskip("gradio")
    kept = []
    source = types.SimpleNamespace(keep=kept.append)
    monkeypatch.setattr(A, "_load", lambda: dict(error=None, load_seconds=1.0, regions=None, source=source))
    monkeypatch.setattr(A, "prepare_runs", lambda *args: dict(runs={"A": args}, ladder=None))
    seen = []
    monkeypatch.setattr(A, "present", lambda payload, state: seen.append(payload) or ("drawn",) * len(A.OUTPUTS))

    def fake_compute(prepared, *args):
        assert prepared["runs"]["A"] == args
        yield ("**A · 1/5 replay** …", 0.1)
        yield ("**A · 2/5 noise** …", 0.3)
        yield dict(results={}, compute_seconds=12.0, kernels={"key": "computed on the worker"})
    bars = []
    out = list(A.run_pipeline(fake_compute, "x", 1, progress=lambda f, desc: bars.append((f, desc))))
    assert "starting" in out[0][1] and [o[1] for o in out[1:3]] == ["**A · 1/5 replay** …", "**A · 2/5 noise** …"] and all(len(o) == len(A.OUTPUTS) for o in out[:4])
    assert "drawing" in out[3][1] and out[4] == ("drawn",) * len(A.OUTPUTS)
    assert bars == [(0.0, "starting"), (0.1, "A · 1/5 replay"), (0.3, "A · 2/5 noise")] and seen[0]["compute_seconds"] == 12.0
    assert kept == [{"key": "computed on the worker"}]          # the page keeps what the worker computed, before drawing


def test_the_payload_carries_float32_volumes():
    """_light: the DWI and the clean replay cross the fork as float32, everything else of the result as it was."""
    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((2, 2, 2, 7)); floor = np.zeros((2, 2, 2))
    res = P.Result(p, m, dwi, floor, np.ones((2, 2, 2)), np.zeros((2, 2, 2, 45)), None, None, np.zeros((16, 16)), {}, clean=dwi * 0.5, snr=30.0)
    light = A._light(res)
    assert light.dwi.dtype == np.float32 and light.clean.dtype == np.float32 and res.dwi.dtype == np.float64
    np.testing.assert_allclose(light.dwi, dwi, rtol=1e-6); assert light.snr == 30.0 and light.meas is m
    assert A._light(P.Result(p, m, dwi, floor, None, None, None, None, None, {})).clean is None