File size: 29,831 Bytes
6bc44d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b11d6eb
6bc44d6
 
 
 
 
b11d6eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6bc44d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b11d6eb
 
 
6bc44d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b11d6eb
 
 
6bc44d6
b11d6eb
 
6bc44d6
 
 
 
 
b11d6eb
6bc44d6
 
b11d6eb
 
 
 
6bc44d6
b11d6eb
 
 
 
 
6bc44d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b11d6eb
6bc44d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b11d6eb
 
 
 
 
 
 
6bc44d6
b11d6eb
 
 
 
 
6bc44d6
 
b11d6eb
 
 
 
 
 
 
 
 
 
 
 
6bc44d6
b11d6eb
 
 
6bc44d6
 
 
 
 
 
 
b11d6eb
 
6bc44d6
b11d6eb
6bc44d6
b11d6eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
"""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 preset["shape"] in shapes, name
        assert P.preset_protocol(CFG, name).name == name
    for s in CFG["shapes"].values():
        assert 0 < s["delta"] < s["Delta"] < s["TE"]


#: the paper's flagship acquisitions (experiments/disco/disco_acq.py): b = 0 rows, shells (b, directions), class
FLAGSHIP = {"clinical b1000 x 30": (3, [(1000, 30)], "d12-D24"), "research 3-shell x 90": (9, [(1000, 90), (2000, 90), (3000, 90)], "d17-D30"),
            "Connectome 2.0 b6000 x 60": (6, [(6000, 60)], "d8-D20"), "cosine OGSE 50 Hz b1000 x 60": (6, [(1000, 60)], "cos-f50-T20")}


def test_the_presets_are_the_papers_flagship_tables():
    """Every preset is its stored gradient table, the paper's own (data/protocols/, the same files the paper replays):
    its b = 0 rows first, then each shell's directions, every row on the preset's class."""
    assert set(FLAGSHIP) <= set(CFG["presets"])
    for name, (n_b0, shells, shape) in FLAGSHIP.items():
        p = P.preset_protocol(CFG, name)
        assert p.n_b0 == n_b0 and [(s.b, s.n_dirs) for s in p.shells] == shells and {s.shape for s in p.shells} == {shape}
        m = P.measurements(p, CFG["shapes"])
        np.testing.assert_allclose(np.linalg.norm(m.dirs, axis=1), 1.0, atol=1e-6)


def test_each_class_plays_its_one_waveform():
    """A class's rows are its one shape scaled to each row's b along each row's direction: the b of every row is its
    own; a square PGSE class needs the textbook amplitude; the OGSE class keeps the 200 T/m/s edges of its b = 1000
    build at every b (the shape is fixed), a b = 0 row plays nothing."""
    from space import classes as C
    u = np.array([[1.0, 0.0, 0.0], [0.0, 0.6, 0.8], [0.0, 0.0, 1.0]])
    for name, sh in CFG["shapes"].items():
        if C.kind(sh) == "tensor":                                     # its rows are level matrices: tests/test_tensor.py
            continue
        b = np.array([500.0, 1000.0, 0.0]) if C.kind(sh) == "ogse" else np.array([1000.0, 3000.0, 0.0])
        seq = C.play(sh, b * 1e6, u)
        np.testing.assert_allclose(seq.b(), b * 1e6, rtol=1e-5, atol=1e3)
        G = np.asarray(seq.G, np.float64)
        assert not G[2].any()
        if C.kind(sh) == "pgse":
            assert abs(C.amplitude(sh, 1000) / P.gradient_needed(1000, sh["delta"], sh["Delta"]) - 1) < 0.02, name
    sh = CFG["shapes"]["cos-f50-T20"]
    G = np.abs(np.asarray(C.play(sh, [0.5e9, 1e9], u[:2]).G, np.float64)).max(axis=2)
    np.testing.assert_allclose(G[0] / G[0].max(), G[1] / G[1].max(), atol=1e-6)        # one shape at both b
    assert 0.2 < C.amplitude(sh, 1000) < 0.3                                             # 0.25 T/m: a 50 Hz cosine is weak in b


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 False
    monkeypatch.setenv("DISCO_BACKEND", "torch"); monkeypatch.setenv("DISCO_RESIDENT", "1")
    assert P.backend(CFG) == "torch" and P.resident(CFG) is True
    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 is a stored partition whose terms are that class's
    waveform (its template fits them), and the tiers when the tissue panel starts on (needs the Hub; skipped offline)."""
    import hashlib
    import json
    from dmipy_replay.replay.time_segments import Partition, Shape
    from space import classes as C
    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['layout']}/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])
    raw = open(path, "rb").read(); m = json.loads(raw)
    if d.get("manifest_sha256"):
        assert hashlib.sha256(raw).hexdigest() == d["manifest_sha256"]
    missing = [c for c in CFG["shapes"] if c not in m["partitions"]]
    assert not missing, f"the layout at {d['revision'][:12]} lacks {missing}"
    for name, sh in CFG["shapes"].items():
        p = m["partitions"][name]
        part = Partition(name, tuple(tuple(t["t"]) for t in p["segments"]), tuple(int(t["gate"]) for t in p["segments"]),
                         tuple(Shape.from_meta(t["shape"], name) for t in p["segments"]))
        part.levels(C.template(sh))                                       # refused by name unless the waveform is the class's
    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.layout = _M(); lay.tiers = True; lay.backend = "torch"; lay.unseeded = ("myelin",); lay.resident = True
    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 (the class's waveform, no
    delivery); a machine asks the layout's delivery for every class with the grid centred at the physics' offset, and
    hands it to that class's image; the image's normalised signal is scaled by every voxel's walker weight; a machine
    with its own gradient on a class without background vectors, and any machine on a shaped class (the OGSE), is
    refused by name."""
    from dmipy_replay.phantom import Grid
    from space import classes as C
    seen = []

    class _Part:
        def __init__(self, constant):
            self.is_constant = constant

    class _M:
        grid = Grid(shape=(2, 1, 1), voxel_size_m=(25e-6,) * 3, origin_m=(0.0, 0.0, 0.0))
        partitions = {n: _Part(C.kind(sh) != "ogse") for n, sh in CFG["shapes"].items()}
        manifest = {"partitions": {n: {"group": "g0"} for n in CFG["shapes"]}, "groups": {"g0": {}}}

        def delivery(self, seq, scanner, grid, *, partition):
            seen.append(("delivery", partition, scanner.name, tuple(np.round(np.asarray(grid.isocenter_m) - np.asarray(self.grid.isocenter_m), 6))))
            return ("delivered", partition)

        def image(self, seq, *, partition, backend, resident, tissue, scanner, b0_direction, delivered=None):
            seen.append(("image", partition, delivered, scanner, tuple(b0_direction), np.asarray(seq.G).shape[0]))
            return np.full((2, 1, 1, np.asarray(seq.G).shape[0]), 0.5), np.zeros((2, 1, 1))
    lay = D.Layout.__new__(D.Layout); lay.layout = _M(); lay.tiers = True; lay.backend = "jax"; lay.unseeded = ("myelin",); lay.resident = False
    lay.M0 = np.array([2.0, 4.0]).reshape(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"])
    dwi, floor, factor, _ = D.Layout.replay(lay, meas, P.Physics.at(3.0))
    assert [s[2] for s in seen if s[0] == "image"] == [None, None] and [s[5] for s in seen if s[0] == "image"] == [4, 3]
    np.testing.assert_allclose(dwi, 1.0); np.testing.assert_allclose(factor.ravel(), [0.25, 0.5])   # S0 = 0.5 W over the fullest voxel's W
    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")]
    with pytest.raises(ValueError, match="shaped waveforms"):
        D.Layout.validate(lay, P.Protocol((P.Shell("cos-f50-T20", 1000, 3),), n_b0=1), ph)
    swoop = P.Physics.at(0.064, scanner="hyperfine_swoop_64mT", b0_direction=(0.0, 1.0, 0.0))
    with pytest.raises(ValueError, match="background vectors"):
        D.Layout.validate(lay, P.Protocol((P.Shell("d12-D24", 100, 3),), n_b0=1), swoop)
    lay.layout.manifest["groups"]["g0"]["background"] = {"column": "bg_g0.npy"}
    D.Layout.validate(lay, P.Protocol((P.Shell("d12-D24", 100, 3),), n_b0=1), swoop)


def test_each_preset_starts_at_its_own_snr():
    """The b-tensor preset starts at SNR 100 (µFA is noise-limited); every other preset at ``[noise] snr``."""
    for name, p in CFG["presets"].items():
        assert P.preset_snr(CFG, name) == (100.0 if "tensor" in p else float(CFG["noise"]["snr"])) and CFG["noise"]["snr"] == 30
    assert P.preset_snr(CFG, "DiSCo 364") == 30.0


def test_a_tractogram_writes_to_tck_and_reads_back(tmp_path):
    """The .tck download: dmipy-tract's writer read back through dmipy-sim's reader (their conventions agree)."""
    from dmipy_sim.io.strands import read_tck
    from dmipy_tract import Tractogram
    t = Tractogram(np.random.default_rng(0).random((10, 3)).astype(np.float32), np.array([0, 4, 10]), np.array([0, 1]), np.zeros((2, 2), int))
    t.to_tck(str(tmp_path / "t.tck"))
    assert [len(s) for s in read_tck(str(tmp_path / "t.tck"), coordinate_unit_m=1e-3)] == [4, 6]


def test_the_csd_runs_on_the_configured_backend_alone(monkeypatch):
    """The FOD field (the response estimate and the fit) runs on the backend it is given: on ``jax`` nothing reaches
    for PyTorch, which the Docker Space does not install."""
    import sys
    monkeypatch.setitem(sys.modules, "torch", None)            # any import of torch now fails
    p = P.Protocol((P.Shell("d12-D24", 1000, 30),))
    m = P.measurements(p, CFG["shapes"])
    rng = np.random.default_rng(3)
    axes = rng.normal(size=(2, 2, 2, 3))
    axes /= np.linalg.norm(axes, axis=-1, keepdims=True)
    g = m.dirs / np.maximum(np.linalg.norm(m.dirs, axis=-1, keepdims=True), 1e-12)
    cos2 = np.einsum("xyzk,nk->xyzn", axes, g) ** 2
    b = m.bvals * 1e-3                                         # ms/um^2 x um^2/ms
    dwi = np.exp(-b * (0.3 + 1.4 * cos2))                     # a prolate tensor along each voxel's axis
    mask = np.ones(dwi.shape[:3], bool)
    sh, seconds, _ = P.csd(dwi, m, mask, backend="jax")
    assert sh.shape == (2, 2, 2, 45) and np.isfinite(sh).all() and seconds > 0