|
| 1 | +"""TDD: Validate HU → acoustic properties using TFUScapes CT data.""" |
| 2 | +import numpy as np |
| 3 | +import pytest |
| 4 | +from pathlib import Path |
| 5 | + |
| 6 | +SAMPLE = Path(__file__).parent.parent / "benchmarks" / "tfuscapes_data" / "sample_0.npz" |
| 7 | + |
| 8 | + |
| 9 | +@pytest.mark.skipif(not SAMPLE.exists(), reason="TFUScapes sample not downloaded") |
| 10 | +def test_ct_to_acoustic_properties_physical(): |
| 11 | + """Acoustic properties derived from TFUScapes CT should be physical.""" |
| 12 | + from benchmarks.pseudo_ct_validation import hu_to_acoustic_properties |
| 13 | + |
| 14 | + d = np.load(SAMPLE) |
| 15 | + ct = d["ct"] |
| 16 | + props = hu_to_acoustic_properties(ct) |
| 17 | + |
| 18 | + # Sound speed in physical range |
| 19 | + assert props["sound_speed"].min() >= 1400 |
| 20 | + assert props["sound_speed"].max() <= 4500 |
| 21 | + |
| 22 | + # Density: water to bone |
| 23 | + assert props["density"].min() >= 1000 |
| 24 | + assert props["density"].max() <= 3000 |
| 25 | + |
| 26 | + # Skull regions (HU > 700) should have higher c than brain |
| 27 | + skull = ct > 700 |
| 28 | + brain = (ct > 50) & (ct < 200) |
| 29 | + if skull.any() and brain.any(): |
| 30 | + c_skull = props["sound_speed"][skull].mean() |
| 31 | + c_brain = props["sound_speed"][brain].mean() |
| 32 | + assert c_skull > c_brain, f"Skull c={c_skull:.0f} not > brain c={c_brain:.0f}" |
| 33 | + |
| 34 | + |
| 35 | +@pytest.mark.skipif(not SAMPLE.exists(), reason="TFUScapes sample not downloaded") |
| 36 | +def test_ct_labels_match_acoustic_segmentation(): |
| 37 | + """CT-derived labels should match our tissue segmentation thresholds.""" |
| 38 | + from benchmarks.tfuscapes_compare import ct_to_labels |
| 39 | + |
| 40 | + d = np.load(SAMPLE) |
| 41 | + labels = ct_to_labels(d["ct"]) |
| 42 | + |
| 43 | + unique = np.unique(labels) |
| 44 | + # Should have at least water, skull, and some brain tissue |
| 45 | + assert 0 in unique # water/air |
| 46 | + assert 2 in unique # skull |
| 47 | + |
| 48 | + # Skull voxels should correspond to high HU regions |
| 49 | + skull_mask = labels == 2 |
| 50 | + skull_hu = d["ct"][skull_mask] |
| 51 | + assert skull_hu.mean() > 700, f"Skull HU mean too low: {skull_hu.mean():.0f}" |
| 52 | + |
| 53 | + |
| 54 | +@pytest.mark.skipif(not SAMPLE.exists(), reason="TFUScapes sample not downloaded") |
| 55 | +def test_archies_law_conductivity_from_ct(): |
| 56 | + """Archie's Law should produce physical conductivity from CT porosity.""" |
| 57 | + from benchmarks.pseudo_ct_validation import hu_to_acoustic_properties |
| 58 | + |
| 59 | + d = np.load(SAMPLE) |
| 60 | + ct = d["ct"] |
| 61 | + |
| 62 | + # Porosity from HU |
| 63 | + HU_MAX = 2500.0 |
| 64 | + porosity = 1.0 - np.clip(ct, 0, HU_MAX) / HU_MAX |
| 65 | + porosity = np.maximum(porosity, 0.05) |
| 66 | + |
| 67 | + # Archie's Law |
| 68 | + sigma_brine = 2.0 # S/m |
| 69 | + m = 1.5 |
| 70 | + sigma = sigma_brine * (porosity ** m) |
| 71 | + |
| 72 | + # Dense skull (HU > 1200) should have lower conductivity than brain |
| 73 | + dense_skull = ct > 1200 |
| 74 | + brain = (ct > 50) & (ct < 200) |
| 75 | + if dense_skull.any() and brain.any(): |
| 76 | + sigma_skull = sigma[dense_skull].mean() |
| 77 | + sigma_brain = sigma[brain].mean() |
| 78 | + assert sigma_skull < sigma_brain, ( |
| 79 | + f"Dense skull sigma={sigma_skull:.3f} should be < brain sigma={sigma_brain:.3f}" |
| 80 | + ) |
| 81 | + # Dense skull conductivity should be physically reasonable |
| 82 | + assert sigma_skull < 1.5, f"Dense skull conductivity {sigma_skull:.3f} too high" |
0 commit comments