Refactor radar system
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user