Spaces:
Running on Zero
Running on Zero
Download tests/test_build_brain_asset.py from rfick/brain-zero: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/spaces/rfick/brain-zero/resolve/main/tests/test_build_brain_asset.py
- Command line
-
hf download hf://spaces/rfick/brain-zero/tests/test_build_brain_asset.py
-
curl -L -o test_build_brain_asset.py https://huggingface.co/spaces/rfick/brain-zero/resolve/main/tests/test_build_brain_asset.py
10.8 kB
| """``build_brain_asset.py``'s pieces, on tiny synthetic data: merging shells, the mean b=0, the manifest payload and | |
| the checks sample are pure numpy/nibabel and run in every CI; the dmipy-fit MSMT-CSD fit itself needs cvxpy and is | |
| skipped when it is not installed (the offline build's own venv; not part of the Space's requirements.txt).""" | |
| import json | |
| import os | |
| import nibabel as nib | |
| import numpy as np | |
| import pytest | |
| from tools import build_brain_asset as BA | |
| def _write_shell(tmp_path, stem, shape, bval, n_dirs, affine, seed): | |
| rng = np.random.default_rng(seed) | |
| n = n_dirs + 1 | |
| data = rng.random(shape + (n,)).astype(np.float32) + 1.0 | |
| nib.Nifti1Image(data, affine).to_filename(os.path.join(tmp_path, stem + ".nii.gz")) | |
| bvals = np.r_[np.full(n_dirs, float(bval)), 0.0] | |
| dirs = rng.normal(size=(n_dirs, 3)) | |
| dirs /= np.linalg.norm(dirs, axis=1, keepdims=True) | |
| bvecs = np.vstack([dirs, [[0.0, 0.0, 0.0]]]) | |
| np.savetxt(os.path.join(tmp_path, stem + ".bval"), bvals[None, :], fmt="%g") | |
| np.savetxt(os.path.join(tmp_path, stem + ".bvec"), bvecs.T, fmt="%g") | |
| return data, bvals, bvecs | |
| def test_merge_shells_concatenates_and_orders(tmp_path): | |
| affine = np.diag([2.5, 2.5, 2.5, 1.0]) | |
| shape = (4, 4, 3) | |
| d1, bv1, vec1 = _write_shell(tmp_path, "a_b1000n4_dwi", shape, 1000, 4, affine, seed=0) | |
| d2, bv2, vec2 = _write_shell(tmp_path, "b_b2000n6_dwi", shape, 2000, 6, affine, seed=1) | |
| data, bvals, bvecs, aff, shells = BA.merge_shells(str(tmp_path)) | |
| assert data.shape == shape + (5 + 7,) | |
| assert bvals.shape == (12,) | |
| assert bvecs.shape == (12, 3) | |
| np.testing.assert_allclose(aff, affine) | |
| assert shells == [(1000.0, 4), (2000.0, 6)] | |
| # every file's own b0 (last column) survives the concatenation | |
| assert np.sum(bvals < BA.B0_THRESHOLD_S_MM2) == 2 | |
| def test_merge_shells_rejects_a_mismatched_grid(tmp_path): | |
| affine = np.diag([2.5, 2.5, 2.5, 1.0]) | |
| _write_shell(tmp_path, "a_b1000n2_dwi", (4, 4, 3), 1000, 2, affine, seed=0) | |
| _write_shell(tmp_path, "b_b2000n2_dwi", (5, 4, 3), 2000, 2, affine, seed=1) | |
| with pytest.raises(ValueError): | |
| BA.merge_shells(str(tmp_path)) | |
| def test_mean_b0_averages_only_the_b0_volumes(): | |
| bvals = np.array([0.0, 1000.0, 1000.0, 10.0]) | |
| data = np.zeros((2, 2, 2, 4), dtype=np.float32) | |
| data[..., 0] = 2.0 | |
| data[..., 3] = 4.0 | |
| data[..., 1] = 999.0 | |
| data[..., 2] = 999.0 | |
| out = BA.mean_b0(data, bvals) | |
| assert out.shape == (2, 2, 2) | |
| np.testing.assert_allclose(out, 3.0) | |
| def test_mean_b0_raises_without_a_b0(): | |
| with pytest.raises(ValueError): | |
| BA.mean_b0(np.zeros((2, 2, 2, 3)), np.array([1000.0, 1000.0, 1000.0])) | |
| def test_build_manifest_schema(): | |
| m = BA.build_manifest( | |
| subject="s", scan="sc", source_doi="10.x/y", grid_shape=(2, 3, 4), voxel_size_mm=(2.5, 2.5, 2.5), | |
| affine=np.eye(4), bvals=np.array([0.0, 1000.0]), bvecs=np.zeros((2, 3)), te_s=None, delta_s=None, | |
| delta_cap_s=None, dmipy_fit_commit="abc123", disco_space_commit="def456", solver="csd_cvxpy", n_regions=84) | |
| json.dumps(m) # every field must be JSON-serialisable | |
| assert m["source"]["dataset"] == "OpenNeuro ds003416" | |
| assert m["source"]["paper_doi"] == "10.1002/mrm.28926" | |
| assert m["source"]["license"] == "CC0" | |
| assert m["grid"]["shape"] == [2, 3, 4] | |
| assert m["fod"]["lmax"] == BA.SH_ORDER | |
| assert m["fractions"] == ["wm", "gm", "csf"] | |
| assert m["reconstruction"]["tool"] == "dmipy-fit" | |
| assert m["reconstruction"]["commit"] == "abc123" | |
| assert "eroded 3 passes" in m["reconstruction"]["responses"] and "tournier13" in m["reconstruction"]["responses"] | |
| assert m["parcellation"]["n_regions"] == 84 | |
| def test_save_checks_sample_is_reproducible(tmp_path): | |
| rng = np.random.default_rng(0) | |
| data = rng.random((5, 5, 5, 7)).astype(np.float32) | |
| sh = rng.random((5, 5, 5, 45)).astype(np.float32) | |
| fractions = rng.random((5, 5, 5, 3)).astype(np.float32) | |
| mask = np.zeros((5, 5, 5), dtype=bool) | |
| mask[1:4, 1:4, 1:4] = True | |
| bvals = np.linspace(0, 3000, 7) | |
| BA.save_checks_sample(str(tmp_path), data, bvals, sh, fractions, mask, n_sample=10, seed=0) | |
| out = np.load(os.path.join(tmp_path, "checks", "msmt_sample.npz")) | |
| assert out["voxel_index"].shape == (10, 3) | |
| assert out["signal"].shape == (10, 7) | |
| assert out["sh_coeff"].shape == (10, 45) | |
| assert out["fractions"].shape == (10, 3) | |
| for idx, sig in zip(out["voxel_index"], out["signal"]): | |
| assert mask[tuple(idx)] | |
| np.testing.assert_allclose(sig, data[tuple(idx)]) | |
| BA.save_checks_sample(str(tmp_path), data, bvals, sh, fractions, mask, n_sample=10, seed=0) | |
| out2 = np.load(os.path.join(tmp_path, "checks", "msmt_sample.npz")) | |
| np.testing.assert_array_equal(out["voxel_index"], out2["voxel_index"]) | |
| def test_fraction_sanity(): | |
| fractions = np.zeros((2, 2, 1, 3), dtype=np.float32) | |
| fractions[0, 0, 0] = [0.7, 0.2, 0.1] # sum 1.0, WM > 0.5 | |
| fractions[0, 1, 0] = [0.3, 0.3, 0.3] # sum 0.9, WM not > 0.5 | |
| fractions[1, 0, 0] = [0.9, 0.05, 0.05] # sum 1.0, WM > 0.5 | |
| mask = np.zeros((2, 2, 1), dtype=bool) | |
| mask[0, 0, 0] = mask[0, 1, 0] = mask[1, 0, 0] = True # (1, 1, 0) left out of the mask on purpose | |
| out = BA.fraction_sanity(fractions, mask) | |
| assert out["n_mask"] == 3 | |
| assert out["n_wm_gt_half"] == 2 | |
| assert out["sum_median"] == pytest.approx(1.0) | |
| def _pure_direction_sh(sh_order, direction): | |
| """The real-SH coefficients of a single delta-like lobe along ``direction`` (dipy's own ``sf_to_sh`` on a | |
| one-hot signal at a fine sphere, so the test does not depend on dmipy-fit's basis machinery).""" | |
| from dipy.data import get_sphere | |
| from dipy.reconst.shm import sf_to_sh | |
| sphere = get_sphere(name="repulsion724") | |
| cos_angle = sphere.vertices @ np.asarray(direction) / np.linalg.norm(direction) | |
| sf = np.clip(cos_angle, 0, None) ** 16 # a sharp lobe peaked at `direction` | |
| return sf_to_sh(sf, sphere, sh_order_max=sh_order, basis_type="tournier07") | |
| def test_fod_peak_counts_single_vs_crossing(): | |
| sh_order = 8 | |
| single = _pure_direction_sh(sh_order, (0, 0, 1)) | |
| crossing = _pure_direction_sh(sh_order, (1, 0, 0)) + _pure_direction_sh(sh_order, (0, 1, 0)) | |
| sh = np.stack([single, crossing]).reshape(1, 2, 1, -1).astype(np.float32) | |
| wm_mask = np.ones((1, 2, 1), dtype=bool) | |
| counts = BA.fod_peak_counts(sh, wm_mask, sh_order=sh_order) | |
| assert sum(counts.values()) == 2 | |
| assert counts.get(1, 0) >= 1 # the single lobe | |
| assert counts.get(2, 0) >= 1 # the two well-separated lobes | |
| def test_save_mean_b0_fod_png_writes_a_file(tmp_path): | |
| shape = (4, 4, 2) | |
| mean_b0_volume = np.random.default_rng(0).random(shape).astype(np.float32) | |
| sh = np.zeros(shape + (45,), dtype=np.float32) | |
| sh[..., 0] = 1.0 # an isotropic (but non-zero) FOD everywhere: a valid, if uninteresting, principal direction | |
| wm_mask = np.ones(shape, dtype=bool) | |
| out_path = os.path.join(tmp_path, "slice.png") | |
| BA.save_mean_b0_fod_png(out_path, mean_b0_volume, sh, wm_mask, z=0) | |
| assert os.path.exists(out_path) and os.path.getsize(out_path) > 0 | |
| def _single_tensor_attenuation(bvals_s_mm2, bvecs, eigenvalues_mm2_s, axis): | |
| """A closed-form single-tensor signal attenuation ``exp(-b g^T D g)``, no dmipy-sim/jax needed: ``D`` has its | |
| principal eigenvector along ``axis`` (unit vector) with ``eigenvalues_mm2_s = (lambda_par, lambda_perp)``.""" | |
| axis = np.asarray(axis, dtype=np.float64) | |
| axis = axis / np.linalg.norm(axis) | |
| lambda_par, lambda_perp = eigenvalues_mm2_s | |
| d_par_perp = lambda_par - lambda_perp | |
| cos2 = (bvecs @ axis) ** 2 | |
| return np.exp(-bvals_s_mm2 * (lambda_perp + d_par_perp * cos2)) | |
| def test_fit_msmt_csd_end_to_end_on_a_tiny_synthetic_volume(): | |
| """A smoke test of the dmipy-fit call itself (skipped if cvxpy is not installed): a few voxels of a synthetic | |
| single-tensor-plus-isotropic signal (plain numpy, no dmipy-sim), checking only shapes and that the fractions | |
| come out non-negative.""" | |
| pytest.importorskip("cvxpy") | |
| rng = np.random.default_rng(0) | |
| n_dirs = 30 | |
| dirs = rng.normal(size=(n_dirs, 3)) | |
| dirs /= np.linalg.norm(dirs, axis=1, keepdims=True) | |
| bvals_shell = np.repeat([1000.0, 2000.0], n_dirs) | |
| bvecs_shell = np.tile(dirs, (2, 1)) | |
| bvals = np.r_[0.0, 0.0, bvals_shell] | |
| bvecs = np.vstack([[0, 0, 0], [0, 0, 0], bvecs_shell]) | |
| # dhollander16 classifies every voxel by FA and a signal-decay metric, then takes small percentiles (2 %, 10 %) | |
| # of the GM-like / CSF-like populations as its candidates: a few discrete classes risk one of those percentiles | |
| # rounding to an empty set (a NaN response, poisoning the CSD kernel), so the volume instead has a continuum of | |
| # randomly oriented, randomly scaled tensors spanning WM-like (high FA) through GM-like to CSF-like (isotropic, | |
| # high diffusivity) -- much closer to what real brain data actually looks like to this heuristic. | |
| # every side 6 voxels longer than the 3-pass erosion of the response selection removes from it | |
| shape = (16, 16, 12) | |
| n_voxels = int(np.prod(shape)) | |
| fa_like = rng.uniform(0.0, 1.0, size=n_voxels) # 0 = isotropic, 1 = maximally anisotropic | |
| mean_d = np.where(fa_like < 0.5, rng.uniform(0.7e-3, 1.0e-3, n_voxels), # GM/WM-ish mean diffusivity | |
| rng.uniform(1.0e-3, 1.4e-3, n_voxels)) | |
| is_csf = rng.uniform(0, 1, n_voxels) < 0.15 | |
| mean_d = np.where(is_csf, rng.uniform(2.8e-3, 3.2e-3, n_voxels), mean_d) | |
| fa_like = np.where(is_csf, 0.0, fa_like) | |
| lambda_perp = mean_d * (1.0 - 0.6 * fa_like) | |
| lambda_par = mean_d * (1.0 + 1.2 * fa_like) | |
| axes = rng.normal(size=(n_voxels, 3)) | |
| mask = np.ones(shape, dtype=bool) | |
| data = np.empty((n_voxels, len(bvals)), dtype=np.float64) | |
| for i in range(n_voxels): | |
| data[i] = _single_tensor_attenuation(bvals, bvecs, (lambda_par[i], lambda_perp[i]), axis=axes[i]) | |
| data *= 1.0 + 0.01 * rng.standard_normal(data.shape) | |
| data = data.reshape(shape + (len(bvals),)) | |
| voxel_positions = np.zeros(shape, dtype=bool) | |
| voxel_positions[0, 0, 0] = voxel_positions[1, 0, 0] = voxel_positions[2, 0, 0] = voxel_positions[3, 0, 0] = True | |
| sh, fractions, responses, out_scheme = BA.fit_msmt_csd( | |
| data, bvals, bvecs, mask, sh_order=4, voxel_positions=voxel_positions) | |
| assert sh.shape == shape + (15,) # lmax = 4 -> 15 real-SH coefficients | |
| assert fractions.shape == shape + (3,) | |
| assert responses["n_voxels_fit"] == voxel_positions.sum() == 4 | |
| assert np.all(fractions[voxel_positions] >= -1e-4) # non-negative to the QP solver's feasibility tolerance | |
| assert np.all(fractions[~voxel_positions] == 0) # only the requested voxels were fit | |