Skip to content

Commit 47f1057

Browse files
author
Brandon Kirkland
committed
Add behavioral tests for SimulationCorrected and degenerate GMM (#150)
SimulationCorrected: test delay computation from mocked arrival times, fallback on simulation failure, and fallback when k-wave is missing. ThresholdMRI: test that near-constant brain intensity falls back to gray_matter instead of collapsing to all-CSF.
1 parent 196b071 commit 47f1057

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

tests/test_simulation_corrected.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import annotations
22

3+
from unittest.mock import patch
4+
5+
import numpy as np
36
import pytest
47

58
from openlifu.bf.delay_methods import DelayMethod, SimulationCorrected
@@ -126,3 +129,58 @@ def test_to_table(self):
126129
assert table.iloc[0]['Value'] == 'SimulationCorrected'
127130
assert table.iloc[1]['Name'] == 'Default Sound Speed'
128131
assert table.iloc[1]['Value'] == 1500.0
132+
133+
134+
class TestSimulationCorrectedBehavior:
135+
"""Test delay calculation logic by mocking the k-wave simulation layer."""
136+
137+
def test_delays_from_known_arrival_times(self):
138+
"""When _run_reciprocal_simulation returns known arrival times,
139+
calc_delays should return max(arrival) - arrival for each element."""
140+
arrival_times = np.array([0.001, 0.002, 0.003])
141+
expected_delays = np.array([0.002, 0.001, 0.0])
142+
143+
method = SimulationCorrected()
144+
with patch.object(
145+
SimulationCorrected,
146+
"_run_reciprocal_simulation",
147+
return_value=arrival_times,
148+
), patch("importlib.util.find_spec", return_value=True):
149+
delays = method.calc_delays(
150+
arr=None, target=None, params=None, transform=None
151+
)
152+
np.testing.assert_allclose(delays, expected_delays)
153+
154+
def test_fallback_on_simulation_failure(self):
155+
"""When _run_reciprocal_simulation raises, calc_delays should
156+
fall back to Direct geometric delays without crashing."""
157+
method = SimulationCorrected()
158+
with patch.object(
159+
SimulationCorrected,
160+
"_run_reciprocal_simulation",
161+
side_effect=RuntimeError("mocked failure"),
162+
), patch("importlib.util.find_spec", return_value=True), patch.object(
163+
SimulationCorrected,
164+
"_fallback_delays",
165+
return_value=np.array([0.0, 0.0, 0.0]),
166+
) as mock_fallback:
167+
delays = method.calc_delays(
168+
arr=None, target=None, params=None, transform=None
169+
)
170+
mock_fallback.assert_called_once()
171+
np.testing.assert_array_equal(delays, [0.0, 0.0, 0.0])
172+
173+
def test_fallback_when_kwave_missing(self):
174+
"""When k-wave is not installed, calc_delays should fall back
175+
to Direct geometric delays."""
176+
method = SimulationCorrected()
177+
with patch("importlib.util.find_spec", return_value=None), patch.object(
178+
SimulationCorrected,
179+
"_fallback_delays",
180+
return_value=np.array([0.0, 0.0]),
181+
) as mock_fallback:
182+
delays = method.calc_delays(
183+
arr=None, target=None, params=None, transform=None
184+
)
185+
mock_fallback.assert_called_once()
186+
np.testing.assert_array_equal(delays, [0.0, 0.0])

tests/test_threshold_mri.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,30 @@ def test_classify_brain_gmm_failure_fallback() -> None:
289289
assert set(np.unique(result.to_numpy()[brain_voxels])) == {idx["gray_matter"]}
290290

291291

292+
def test_classify_brain_degenerate_constant_intensity() -> None:
293+
"""When the brain interior has near-constant intensity, the GMM cannot
294+
fit meaningful components. All brain voxels should fall back to
295+
gray_matter rather than collapsing to CSF (argmax class 0)."""
296+
volume = create_synthetic_head_volume(shape=(64, 64, 64), intensity=100.0)
297+
seg = ThresholdMRI(
298+
classify_brain_tissues=True,
299+
air_threshold_quantile=0.0,
300+
skull_thickness_mm=7.0,
301+
)
302+
result = seg._segment(volume)
303+
idx = seg._material_indices()
304+
brain_labels = {idx["csf"], idx["gray_matter"], idx["white_matter"]}
305+
brain_voxels = np.isin(result.to_numpy(), list(brain_labels))
306+
if np.any(brain_voxels):
307+
unique_brain = set(np.unique(result.to_numpy()[brain_voxels]))
308+
assert idx["gray_matter"] in unique_brain, (
309+
"Degenerate GMM should fall back to gray_matter"
310+
)
311+
assert idx["csf"] not in unique_brain, (
312+
"Degenerate GMM should not produce all-CSF labels"
313+
)
314+
315+
292316
def test_gmm_preserves_natural_class_proportions() -> None:
293317
"""EM-GMM should recover class proportions that reflect the actual geometry
294318
of the concentric spheres, NOT force them to ~33% each (as histogram

0 commit comments

Comments
 (0)