Rework radar system
This commit is contained in:
@@ -9,6 +9,7 @@ accumulating forward) and lets us reason precisely about the frequency mapping.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
@@ -26,7 +27,7 @@ def _reference(phase: np.ndarray, amplitude: float = 1000.0) -> np.ndarray:
|
||||
|
||||
# Calibration shared by the processor tests: phase 0 rad -> 2 GHz, phase 100 rad
|
||||
# -> 4 GHz (slope 2e7 Hz/rad). Band [2.5, 3.5] GHz corresponds to phase [25, 75].
|
||||
_CALIBRATION = dict(
|
||||
_CALIBRATION = KamilAdcProcessingParams(
|
||||
phase0_rad=0.0,
|
||||
freq0_hz=2_000_000_000.0,
|
||||
phase1_rad=100.0,
|
||||
@@ -39,7 +40,7 @@ _CALIBRATION = dict(
|
||||
|
||||
class ProcessingParamsTest(unittest.TestCase):
|
||||
def _params(self, **overrides: float) -> KamilAdcProcessingParams:
|
||||
return KamilAdcProcessingParams(**{**_CALIBRATION, **overrides}) # type: ignore[arg-type]
|
||||
return replace(_CALIBRATION, **overrides)
|
||||
|
||||
def test_slope_is_constant_from_anchors(self) -> None:
|
||||
self.assertAlmostEqual(self._params().hz_per_rad, 2.0e7)
|
||||
@@ -63,7 +64,7 @@ class ProcessingParamsTest(unittest.TestCase):
|
||||
|
||||
class SweepProcessorTest(unittest.TestCase):
|
||||
def _processor(self, **overrides: float) -> KamilAdcSweepProcessor:
|
||||
return KamilAdcSweepProcessor(KamilAdcProcessingParams(**{**_CALIBRATION, **overrides})) # type: ignore[arg-type]
|
||||
return KamilAdcSweepProcessor(replace(_CALIBRATION, **overrides))
|
||||
|
||||
def test_grid_is_fixed_and_identical_across_calls(self) -> None:
|
||||
processor = self._processor()
|
||||
@@ -91,8 +92,8 @@ class SweepProcessorTest(unittest.TestCase):
|
||||
line = lambda ref: ((processor.reference_frequency_axis(ref) - 3.0e9) / 1.0e9) * np.abs(ref)
|
||||
s_short = processor.process(line(ref_short), ref_short)
|
||||
s_long = processor.process(line(ref_long), ref_long)
|
||||
self.assertIsNotNone(s_short)
|
||||
self.assertIsNotNone(s_long)
|
||||
assert s_short is not None
|
||||
assert s_long is not None
|
||||
expected = (processor.grid_hz.astype(np.float64) - 3.0e9) / 1.0e9
|
||||
# Endpoint normalization would compress the longer sweep's axis and break this.
|
||||
np.testing.assert_allclose(s_short.real, expected, atol=1e-3)
|
||||
@@ -105,7 +106,7 @@ class SweepProcessorTest(unittest.TestCase):
|
||||
# |main| = 8 everywhere -> |S| = 8 / 4 = 2.
|
||||
main = 8.0 * np.exp(1j * np.angle(ref))
|
||||
result = processor.process(main, ref)
|
||||
self.assertIsNotNone(result)
|
||||
assert result is not None
|
||||
np.testing.assert_allclose(np.abs(result), 2.0, atol=1e-3)
|
||||
|
||||
def test_rejects_sweep_that_does_not_cover_band(self) -> None:
|
||||
@@ -118,7 +119,7 @@ class SweepProcessorTest(unittest.TestCase):
|
||||
processor = self._processor()
|
||||
ref = _reference(np.linspace(0.0, 100.0, 401)) # freq [2.0, 4.0] GHz
|
||||
result = processor.process(np.abs(ref).astype(np.complex128), ref)
|
||||
self.assertIsNotNone(result)
|
||||
assert result is not None
|
||||
self.assertEqual(result.shape, (11,))
|
||||
self.assertEqual(result.dtype, np.complex64)
|
||||
np.testing.assert_allclose(np.abs(result), 1.0, atol=1e-3) # main=|ref| -> |S|=1
|
||||
@@ -130,7 +131,7 @@ class SweepProcessorTest(unittest.TestCase):
|
||||
# S(f) = (f - 2.5 GHz) / 1 GHz -> must resample to that same line on the grid.
|
||||
main = ((freqs - 2.5e9) / 1.0e9) * np.abs(ref)
|
||||
result = processor.process(main, ref)
|
||||
self.assertIsNotNone(result)
|
||||
assert result is not None
|
||||
expected = (processor.grid_hz.astype(np.float64) - 2.5e9) / 1.0e9
|
||||
np.testing.assert_allclose(result.real, expected, atol=1e-3)
|
||||
np.testing.assert_allclose(result.imag, 0.0, atol=1e-3)
|
||||
@@ -141,7 +142,7 @@ class SweepProcessorTest(unittest.TestCase):
|
||||
processor = self._processor(phase1_rad=-100.0)
|
||||
ref = _reference(np.linspace(0.0, -100.0, 401))
|
||||
result = processor.process(np.abs(ref).astype(np.complex128), ref)
|
||||
self.assertIsNotNone(result)
|
||||
assert result is not None
|
||||
self.assertEqual(result.shape, (11,))
|
||||
np.testing.assert_allclose(np.abs(result), 1.0, atol=1e-3)
|
||||
|
||||
@@ -163,7 +164,7 @@ class SweepProcessorTest(unittest.TestCase):
|
||||
ref[10] = 0.0 # vanished reference samples must be ignored, not crash
|
||||
ref[200] = 0.0
|
||||
result = processor.process(main, ref)
|
||||
self.assertIsNotNone(result)
|
||||
assert result is not None
|
||||
self.assertTrue(np.all(np.isfinite(result.real)))
|
||||
self.assertTrue(np.all(np.isfinite(result.imag)))
|
||||
|
||||
@@ -195,7 +196,7 @@ class SweepProcessorTest(unittest.TestCase):
|
||||
covering = _reference(np.linspace(0.0, 100.0, 401))
|
||||
self.assertIsNotNone(processor.process(np.abs(covering).astype(np.complex128), covering))
|
||||
anchor_after_accept = processor._previous_anchor_rad
|
||||
self.assertIsNotNone(anchor_after_accept)
|
||||
assert anchor_after_accept is not None
|
||||
short = _reference(np.linspace(0.0, 40.0, 201)) # does not span the band
|
||||
self.assertIsNone(processor.process(np.ones(201, dtype=np.complex128), short))
|
||||
self.assertEqual(processor._previous_anchor_rad, anchor_after_accept)
|
||||
@@ -210,10 +211,12 @@ class SweepProcessorTest(unittest.TestCase):
|
||||
ref_home = _reference(ramp_home)
|
||||
ref_drift = _reference(ramp_drift)
|
||||
self.assertIsNotNone(processor.process(np.abs(ref_home).astype(np.complex128), ref_home))
|
||||
assert processor._previous_anchor_rad is not None
|
||||
self.assertAlmostEqual(processor._previous_anchor_rad, 3.0, places=2)
|
||||
self.assertIsNotNone(processor.process(np.abs(ref_drift).astype(np.complex128), ref_drift))
|
||||
# Without correction this would be ~-2.98 (one turn below); corrected it
|
||||
# continues smoothly from 3.0 to ~3.3.
|
||||
assert processor._previous_anchor_rad is not None
|
||||
self.assertAlmostEqual(processor._previous_anchor_rad, 3.3, places=2)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user