Update GPR and SAR3D processing
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""The sar3d processor on the real CUDA backend: notebook reference and contracts.
|
||||
|
||||
Every case runs build/native_tests/backprojection_fixture (BACKPROJECTION_FIXTURE overrides it).
|
||||
Without a usable GPU the processor publishes sar3d_status 3: then only that
|
||||
warning is checked and the remaining tests are skipped. The notebook is needed
|
||||
only to regenerate sar3d_reference.npz with generate_backprojection_reference.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
FIXTURE = Path(os.environ.get("BACKPROJECTION_FIXTURE", ROOT / "build/native_tests/backprojection_fixture"))
|
||||
REFERENCE = Path(__file__).with_name("sar3d_reference.npz")
|
||||
VIEWS = ("sar3d_front", "sar3d_side", "sar3d_top", "sar3d_oblique")
|
||||
|
||||
|
||||
def run(request: dict | list[dict]) -> dict[str, dict]:
|
||||
output = subprocess.run([str(FIXTURE)], input=json.dumps(request), text=True,
|
||||
capture_output=True, timeout=30, check=True)
|
||||
result = json.loads(output.stdout)
|
||||
if isinstance(request, list):
|
||||
result = result[-1]
|
||||
return {item["name"]: item for item in result["payloads"]}
|
||||
|
||||
|
||||
def status(payloads: dict[str, dict]) -> tuple[int, str]:
|
||||
table = payloads["sar3d_status"]["table"]
|
||||
return int(table[0]), bytes(map(int, table[1:])).decode("utf-8")
|
||||
|
||||
|
||||
def reference_cases(archive) -> list[dict]:
|
||||
"""Requests of the reference archive, one per notebook parameter case."""
|
||||
cases = []
|
||||
for case in json.loads(archive["cases"].item()):
|
||||
request = json.loads(archive["request"].item())
|
||||
request.update(case["settings"])
|
||||
for member, windows in zip([*request["previous"], request["latest"]], case["windows"], strict=True):
|
||||
for trace, (start, end) in zip(member["traces"], windows, strict=True):
|
||||
trace.update(start=start, end=end)
|
||||
cases.append(request)
|
||||
return cases
|
||||
|
||||
|
||||
class Sar3dTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
with np.load(REFERENCE, allow_pickle=False) as archive:
|
||||
cls.reference = {key: archive[key] for key in archive.files}
|
||||
cls.cases = reference_cases(cls.reference)
|
||||
cls.scene_payloads = run(cls.cases[0])
|
||||
code, message = status(cls.scene_payloads)
|
||||
cls.unavailable = message if code == 3 else None
|
||||
|
||||
def scene(self, **settings) -> dict:
|
||||
if self.unavailable:
|
||||
self.skipTest(self.unavailable)
|
||||
request = copy.deepcopy(self.cases[0])
|
||||
request.update(settings)
|
||||
return request
|
||||
|
||||
def test_unavailable_gpu_is_a_visible_warning(self) -> None:
|
||||
code, message = status(self.scene_payloads)
|
||||
if self.unavailable:
|
||||
self.assertIn("CUDA", message)
|
||||
self.assertFalse(any(item["image"] for item in self.scene_payloads.values()))
|
||||
else:
|
||||
self.assertEqual(code, 0, message)
|
||||
|
||||
def test_cuda_matches_notebook(self) -> None:
|
||||
if self.unavailable:
|
||||
self.skipTest(self.unavailable)
|
||||
for index, request in enumerate(self.cases):
|
||||
with self.subTest(case=index):
|
||||
payloads = run(request)
|
||||
self.assertEqual(status(payloads)[0], 0, status(payloads))
|
||||
for view, horizontal, vertical in (("front", "x", "y"), ("side", "z", "y"), ("top", "x", "z")):
|
||||
payload = payloads[f"sar3d_{view}"]
|
||||
expected = self.reference[f"case_{index}_{view}"]
|
||||
np.testing.assert_allclose(payload["x"], self.reference[f"case_{index}_{horizontal}"],
|
||||
rtol=2e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(payload["y"], self.reference[f"case_{index}_{vertical}"],
|
||||
rtol=2e-6, atol=1e-6)
|
||||
np.testing.assert_allclose(np.asarray(payload["image"]).reshape(expected.shape),
|
||||
expected, rtol=2e-3, atol=3e-4)
|
||||
np.testing.assert_allclose(payloads["sar3d_target_width"]["table"],
|
||||
self.reference[f"case_{index}_target_width"], rtol=2e-3, atol=2e-4)
|
||||
peak = payloads["sar3d_peak"]["table"]
|
||||
expected_peak = self.reference[f"case_{index}_peak"]
|
||||
np.testing.assert_allclose(peak[1:], expected_peak[1:], rtol=0, atol=2e-6)
|
||||
np.testing.assert_allclose(peak[0], expected_peak[0], rtol=2e-3, atol=0)
|
||||
|
||||
def test_invalid_input_is_an_error_with_a_reason(self) -> None:
|
||||
def no_traces(request): request["latest"]["traces"] = []
|
||||
def empty_window(request): request["latest"]["traces"][0]["end"] = request["latest"]["traces"][0]["start"]
|
||||
def falling_frequency(request): request["latest"]["traces"][0]["frequency"].reverse()
|
||||
for name, settings, damage in (
|
||||
("no traces", {}, no_traces),
|
||||
("empty capture window", {}, empty_window),
|
||||
("falling frequency", {}, falling_frequency),
|
||||
("grid beyond 32-bit indexing", {"voxel": 1e-8}, None),
|
||||
("vertical look angle", {"pitch": 90}, None),
|
||||
("result larger than the ring slot", {"slot": 1024}, None),
|
||||
):
|
||||
with self.subTest(name):
|
||||
request = self.scene(**settings)
|
||||
if damage:
|
||||
damage(request)
|
||||
code, message = status(run(request))
|
||||
self.assertEqual(code, 2, message)
|
||||
self.assertTrue(message)
|
||||
|
||||
def test_cached_aperture_updates_when_previous_mean_context_changes(self) -> None:
|
||||
request = self.scene(subtract_mean=True, mean_count=1)
|
||||
shorter = copy.deepcopy(request)
|
||||
shorter["previous"] = shorter["previous"][-1:]
|
||||
cached, fresh = run([request, shorter]), run(shorter)
|
||||
self.assertEqual(status(cached)[0], 0, status(cached))
|
||||
for name in VIEWS:
|
||||
np.testing.assert_allclose(cached[name]["image"], fresh[name]["image"], rtol=2e-3, atol=3e-4)
|
||||
|
||||
def test_empty_signal_is_reported_without_a_peak(self) -> None:
|
||||
request = self.scene(previous=[], radius=0)
|
||||
for trace in request["latest"]["traces"]:
|
||||
trace["s21"] = [[0, 0]] * len(trace["frequency"])
|
||||
payloads = run(request)
|
||||
self.assertEqual(status(payloads)[0], 1)
|
||||
self.assertNotIn("sar3d_peak", payloads)
|
||||
for name in VIEWS:
|
||||
self.assertFalse(np.any(payloads[name]["image"]))
|
||||
|
||||
def test_weak_signal_stays_finite(self) -> None:
|
||||
request = self.scene(previous=[], radius=0, normalize=False)
|
||||
for trace in request["latest"]["traces"]:
|
||||
trace["s21"] = (np.asarray(trace["s21"]) * 1e-38).tolist()
|
||||
payloads = run(request)
|
||||
self.assertEqual(status(payloads)[0], 0, status(payloads))
|
||||
for name in VIEWS:
|
||||
values = np.asarray(payloads[name]["image"])
|
||||
self.assertTrue(np.isfinite(values).all())
|
||||
self.assertGreater(values.max(), 0)
|
||||
self.assertLess(values.max(), 1e-10)
|
||||
|
||||
|
||||
class GprTest(unittest.TestCase):
|
||||
def test_changing_tx_rx_counts_rebuilds_the_cached_grid(self) -> None:
|
||||
with np.load(REFERENCE.with_name("gpr_reference.npz"), allow_pickle=False) as reference:
|
||||
base = json.loads(reference["case_0_request"].item())
|
||||
requests = []
|
||||
for tx, rx in ((2, 1), (1, 2)):
|
||||
request = copy.deepcopy(base)
|
||||
request.update(tx=[[i, 0, 0, 0] for i in range(tx)], rx=[[i, 0, 0, 0] for i in range(rx)])
|
||||
request["latest"]["traces"] = [trace for trace in request["latest"]["traces"]
|
||||
if trace["output"] < tx and trace["input"] < rx]
|
||||
requests.append(request)
|
||||
np.testing.assert_allclose(run(requests)["gpr_accumulator"]["image"],
|
||||
run(requests[-1])["gpr_accumulator"]["image"])
|
||||
|
||||
def test_coherent_ground_bp_matches_horns_and_notebook_equalizer(self) -> None:
|
||||
with np.load(REFERENCE.with_name("gpr_reference.npz"), allow_pickle=False) as reference:
|
||||
for i in range(3):
|
||||
with self.subTest(case=i):
|
||||
request = json.loads(reference[f"case_{i}_request"].item())
|
||||
payload = run(request)["gpr_accumulator"]
|
||||
expected = reference[f"case_{i}_image"]
|
||||
np.testing.assert_allclose(np.asarray(payload["image"]).reshape(expected.shape), expected,
|
||||
rtol=2e-4, atol=3e-5)
|
||||
np.testing.assert_allclose(payload["y"], np.linspace(.1, request["max_depth"], expected.shape[0]),
|
||||
rtol=1e-6, atol=1e-7)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user