fix(gui): align radar parameters to FPGA truth (radar_scene.py)
- Bandwidth 500 MHz -> 20 MHz, sample rate 4 MHz -> 100 MHz (DDC output) - Range formula: deramped FMCW -> matched-filter c/(2*Fs)*decimation - Velocity formula: use PRI (167 us) and chirps_per_subframe (16) - Carrier frequency: 10.525 GHz -> 10.5 GHz per radar_scene.py - Range per bin: 4.8 m -> 24 m, max range: 307 m -> 1536 m - Fix simulator target spawn range to match new coverage (50-1400 m) - Remove dead BANDWIDTH constant, add SAMPLE_RATE to V65 Tk - All 174 tests pass, ruff clean
This commit is contained in:
@@ -98,9 +98,10 @@ class DemoTarget:
|
|||||||
|
|
||||||
__slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity")
|
__slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity")
|
||||||
|
|
||||||
# Physical range grid: 64 bins x ~4.8 m/bin = ~307 m max
|
# Physical range grid: 64 bins x ~24 m/bin = ~1536 m max
|
||||||
_RANGE_PER_BIN: float = (3e8 / (2 * 500e6)) * 16 # ~4.8 m
|
# Bin spacing = c / (2 * Fs) * decimation, where Fs = 100 MHz DDC output.
|
||||||
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~307 m
|
_RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 16 # ~24 m
|
||||||
|
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~1536 m
|
||||||
|
|
||||||
def __init__(self, tid: int):
|
def __init__(self, tid: int):
|
||||||
self.id = tid
|
self.id = tid
|
||||||
@@ -187,10 +188,10 @@ class DemoSimulator:
|
|||||||
mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64)
|
mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64)
|
||||||
det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8)
|
det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8)
|
||||||
|
|
||||||
# Range/Doppler scaling (approximate)
|
# Range/Doppler scaling: bin spacing = c/(2*Fs)*decimation
|
||||||
range_per_bin = (3e8 / (2 * 500e6)) * 16 # ~4.8 m/bin
|
range_per_bin = (3e8 / (2 * 100e6)) * 16 # ~24 m/bin
|
||||||
max_range = range_per_bin * NUM_RANGE_BINS
|
max_range = range_per_bin * NUM_RANGE_BINS
|
||||||
vel_per_bin = 1.484 # m/s per Doppler bin (from WaveformConfig)
|
vel_per_bin = 5.34 # m/s per Doppler bin (radar_scene.py: lam/(2*16*PRI))
|
||||||
|
|
||||||
for t in targets:
|
for t in targets:
|
||||||
if t.range_m > max_range or t.range_m < 0:
|
if t.range_m > max_range or t.range_m < 0:
|
||||||
@@ -385,7 +386,7 @@ class RadarDashboard:
|
|||||||
UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh
|
UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh
|
||||||
|
|
||||||
# Radar parameters used for range-axis scaling.
|
# Radar parameters used for range-axis scaling.
|
||||||
BANDWIDTH = 500e6 # Hz — chirp bandwidth
|
SAMPLE_RATE = 100e6 # Hz — DDC output I/Q rate (matched filter input)
|
||||||
C = 3e8 # m/s — speed of light
|
C = 3e8 # m/s — speed of light
|
||||||
|
|
||||||
def __init__(self, root: tk.Tk, mock: bool,
|
def __init__(self, root: tk.Tk, mock: bool,
|
||||||
@@ -526,9 +527,8 @@ class RadarDashboard:
|
|||||||
|
|
||||||
def _build_display_tab(self, parent):
|
def _build_display_tab(self, parent):
|
||||||
# Compute physical axis limits
|
# Compute physical axis limits
|
||||||
range_res = self.C / (2.0 * self.BANDWIDTH) # ~0.3 m per FFT bin
|
# Bin spacing = c / (2 * Fs_ddc) for matched-filter processing.
|
||||||
# After decimation 1024→64, each range bin = 16 FFT bins
|
range_per_bin = self.C / (2.0 * self.SAMPLE_RATE) * 16 # ~24 m
|
||||||
range_per_bin = range_res * 16
|
|
||||||
max_range = range_per_bin * NUM_RANGE_BINS
|
max_range = range_per_bin * NUM_RANGE_BINS
|
||||||
|
|
||||||
doppler_bin_lo = 0
|
doppler_bin_lo = 0
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ class TestRadarSettings(unittest.TestCase):
|
|||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
s = _models().RadarSettings()
|
s = _models().RadarSettings()
|
||||||
self.assertEqual(s.system_frequency, 10e9)
|
self.assertEqual(s.system_frequency, 10.5e9)
|
||||||
self.assertEqual(s.coverage_radius, 50000)
|
self.assertEqual(s.coverage_radius, 1536)
|
||||||
self.assertEqual(s.max_distance, 50000)
|
self.assertEqual(s.max_distance, 1536)
|
||||||
|
|
||||||
|
|
||||||
class TestGPSData(unittest.TestCase):
|
class TestGPSData(unittest.TestCase):
|
||||||
@@ -425,26 +425,28 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertEqual(wc.sample_rate_hz, 4e6)
|
self.assertEqual(wc.sample_rate_hz, 100e6)
|
||||||
self.assertEqual(wc.bandwidth_hz, 500e6)
|
self.assertEqual(wc.bandwidth_hz, 20e6)
|
||||||
self.assertEqual(wc.chirp_duration_s, 300e-6)
|
self.assertEqual(wc.chirp_duration_s, 30e-6)
|
||||||
self.assertEqual(wc.center_freq_hz, 10.525e9)
|
self.assertEqual(wc.pri_s, 167e-6)
|
||||||
|
self.assertEqual(wc.center_freq_hz, 10.5e9)
|
||||||
self.assertEqual(wc.n_range_bins, 64)
|
self.assertEqual(wc.n_range_bins, 64)
|
||||||
self.assertEqual(wc.n_doppler_bins, 32)
|
self.assertEqual(wc.n_doppler_bins, 32)
|
||||||
|
self.assertEqual(wc.chirps_per_subframe, 16)
|
||||||
self.assertEqual(wc.fft_size, 1024)
|
self.assertEqual(wc.fft_size, 1024)
|
||||||
self.assertEqual(wc.decimation_factor, 16)
|
self.assertEqual(wc.decimation_factor, 16)
|
||||||
|
|
||||||
def test_range_resolution(self):
|
def test_range_resolution(self):
|
||||||
"""range_resolution_m should be ~5.62 m/bin with ADI defaults."""
|
"""range_resolution_m should be ~23.98 m/bin (matched filter, 100 MSPS)."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertAlmostEqual(wc.range_resolution_m, 5.621, places=1)
|
self.assertAlmostEqual(wc.range_resolution_m, 23.983, places=1)
|
||||||
|
|
||||||
def test_velocity_resolution(self):
|
def test_velocity_resolution(self):
|
||||||
"""velocity_resolution_mps should be ~1.484 m/s/bin."""
|
"""velocity_resolution_mps should be ~5.34 m/s/bin (PRI=167us, 16 chirps)."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertAlmostEqual(wc.velocity_resolution_mps, 1.484, places=2)
|
self.assertAlmostEqual(wc.velocity_resolution_mps, 5.343, places=1)
|
||||||
|
|
||||||
def test_max_range(self):
|
def test_max_range(self):
|
||||||
"""max_range_m = range_resolution * n_range_bins."""
|
"""max_range_m = range_resolution * n_range_bins."""
|
||||||
@@ -466,7 +468,7 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
"""Non-default parameters correctly change derived values."""
|
"""Non-default parameters correctly change derived values."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc1 = WaveformConfig()
|
wc1 = WaveformConfig()
|
||||||
wc2 = WaveformConfig(bandwidth_hz=1e9) # double BW → halve range res
|
wc2 = WaveformConfig(sample_rate_hz=200e6) # double Fs → halve range bin
|
||||||
self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2)
|
self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2)
|
||||||
|
|
||||||
def test_zero_center_freq_velocity(self):
|
def test_zero_center_freq_velocity(self):
|
||||||
@@ -925,9 +927,9 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
|||||||
"""Detection at range bin 10 → range = 10 * range_resolution."""
|
"""Detection at range bin 10 → range = 10 * range_resolution."""
|
||||||
from v7.processing import extract_targets_from_frame
|
from v7.processing import extract_targets_from_frame
|
||||||
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0
|
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0
|
||||||
targets = extract_targets_from_frame(frame, range_resolution=5.621)
|
targets = extract_targets_from_frame(frame, range_resolution=23.983)
|
||||||
self.assertEqual(len(targets), 1)
|
self.assertEqual(len(targets), 1)
|
||||||
self.assertAlmostEqual(targets[0].range, 10 * 5.621, places=2)
|
self.assertAlmostEqual(targets[0].range, 10 * 23.983, places=1)
|
||||||
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
|
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
|
||||||
|
|
||||||
def test_velocity_sign(self):
|
def test_velocity_sign(self):
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class RadarMapWidget(QWidget):
|
|||||||
)
|
)
|
||||||
self._targets: list[RadarTarget] = []
|
self._targets: list[RadarTarget] = []
|
||||||
self._pending_targets: list[RadarTarget] | None = None
|
self._pending_targets: list[RadarTarget] | None = None
|
||||||
self._coverage_radius = 50_000 # metres
|
self._coverage_radius = 1_536 # metres (64 bins x ~24 m/bin)
|
||||||
self._tile_server = TileServer.OPENSTREETMAP
|
self._tile_server = TileServer.OPENSTREETMAP
|
||||||
self._show_coverage = True
|
self._show_coverage = True
|
||||||
self._show_trails = False
|
self._show_trails = False
|
||||||
|
|||||||
@@ -108,12 +108,12 @@ class RadarSettings:
|
|||||||
range_resolution and velocity_resolution should be calibrated to
|
range_resolution and velocity_resolution should be calibrated to
|
||||||
the actual waveform parameters.
|
the actual waveform parameters.
|
||||||
"""
|
"""
|
||||||
system_frequency: float = 10e9 # Hz (carrier, used for velocity calc)
|
system_frequency: float = 10.5e9 # Hz (carrier, used for velocity calc)
|
||||||
range_resolution: float = 781.25 # Meters per range bin (default: 50km/64)
|
range_resolution: float = 24.0 # Meters per range bin (c/(2*Fs)*decim)
|
||||||
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform)
|
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform)
|
||||||
max_distance: float = 50000 # Max detection range (m)
|
max_distance: float = 1536 # Max detection range (m)
|
||||||
map_size: float = 50000 # Map display size (m)
|
map_size: float = 2000 # Map display size (m)
|
||||||
coverage_radius: float = 50000 # Map coverage radius (m)
|
coverage_radius: float = 1536 # Map coverage radius (m)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -199,39 +199,46 @@ class WaveformConfig:
|
|||||||
Encapsulates the radar waveform so that range/velocity resolution
|
Encapsulates the radar waveform so that range/velocity resolution
|
||||||
can be derived automatically instead of hardcoded in RadarSettings.
|
can be derived automatically instead of hardcoded in RadarSettings.
|
||||||
|
|
||||||
Defaults match the ADI CN0566 Phaser capture parameters used in
|
Defaults match the AERIS-10 production system parameters from
|
||||||
the golden_reference cosim (4 MSPS, 500 MHz BW, 300 us chirp).
|
radar_scene.py / plfm_chirp_controller.v:
|
||||||
|
100 MSPS DDC output, 20 MHz chirp BW, 30 us long chirp,
|
||||||
|
167 us long-chirp PRI, X-band 10.5 GHz carrier.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sample_rate_hz: float = 4e6 # ADC sample rate
|
sample_rate_hz: float = 100e6 # DDC output I/Q rate (matched filter input)
|
||||||
bandwidth_hz: float = 500e6 # Chirp bandwidth
|
bandwidth_hz: float = 20e6 # Chirp bandwidth (not used in range calc;
|
||||||
chirp_duration_s: float = 300e-6 # Chirp ramp time
|
# retained for time-bandwidth product / display)
|
||||||
center_freq_hz: float = 10.525e9 # Carrier frequency
|
chirp_duration_s: float = 30e-6 # Long chirp ramp time
|
||||||
|
pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen)
|
||||||
|
center_freq_hz: float = 10.5e9 # Carrier frequency (radar_scene.py: F_CARRIER)
|
||||||
n_range_bins: int = 64 # After decimation
|
n_range_bins: int = 64 # After decimation
|
||||||
n_doppler_bins: int = 32 # After Doppler FFT
|
n_doppler_bins: int = 32 # Total Doppler bins (2 sub-frames x 16)
|
||||||
|
chirps_per_subframe: int = 16 # Chirps in one Doppler sub-frame
|
||||||
fft_size: int = 1024 # Pre-decimation FFT length
|
fft_size: int = 1024 # Pre-decimation FFT length
|
||||||
decimation_factor: int = 16 # 1024 → 64
|
decimation_factor: int = 16 # 1024 → 64
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def range_resolution_m(self) -> float:
|
def range_resolution_m(self) -> float:
|
||||||
"""Meters per decimated range bin (FMCW deramped baseband).
|
"""Meters per decimated range bin (matched-filter pulse compression).
|
||||||
|
|
||||||
For deramped FMCW: bin spacing = c * Fs * T / (2 * N_FFT * BW).
|
For FFT-based matched filtering, each IFFT output bin spans
|
||||||
After decimation the bin spacing grows by *decimation_factor*.
|
c / (2 * Fs) in range, where Fs is the I/Q sample rate at the
|
||||||
|
matched-filter input (DDC output). After decimation the bin
|
||||||
|
spacing grows by *decimation_factor*.
|
||||||
"""
|
"""
|
||||||
c = 299_792_458.0
|
c = 299_792_458.0
|
||||||
raw_bin = (
|
raw_bin = c / (2.0 * self.sample_rate_hz)
|
||||||
c * self.sample_rate_hz * self.chirp_duration_s
|
|
||||||
/ (2.0 * self.fft_size * self.bandwidth_hz)
|
|
||||||
)
|
|
||||||
return raw_bin * self.decimation_factor
|
return raw_bin * self.decimation_factor
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def velocity_resolution_mps(self) -> float:
|
def velocity_resolution_mps(self) -> float:
|
||||||
"""m/s per Doppler bin. lambda / (2 * n_doppler * chirp_duration)."""
|
"""m/s per Doppler bin.
|
||||||
|
|
||||||
|
lambda / (2 * chirps_per_subframe * PRI), matching radar_scene.py.
|
||||||
|
"""
|
||||||
c = 299_792_458.0
|
c = 299_792_458.0
|
||||||
wavelength = c / self.center_freq_hz
|
wavelength = c / self.center_freq_hz
|
||||||
return wavelength / (2.0 * self.n_doppler_bins * self.chirp_duration_s)
|
return wavelength / (2.0 * self.chirps_per_subframe * self.pri_s)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_range_m(self) -> float:
|
def max_range_m(self) -> float:
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ class TargetSimulator(QObject):
|
|||||||
self._add_random_target()
|
self._add_random_target()
|
||||||
|
|
||||||
def _add_random_target(self):
|
def _add_random_target(self):
|
||||||
range_m = random.uniform(5000, 40000)
|
range_m = random.uniform(50, 1400)
|
||||||
azimuth = random.uniform(0, 360)
|
azimuth = random.uniform(0, 360)
|
||||||
velocity = random.uniform(-100, 100)
|
velocity = random.uniform(-100, 100)
|
||||||
elevation = random.uniform(-5, 45)
|
elevation = random.uniform(-5, 45)
|
||||||
@@ -368,7 +368,7 @@ class TargetSimulator(QObject):
|
|||||||
|
|
||||||
for t in self._targets:
|
for t in self._targets:
|
||||||
new_range = t.range - t.velocity * 0.5
|
new_range = t.range - t.velocity * 0.5
|
||||||
if new_range < 500 or new_range > 50000:
|
if new_range < 10 or new_range > 1536:
|
||||||
continue # target exits coverage — drop it
|
continue # target exits coverage — drop it
|
||||||
|
|
||||||
new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))
|
new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))
|
||||||
|
|||||||
Reference in New Issue
Block a user