Refactor radar system
This commit is contained in:
@@ -39,17 +39,24 @@ def _sweep(collection_id: int, monotonic_ns: int) -> SweepCollection:
|
||||
frequency_hz=np.linspace(1e9, 6e9, points, dtype=np.float32),
|
||||
s11=np.zeros(points, dtype=np.complex64),
|
||||
s21=np.full(points, collection_id, dtype=np.complex64),
|
||||
capture_start_ns=monotonic_ns,
|
||||
capture_end_ns=monotonic_ns + 1000,
|
||||
)
|
||||
return SweepCollection(collection_id=collection_id, monotonic_ns=monotonic_ns, traces=[trace])
|
||||
return SweepCollection(collection_id=collection_id, monotonic_ns=monotonic_ns, traces=[trace],
|
||||
capture_start_ns=monotonic_ns, capture_end_ns=monotonic_ns + 1000)
|
||||
|
||||
|
||||
@unittest.skipUnless(_PROCESSOR.exists(), "data_processor is not built (run make)")
|
||||
class FedBackSweepsTest(unittest.TestCase):
|
||||
class _ProcessorTestCase(unittest.TestCase):
|
||||
result_slot_size_bytes: int | None = None
|
||||
|
||||
def setUp(self) -> None:
|
||||
workdir = Path(tempfile.mkdtemp(prefix="processor_history_"))
|
||||
config = json.loads((_ROOT / "run_config.json").read_text(encoding="utf-8"))
|
||||
for name, ring in config["rings"].items():
|
||||
ring["name"] = f"/test_processor_history_{os.getpid()}_{name}"
|
||||
if self.result_slot_size_bytes is not None:
|
||||
config["rings"]["results"]["slot_size_bytes"] = self.result_slot_size_bytes
|
||||
config["run"]["combos"] = [{"input": 0, "output": 0}]
|
||||
config["run"]["processing_live_config_path"] = str(workdir / "processing_live.json")
|
||||
config["run"]["locator_server"]["enabled"] = False
|
||||
@@ -61,11 +68,14 @@ class FedBackSweepsTest(unittest.TestCase):
|
||||
|
||||
rings = config["rings"]
|
||||
shm_files = [Path("/dev/shm" + ring["name"]) for ring in rings.values()]
|
||||
self._stderr_path = workdir / "processor.log"
|
||||
stderr_log = self._stderr_path.open("w")
|
||||
self.addCleanup(stderr_log.close)
|
||||
processor = subprocess.Popen(
|
||||
[str(_PROCESSOR), "--config", str(config_path)],
|
||||
cwd=_ROOT,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stderr=stderr_log,
|
||||
)
|
||||
|
||||
def cleanup() -> None:
|
||||
@@ -78,6 +88,7 @@ class FedBackSweepsTest(unittest.TestCase):
|
||||
workdir.rmdir()
|
||||
|
||||
self.addCleanup(cleanup)
|
||||
self._processor = processor
|
||||
# The processor creates both rings at start-up, the results ring last.
|
||||
self._results = ShmRingReader(rings["results"]["name"], open_timeout_s=_TIMEOUT_S)
|
||||
self.addCleanup(self._results.close)
|
||||
@@ -103,6 +114,22 @@ class FedBackSweepsTest(unittest.TestCase):
|
||||
deadline = min(deadline, time.monotonic() + 0.3)
|
||||
return ids
|
||||
|
||||
def _result_for_revision(self, revision: int, collection_id: int | None = None):
|
||||
deadline = time.monotonic() + _TIMEOUT_S
|
||||
while time.monotonic() < deadline:
|
||||
result = self._results.pop_result_collection()
|
||||
if result is not None:
|
||||
payload = next((item for item in result.collection_payloads
|
||||
if item.processing_name == "processing_revision"), None)
|
||||
if payload is not None:
|
||||
actual = sum(int(word) << (index * 16) for index, word in enumerate(payload.table[0]))
|
||||
if actual == revision and (collection_id is None or result.collection_id == collection_id):
|
||||
return result
|
||||
time.sleep(0.01)
|
||||
self.fail(f"No processor result for settings revision {revision}: {self._stderr_path.read_text()}")
|
||||
|
||||
|
||||
class FedBackSweepsTest(_ProcessorTestCase):
|
||||
def test_fed_back_sweeps_are_processed_but_not_retained(self) -> None:
|
||||
start_ns = time.monotonic_ns()
|
||||
sweeps = [_sweep(index, start_ns + index * 1_000_000) for index in range(1, _SWEEPS + 1)]
|
||||
@@ -119,6 +146,139 @@ class FedBackSweepsTest(unittest.TestCase):
|
||||
self._live_writer.write(self._live)
|
||||
self.assertEqual(self._published_ids(_SWEEPS), expected)
|
||||
|
||||
def test_live_geometry_recomputes_retained_gpr_without_restarting(self) -> None:
|
||||
self._live.processor_mode = "gpr"
|
||||
self._live.gpr_input_positions = [0]
|
||||
self._live.gpr_output_positions = [0]
|
||||
self._live.gpr_background_subtract_enabled = False
|
||||
self._live.tx_geometry = [{"output_pos": 0, "x_m": 0.0, "y_m": 0.0, "z_m": 0.0}]
|
||||
self._live.rx_geometry = [{"input_pos": 0, "x_m": 0.0, "y_m": 0.0, "z_m": 0.0}]
|
||||
self._live.settings_revision = (1 << 63) + 987654321
|
||||
self._live_writer.write(self._live)
|
||||
self._push([_sweep(1, time.monotonic_ns())])
|
||||
original = self._result_for_revision(self._live.settings_revision)
|
||||
original_image = next(payload for payload in original.collection_payloads if payload.processing_name == "gpr_accumulator")
|
||||
# A malformed live file retains the last accepted geometry and revision.
|
||||
accepted_revision = self._live.settings_revision
|
||||
self._live.settings_revision += 1
|
||||
self._live.tx_geometry.append(dict(self._live.tx_geometry[0]))
|
||||
self._live_writer.write(self._live)
|
||||
self._push([_sweep(2, time.monotonic_ns())])
|
||||
retained = self._result_for_revision(accepted_revision, collection_id=2)
|
||||
retained_image = next(payload for payload in retained.collection_payloads if payload.processing_name == "gpr_accumulator")
|
||||
np.testing.assert_array_equal(retained_image.image_x_axis, original_image.image_x_axis)
|
||||
self._live.tx_geometry = self._live.tx_geometry[:1]
|
||||
self._live.tx_geometry[0]["x_m"] = 2.0
|
||||
self._live.rx_geometry[0]["x_m"] = 2.0
|
||||
self._live.settings_revision += 1
|
||||
self._live_writer.write(self._live)
|
||||
updated = self._result_for_revision(self._live.settings_revision)
|
||||
updated_image = next(payload for payload in updated.collection_payloads if payload.processing_name == "gpr_accumulator")
|
||||
self.assertEqual(updated.collection_id, retained.collection_id)
|
||||
self.assertEqual(updated.monotonic_ns, retained.monotonic_ns)
|
||||
np.testing.assert_allclose(updated_image.image_x_axis, original_image.image_x_axis + 2.0, atol=1e-6)
|
||||
self.assertIsNone(self._processor.poll())
|
||||
|
||||
|
||||
class OversizedResultsTest(_ProcessorTestCase):
|
||||
result_slot_size_bytes = 4096
|
||||
|
||||
def test_valid_mode_recovers_after_an_oversized_result(self) -> None:
|
||||
self._live.processor_mode = "gpr"
|
||||
self._live.gpr_input_positions = [0]
|
||||
self._live.gpr_output_positions = [0]
|
||||
self._live.gpr_background_subtract_enabled = False
|
||||
self._live.settings_revision = 17
|
||||
self._live_writer.write(self._live)
|
||||
self._push([_sweep(1, time.monotonic_ns())])
|
||||
deadline = time.monotonic() + _TIMEOUT_S
|
||||
while "requires" not in self._stderr_path.read_text():
|
||||
self.assertIsNone(self._processor.poll(), self._stderr_path.read_text())
|
||||
self.assertLess(time.monotonic(), deadline, self._stderr_path.read_text())
|
||||
time.sleep(0.01)
|
||||
self.assertIn("increase rings.results.slot_size_bytes", self._stderr_path.read_text())
|
||||
self._live.processor_mode = "pass_through"
|
||||
self._live.settings_revision = 18
|
||||
self._live_writer.write(self._live)
|
||||
self.assertEqual(self._result_for_revision(18).collection_id, 1)
|
||||
self._push([_sweep(2, time.monotonic_ns())])
|
||||
self.assertEqual(self._result_for_revision(18, collection_id=2).collection_id, 2)
|
||||
self.assertIsNone(self._processor.poll())
|
||||
|
||||
|
||||
class Sar3dRecoveryTest(_ProcessorTestCase):
|
||||
def test_sar3d_failure_recovers_without_restarting(self) -> None:
|
||||
self._live.processor_mode = "sar3d"
|
||||
self._live.gpr_input_positions = [0]
|
||||
self._live.gpr_output_positions = [0]
|
||||
self._live.gpr_background_subtract_enabled = False
|
||||
self._live.gpr_min_depth_m = 2.0
|
||||
self._live.gpr_max_depth_m = 5.0
|
||||
self._live.gpr_speed_m_s = 0.0
|
||||
self._live.gpr_start_freq_mhz = 1500.0
|
||||
self._live.gpr_stop_freq_mhz = 6000.0
|
||||
self._live.sar3d_voxel_m = 0.4
|
||||
self._live.sar3d_frame_radius = 0
|
||||
self._live.tx_geometry = [{"output_pos": 0, "x_m": -0.3, "y_m": 0.0, "z_m": 0.0}]
|
||||
self._live.rx_geometry = [{"input_pos": 0, "x_m": 0.2, "y_m": 0.4, "z_m": 0.0}]
|
||||
self._live.settings_revision = 31
|
||||
self._live_writer.write(self._live)
|
||||
sweep = _sweep(1, time.monotonic_ns())
|
||||
# df=25 MHz gives a 6 m unambiguous one-way range. The history tests'
|
||||
# 64-point sweep aliases before this test's 2..5 m depth gate.
|
||||
sweep.traces[0].frequency_hz = np.linspace(1e9, 6e9, 201, dtype=np.float32)
|
||||
sweep.traces[0].s11 = np.zeros(201, dtype=np.complex64)
|
||||
sweep.traces[0].s21 = np.exp(-4j * np.pi * sweep.traces[0].frequency_hz * 4.0 / 3e8).astype(np.complex64)
|
||||
self._push([sweep])
|
||||
|
||||
def ready_images(result):
|
||||
payloads = {item.processing_name: item for item in result.collection_payloads}
|
||||
self.assertEqual(payloads["sar3d_status"].table[0, 0], 0)
|
||||
images = [payloads[name].image for name in ("sar3d_front", "sar3d_side", "sar3d_top", "sar3d_oblique")]
|
||||
for image in images:
|
||||
self.assertTrue(np.all(np.isfinite(image)))
|
||||
self.assertGreater(float(image.max()), 0.0)
|
||||
self.assertIn("sar3d_peak", payloads)
|
||||
return images
|
||||
|
||||
first = self._result_for_revision(31)
|
||||
status = next(item for item in first.collection_payloads if item.processing_name == "sar3d_status")
|
||||
if status.table[0, 0] == 3:
|
||||
self.assertIn("CUDA", bytes(map(int, status.table[0, 1:])).decode("utf-8"))
|
||||
self.assertFalse(any(item.processing_name in {"sar3d_front", "sar3d_side", "sar3d_top", "sar3d_oblique"}
|
||||
for item in first.collection_payloads))
|
||||
self._push([_sweep(2, sweep.monotonic_ns + 1_000_000)])
|
||||
repeated = self._result_for_revision(31, collection_id=2)
|
||||
repeated_status = next(item for item in repeated.collection_payloads if item.processing_name == "sar3d_status")
|
||||
np.testing.assert_array_equal(status.table, repeated_status.table)
|
||||
self._live.processor_mode = "pass_through"
|
||||
self._live.settings_revision = 34
|
||||
self._live_writer.write(self._live)
|
||||
recovered = self._result_for_revision(34, collection_id=2)
|
||||
self.assertEqual(len(recovered.blocks), 1)
|
||||
self.assertIsNone(self._processor.poll())
|
||||
return
|
||||
original = ready_images(first)
|
||||
self._live.gpr_start_freq_mhz = 8000.0
|
||||
self._live.gpr_stop_freq_mhz = 9000.0
|
||||
self._live.settings_revision = 32
|
||||
self._live_writer.write(self._live)
|
||||
failed = self._result_for_revision(32)
|
||||
status = next(item for item in failed.collection_payloads if item.processing_name == "sar3d_status")
|
||||
self.assertEqual(status.table[0, 0], 2)
|
||||
self.assertIn("frequency points", bytes(map(int, status.table[0, 1:])).decode("utf-8"))
|
||||
self.assertIsNone(self._processor.poll())
|
||||
|
||||
self._live.gpr_start_freq_mhz = 1500.0
|
||||
self._live.gpr_stop_freq_mhz = 6000.0
|
||||
self._live.settings_revision = 33
|
||||
self._live_writer.write(self._live)
|
||||
recovered = self._result_for_revision(33)
|
||||
self.assertEqual(recovered.collection_id, sweep.collection_id)
|
||||
for before, after in zip(original, ready_images(recovered)):
|
||||
np.testing.assert_array_equal(before, after)
|
||||
self.assertIsNone(self._processor.poll())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user