Rework radar system

This commit is contained in:
Ayzen
2026-10-06 12:32:26 +03:00
parent 552cdc688a
commit 5bb86b7165
299 changed files with 13537 additions and 27763 deletions
+14 -11
View File
@@ -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)