"""Neutral preprocessing grids must preserve the actual radar data unchanged.""" from __future__ import annotations import unittest import numpy as np from python_app.hardware_full.point_target_mock_service import PointTargetMockService from python_app.models.run_config_schema import RunConfigModel from python_app.workflows.neutral_preprocess import build_neutral_s21_sets, supports_neutral_preprocess_sets def _rph_config(*, points: int = 5) -> RunConfigModel: return RunConfigModel.from_dict( { "radar": { "model": "rph_radar", "rph_radar": { "band": {"start_hz": 2_100_000_000.0, "stop_hz": 5_500_000_000.0, "points": points}, }, }, "switches": {"port1": {"positions": 1}, "port2": {"positions": 2}}, "run": {"combos": [{"input": 0, "output": 0}, {"input": 1, "output": 0}]}, } ) class RphRadarNeutralPreprocessTest(unittest.TestCase): def test_builds_passthrough_s21_sets_on_band_grid(self) -> None: calibration, reference = build_neutral_s21_sets(_rph_config(points=5)) expected_frequency = np.linspace(2_100_000_000.0, 5_500_000_000.0, 5).astype(np.float32) self.assertEqual(len(calibration.traces), 2) self.assertEqual(len(reference.traces), 2) self.assertEqual( [(trace.combo.input, trace.combo.output) for trace in calibration.traces], [(0, 0), (1, 0)], ) for trace in calibration.traces: np.testing.assert_array_equal(trace.frequency_hz, expected_frequency) np.testing.assert_array_equal(trace.s21, np.ones(5, dtype=np.complex64)) for trace in reference.traces: np.testing.assert_array_equal(trace.frequency_hz, expected_frequency) np.testing.assert_array_equal(trace.s21, np.zeros(5, dtype=np.complex64)) def test_grid_matches_processor_grid(self) -> None: """The neutral axis must be byte-identical to the acquisition grid.""" from python_app.hardware_full.rph_radar.processing import RphRadarSweepProcessor config = _rph_config(points=17) processor = RphRadarSweepProcessor(config.radar.rph_radar) calibration, _reference = build_neutral_s21_sets(config) np.testing.assert_array_equal(calibration.traces[0].frequency_hz, processor.grid_hz) def test_rejects_degenerate_band(self) -> None: with self.assertRaisesRegex(ValueError, "band.points must be"): build_neutral_s21_sets(_rph_config(points=1)) def test_dtypes_are_float32_and_complex64(self) -> None: calibration, _reference = build_neutral_s21_sets(_rph_config()) trace = calibration.traces[0] self.assertEqual(trace.frequency_hz.dtype, np.float32) self.assertEqual(trace.s21.dtype, np.complex64) def test_calibration_s21_is_a_nonzero_divisor(self) -> None: # The preprocessor divides measured/calibration, so calibration S21 # must never be zero — that is the whole point of the '1+0j neutral' contract. calibration, _reference = build_neutral_s21_sets(_rph_config()) for trace in calibration.traces: self.assertTrue(bool(np.all(trace.s21 != 0))) def test_single_mock_grid_is_exact_and_neutral_s21_is_passthrough(self) -> None: config = RunConfigModel.from_dict({"radar": { "model": "librevna", "driver_mode": "mock", "sweep": {"start_hz": 1e6, "stop_hz": 6e9, "points": 201}, }}) self.assertTrue(supports_neutral_preprocess_sets(config)) radar = PointTargetMockService(config) radar.configure(config.radar.sweep) measured = radar.acquire_collection() calibration, reference = build_neutral_s21_sets(config) for trace, cal, ref in zip(measured.traces, calibration.traces, reference.traces): np.testing.assert_array_equal(trace.frequency_hz, cal.frequency_hz) np.testing.assert_array_equal(trace.frequency_hz, ref.frequency_hz) np.testing.assert_array_equal(trace.s21 / cal.s21 - ref.s21, trace.s21) def test_native_single_axis_remains_outside_verified_shortcut(self) -> None: config = RunConfigModel.from_dict({"radar": {"model": "librevna", "driver_mode": "native"}}) self.assertFalse(supports_neutral_preprocess_sets(config)) if __name__ == "__main__": unittest.main()