Refactor radar system

This commit is contained in:
Ayzen
2026-10-05 16:46:28 +03:00
parent 515049d2ed
commit 8f097dfa2e
179 changed files with 8212 additions and 6676 deletions
+114
View File
@@ -25,6 +25,7 @@ from python_app.models.run_config_schema import (
)
from python_app.models.run_config_validation import (
parse_combos_from_text,
validate_acquisition_config,
validate_combos,
validate_gpr_model,
validate_ring_endpoint,
@@ -78,6 +79,11 @@ class StrictNumericTypingTest(unittest.TestCase):
class StructuralTypingTest(unittest.TestCase):
"""A present-but-wrong-shaped section fails loudly instead of being dropped."""
def test_root_must_be_an_object(self) -> None:
for payload in (None, [], 'bad', 42):
with self.subTest(payload=payload), self.assertRaisesRegex(ValueError, 'root'):
RunConfigModel.from_dict(payload)
def test_combos_must_be_an_array(self) -> None:
payload = _valid_payload()
payload["run"]["combos"] = {"input": 0, "output": 0}
@@ -85,7 +91,103 @@ class StructuralTypingTest(unittest.TestCase):
RunConfigModel.from_dict(payload)
class AcquisitionValidationTest(unittest.TestCase):
def test_default_combo_expansion_is_bounded_before_allocation(self) -> None:
payload = _valid_payload()
payload["switches"]["port1"]["positions"] = 65536
payload["run"]["combos"] = []
with self.assertRaisesRegex(ValueError, "Too many combos"):
RunConfigModel.from_dict(payload)
config = self.config("librevna_multi")
config.radar.multi_device.input_switch_positions = 1 << 32
with self.assertRaisesRegex(ValueError, "Too many combos"):
config.apply_device_model_constraints()
def config(self, model: str = "librevna") -> RunConfigModel:
config = RunConfigModel()
config.radar.model = model
config.radar.sweep.points = 32
for port, switch in enumerate((config.output_switch, config.input_switch), 1):
switch.radar_port = port
switch.driver = "h7992"
switch.gpio_chip = "/dev/gpiochip0"
switch.pin_a, switch.pin_b = 17, 27
for name in ("raw", "preprocessed", "raw_tap", "preprocessed_tap"):
getattr(config.rings, name).slot_size_bytes = 1 << 20
config.ensure_combos()
return config
def test_undersized_trace_rings_fail_before_start(self) -> None:
config = self.config("librevna_multi")
for name in ("raw", "raw_tap", "preprocessed", "preprocessed_tap"):
ring = getattr(config.rings, name)
ring.name = "/test"
original = ring.slot_size_bytes
ring.slot_size_bytes = 4096
with self.subTest(ring=name), self.assertRaisesRegex(ValueError, f"rings.{name}"):
validate_acquisition_config(config)
ring.slot_size_bytes = original
validate_acquisition_config(config)
def test_kamil_requires_native_mode_except_for_replay(self) -> None:
config = self.config("kamil_adc")
with self.assertRaisesRegex(ValueError, "native"):
validate_acquisition_config(config)
config.radar.replay.dataset_dir = "recorded"
validate_acquisition_config(config)
def test_matrix_validates_physical_switch_instead_of_virtual_axis(self) -> None:
config = self.config("librevna_multi")
config.radar.driver_mode = "native"
config.radar.multi_device.output_switch_positions = 4
config.radar.multi_device.input_switch_positions = 4
config.ensure_combos()
self.assertEqual(config.output_switch.positions, 8)
self.assertEqual(config.input_switch.positions, 16)
validate_acquisition_config(config)
config.radar.multi_device.output_switch_positions = 5
config.ensure_combos()
with self.assertRaisesRegex(ValueError, "supports 1..4"):
validate_acquisition_config(config)
config.radar.driver_mode = "mock"
validate_acquisition_config(config)
def test_bad_switch_settings_are_reported_before_hardware_open(self) -> None:
for field, value in (
("driver_mode", "typo"), ("driver", "typo"), ("default_position", 1),
("radar_port", 0), ("pin_a", -1), ("pin_b", 17), ("gpio_chip", ""),
):
config = self.config()
config.output_switch.driver_mode = "native"
setattr(config.output_switch, field, value)
with self.subTest(field=field), self.assertRaises(ValueError):
validate_acquisition_config(config)
class NotchValidationTest(unittest.TestCase):
def test_malformed_bands_and_tapers_match_native_rejection(self) -> None:
for band in (("1", 2), (True, 2), (None, 2), (1, float("nan")), (2, 1), (1, 1e100)):
payload = _valid_payload()
payload["preprocess"]["notch"]["bands_hz"] = [list(band)]
with self.subTest(band=band), self.assertRaises(ValueError):
RunConfigModel.from_dict(payload)
for field, value in (("taper_width_hz", -1), ("taper_type", "typo")):
payload = _valid_payload()
payload["preprocess"]["notch"][field] = value
with self.subTest(field=field), self.assertRaises(ValueError):
RunConfigModel.from_dict(payload)
class SweepValidationTest(unittest.TestCase):
def test_nonfinite_fields_and_wire_overflow_are_rejected(self) -> None:
for field in ('start_hz', 'stop_hz', 'if_bandwidth_hz', 'power_dbm'):
for value in (float('nan'), float('inf')):
sweep = RadarSweepModel()
setattr(sweep, field, value)
with self.subTest(field=field, value=value), self.assertRaisesRegex(ValueError, field):
validate_sweep_model(sweep)
with self.assertRaisesRegex(ValueError, 'points'):
validate_sweep_model(RadarSweepModel(points=2 ** 32))
def test_points_must_be_positive(self) -> None:
for points in (0, -1):
with self.subTest(points=points), self.assertRaisesRegex(ValueError, "points"):
@@ -102,6 +204,10 @@ class SweepValidationTest(unittest.TestCase):
class ComboValidationTest(unittest.TestCase):
def test_direct_model_positions_cannot_be_silently_truncated(self) -> None:
for value in (0.5, True, '0'):
with self.subTest(value=value), self.assertRaisesRegex(ValueError, 'integer'):
validate_combos([ComboModel(value, 0)], input_positions=2, output_positions=2)
def test_within_bounds_passes(self) -> None:
validate_combos(
[ComboModel(input=0, output=0), ComboModel(input=3, output=1)],
@@ -136,6 +242,9 @@ class ComboValidationTest(unittest.TestCase):
class RingValidationTest(unittest.TestCase):
def test_slot_stride_must_keep_atomic_fields_aligned(self) -> None:
with self.assertRaisesRegex(ValueError, 'multiple of 8'):
validate_ring_endpoint(RingEndpointModel(name='r', slot_size_bytes=9))
def test_capacity_and_slot_must_be_positive(self) -> None:
with self.assertRaisesRegex(ValueError, "capacity"):
validate_ring_endpoint(RingEndpointModel(name="r", capacity=0, slot_size_bytes=16))
@@ -189,6 +298,11 @@ class GprValidationTest(unittest.TestCase):
rx_geometry=[GprRxGeometryModel(input_pos=0, x_m=0.0, y_m=0.0, z_m=0.0)],
))
def test_direct_geometry_positions_cannot_be_silently_truncated(self) -> None:
for value in (0.5, True, '0'):
with self.subTest(value=value), self.assertRaisesRegex(ValueError, 'integer'):
self._validate(self._gpr(tx_geometry=[GprTxGeometryModel(output_pos=value)]))
class ParseCombosFromTextTest(unittest.TestCase):
def test_parses_pairs(self) -> None: