mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-11 07:51:17 +00:00
fix(gui): PR-Q.4 — per-subframe WaveformConfig + 48-bin parser (C-5)
Refactor v7.WaveformConfig from single-PRI to PR-Q's 3-PRI staggered
ladder (SHORT 175 us / MEDIUM 161 us / LONG 167 us) and update the
host-side bulk-frame parser dimension to match the FPGA's 48-bin
Doppler output (RP_NUM_DOPPLER_BINS = 48). The parser was rejecting
every production frame with n_doppler != 32, masking the PR-F widening
end-to-end.
WaveformConfig:
- pri_short_s/pri_medium_s/pri_long_s replace single pri_s
- n_doppler_bins 32 -> 48; new num_subframes=3
- Per-subframe velocity_resolution_{short,medium,long}_mps
- Per-subframe max_velocity_{short,medium,long}_mps
- extended_max_velocity_mps_crt(K=6) for 3-PRI alias-resolution ceiling
- Drop pri_s, velocity_resolution_mps, max_velocity_mps (no aliases)
Other:
- radar_protocol.NUM_DOPPLER_BINS 32 -> 48 (NUM_CELLS auto 16384 -> 24576;
BULK_FRAME_MAX_SIZE flows from NUM_CELLS, no other edits needed)
- v7/dashboard.py constant + stale "(64x32)" title replaced with f-string
- v7/processing.py 32-bin fallback -> 48
- v7/workers.py: derive doppler_center from frame.shape; LONG-PRI v_res
used as conservative single-PRI placeholder until PR-Q.5 lands the
CRT extractor (markers in place at both call sites)
- test_v7.py: TestWaveformConfig rewritten (8 tests, per-subframe + CRT
extension); TestExtractTargetsFromFrame center 16 -> 24
Local tests:
TestWaveformConfig 8/8 PASS
TestExtractTargetsFromFrame 6/6 PASS
test_GUI_V65_Tk 117/0/2 PASS
This commit is contained in:
@@ -68,8 +68,8 @@ DATA_PACKET_SIZE = 11 # 1 + 4 + 2 + 2 + 1 + 1
|
|||||||
STATUS_PACKET_SIZE = 26 # 1 + 24 + 1
|
STATUS_PACKET_SIZE = 26 # 1 + 24 + 1
|
||||||
|
|
||||||
NUM_RANGE_BINS = 512
|
NUM_RANGE_BINS = 512
|
||||||
NUM_DOPPLER_BINS = 32
|
NUM_DOPPLER_BINS = 48 # PR-F/PR-Q: 3 sub-frames * 16 (matches FPGA RP_NUM_DOPPLER_BINS)
|
||||||
NUM_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS # 16384
|
NUM_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS # 24576
|
||||||
|
|
||||||
WATERFALL_DEPTH = 64
|
WATERFALL_DEPTH = 64
|
||||||
|
|
||||||
|
|||||||
@@ -428,10 +428,14 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
self.assertEqual(wc.sample_rate_hz, 100e6)
|
self.assertEqual(wc.sample_rate_hz, 100e6)
|
||||||
self.assertEqual(wc.bandwidth_hz, 20e6)
|
self.assertEqual(wc.bandwidth_hz, 20e6)
|
||||||
self.assertEqual(wc.chirp_duration_s, 30e-6)
|
self.assertEqual(wc.chirp_duration_s, 30e-6)
|
||||||
self.assertEqual(wc.pri_s, 167e-6)
|
# PR-Q: 3 staggered PRIs (SHORT 175, MEDIUM 161, LONG 167 us)
|
||||||
|
self.assertEqual(wc.pri_short_s, 175e-6)
|
||||||
|
self.assertEqual(wc.pri_medium_s, 161e-6)
|
||||||
|
self.assertEqual(wc.pri_long_s, 167e-6)
|
||||||
self.assertEqual(wc.center_freq_hz, 10.5e9)
|
self.assertEqual(wc.center_freq_hz, 10.5e9)
|
||||||
self.assertEqual(wc.n_range_bins, 512)
|
self.assertEqual(wc.n_range_bins, 512)
|
||||||
self.assertEqual(wc.n_doppler_bins, 32)
|
self.assertEqual(wc.n_doppler_bins, 48)
|
||||||
|
self.assertEqual(wc.num_subframes, 3)
|
||||||
self.assertEqual(wc.chirps_per_subframe, 16)
|
self.assertEqual(wc.chirps_per_subframe, 16)
|
||||||
self.assertEqual(wc.fft_size, 2048)
|
self.assertEqual(wc.fft_size, 2048)
|
||||||
self.assertEqual(wc.decimation_factor, 4)
|
self.assertEqual(wc.decimation_factor, 4)
|
||||||
@@ -442,11 +446,20 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertAlmostEqual(wc.range_resolution_m, 5.996, places=2)
|
self.assertAlmostEqual(wc.range_resolution_m, 5.996, places=2)
|
||||||
|
|
||||||
def test_velocity_resolution(self):
|
def test_velocity_resolution_per_subframe(self):
|
||||||
"""velocity_resolution_mps should be ~5.34 m/s/bin (PRI=167us, 16 chirps)."""
|
"""Per-subframe v_res = lambda / (2 * 16 * PRI), PR-Q stagger."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertAlmostEqual(wc.velocity_resolution_mps, 5.343, places=1)
|
# lambda = c / 10.5e9 = 0.02856 m
|
||||||
|
# SHORT 175 us: 0.02856 / (32 * 175e-6) = 5.099 m/s/bin
|
||||||
|
# MEDIUM 161 us: 0.02856 / (32 * 161e-6) = 5.543 m/s/bin
|
||||||
|
# LONG 167 us: 0.02856 / (32 * 167e-6) = 5.343 m/s/bin
|
||||||
|
self.assertAlmostEqual(wc.velocity_resolution_short_mps, 5.099, places=2)
|
||||||
|
self.assertAlmostEqual(wc.velocity_resolution_medium_mps, 5.543, places=2)
|
||||||
|
self.assertAlmostEqual(wc.velocity_resolution_long_mps, 5.343, places=2)
|
||||||
|
# Smallest PRI (MEDIUM) gives largest v_res → largest v_unamb.
|
||||||
|
self.assertGreater(wc.velocity_resolution_medium_mps, wc.velocity_resolution_long_mps)
|
||||||
|
self.assertGreater(wc.velocity_resolution_medium_mps, wc.velocity_resolution_short_mps)
|
||||||
|
|
||||||
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."""
|
||||||
@@ -454,15 +467,29 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertAlmostEqual(wc.max_range_m, wc.range_resolution_m * 512, places=1)
|
self.assertAlmostEqual(wc.max_range_m, wc.range_resolution_m * 512, places=1)
|
||||||
|
|
||||||
def test_max_velocity(self):
|
def test_max_velocity_per_subframe(self):
|
||||||
"""max_velocity_mps = velocity_resolution * n_doppler_bins / 2."""
|
"""Per-subframe v_unamb = v_res * chirps_per_subframe / 2."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertAlmostEqual(
|
for vmax, vres in [
|
||||||
wc.max_velocity_mps,
|
(wc.max_velocity_short_mps, wc.velocity_resolution_short_mps),
|
||||||
wc.velocity_resolution_mps * 16,
|
(wc.max_velocity_medium_mps, wc.velocity_resolution_medium_mps),
|
||||||
places=2,
|
(wc.max_velocity_long_mps, wc.velocity_resolution_long_mps),
|
||||||
)
|
]:
|
||||||
|
self.assertAlmostEqual(vmax, vres * 8.0, places=2)
|
||||||
|
|
||||||
|
def test_extended_max_velocity_crt(self):
|
||||||
|
"""CRT-extended v_unamb = max(per-subframe v_unamb) * K."""
|
||||||
|
from v7.models import WaveformConfig
|
||||||
|
wc = WaveformConfig()
|
||||||
|
# MEDIUM has the largest per-subframe v_unamb (smallest PRI).
|
||||||
|
# K=6 default → ~266 m/s; well above UAS speeds 50–80 m/s.
|
||||||
|
v6 = wc.extended_max_velocity_mps_crt()
|
||||||
|
self.assertAlmostEqual(v6, wc.max_velocity_medium_mps * 6, places=2)
|
||||||
|
# K=3 should give half of K=6.
|
||||||
|
v3 = wc.extended_max_velocity_mps_crt(max_alias_k=3)
|
||||||
|
self.assertAlmostEqual(v3, wc.max_velocity_medium_mps * 3, places=2)
|
||||||
|
self.assertAlmostEqual(v6, 2.0 * v3, places=2)
|
||||||
|
|
||||||
def test_custom_params(self):
|
def test_custom_params(self):
|
||||||
"""Non-default parameters correctly change derived values."""
|
"""Non-default parameters correctly change derived values."""
|
||||||
@@ -472,11 +499,15 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
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):
|
||||||
"""Zero center freq should cause ZeroDivisionError in velocity calc."""
|
"""Zero center freq should ZeroDivisionError in any per-subframe velocity calc."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig(center_freq_hz=0.0)
|
wc = WaveformConfig(center_freq_hz=0.0)
|
||||||
with self.assertRaises(ZeroDivisionError):
|
with self.assertRaises(ZeroDivisionError):
|
||||||
_ = wc.velocity_resolution_mps
|
_ = wc.velocity_resolution_long_mps
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
_ = wc.velocity_resolution_short_mps
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
_ = wc.velocity_resolution_medium_mps
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -926,7 +957,8 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
|||||||
def test_single_detection_range(self):
|
def test_single_detection_range(self):
|
||||||
"""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
|
# PR-Q: n_doppler_bins=48 → centre bin = 24 (was 16 in 32-bin world).
|
||||||
|
frame = self._make_frame(det_cells=[(10, 24)])
|
||||||
targets = extract_targets_from_frame(frame, range_resolution=5.996)
|
targets = extract_targets_from_frame(frame, range_resolution=5.996)
|
||||||
self.assertEqual(len(targets), 1)
|
self.assertEqual(len(targets), 1)
|
||||||
self.assertAlmostEqual(targets[0].range, 10 * 5.996, places=1)
|
self.assertAlmostEqual(targets[0].range, 10 * 5.996, places=1)
|
||||||
@@ -935,10 +967,11 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
|||||||
def test_velocity_sign(self):
|
def test_velocity_sign(self):
|
||||||
"""Doppler bin < center → negative velocity, > center → positive."""
|
"""Doppler bin < center → negative velocity, > center → positive."""
|
||||||
from v7.processing import extract_targets_from_frame
|
from v7.processing import extract_targets_from_frame
|
||||||
frame = self._make_frame(det_cells=[(5, 10), (5, 20)])
|
# PR-Q: centre = 24 in 48-bin frame. dbin=10 below, dbin=30 above.
|
||||||
|
frame = self._make_frame(det_cells=[(5, 10), (5, 30)])
|
||||||
targets = extract_targets_from_frame(frame, velocity_resolution=1.484)
|
targets = extract_targets_from_frame(frame, velocity_resolution=1.484)
|
||||||
# dbin=10: vel = (10-16)*1.484 = -8.904 (approaching)
|
# dbin=10: vel = (10-24)*1.484 = -20.776 (approaching)
|
||||||
# dbin=20: vel = (20-16)*1.484 = +5.936 (receding)
|
# dbin=30: vel = (30-24)*1.484 = +8.904 (receding)
|
||||||
self.assertLess(targets[0].velocity, 0)
|
self.assertLess(targets[0].velocity, 0)
|
||||||
self.assertGreater(targets[1].velocity, 0)
|
self.assertGreater(targets[1].velocity, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Frame dimensions from FPGA
|
# Frame dimensions from FPGA (mirrors radar_protocol.NUM_*; PR-F/PR-Q)
|
||||||
NUM_RANGE_BINS = 64
|
NUM_RANGE_BINS = 64
|
||||||
NUM_DOPPLER_BINS = 32
|
NUM_DOPPLER_BINS = 48
|
||||||
|
|
||||||
# Force C locale (period as decimal separator) for all QDoubleSpinBox instances.
|
# Force C locale (period as decimal separator) for all QDoubleSpinBox instances.
|
||||||
_C_LOCALE = QLocale(QLocale.Language.C)
|
_C_LOCALE = QLocale(QLocale.Language.C)
|
||||||
@@ -94,7 +94,7 @@ def _make_dspin() -> QDoubleSpinBox:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
class RangeDopplerCanvas(FigureCanvasQTAgg):
|
class RangeDopplerCanvas(FigureCanvasQTAgg):
|
||||||
"""Matplotlib canvas showing the 64x32 Range-Doppler map with dark theme."""
|
"""Matplotlib canvas showing the Range-Doppler map (NUM_RANGE_BINS x NUM_DOPPLER_BINS) with dark theme."""
|
||||||
|
|
||||||
def __init__(self, _parent=None):
|
def __init__(self, _parent=None):
|
||||||
fig = Figure(figsize=(10, 6), facecolor=DARK_BG)
|
fig = Figure(figsize=(10, 6), facecolor=DARK_BG)
|
||||||
@@ -106,7 +106,10 @@ class RangeDopplerCanvas(FigureCanvasQTAgg):
|
|||||||
extent=[0, NUM_DOPPLER_BINS, 0, NUM_RANGE_BINS], origin="lower",
|
extent=[0, NUM_DOPPLER_BINS, 0, NUM_RANGE_BINS], origin="lower",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.ax.set_title("Range-Doppler Map (64x32)", color=DARK_FG)
|
self.ax.set_title(
|
||||||
|
f"Range-Doppler Map ({NUM_RANGE_BINS}x{NUM_DOPPLER_BINS})",
|
||||||
|
color=DARK_FG,
|
||||||
|
)
|
||||||
self.ax.set_xlabel("Doppler Bin", color=DARK_FG)
|
self.ax.set_xlabel("Doppler Bin", color=DARK_FG)
|
||||||
self.ax.set_ylabel("Range Bin", color=DARK_FG)
|
self.ax.set_ylabel("Range Bin", color=DARK_FG)
|
||||||
self.ax.tick_params(colors=DARK_FG)
|
self.ax.tick_params(colors=DARK_FG)
|
||||||
|
|||||||
@@ -199,56 +199,109 @@ class TileServer(Enum):
|
|||||||
class WaveformConfig:
|
class WaveformConfig:
|
||||||
"""Physical waveform parameters for converting bins to SI units.
|
"""Physical waveform parameters for converting bins to SI units.
|
||||||
|
|
||||||
Encapsulates the radar waveform so that range/velocity resolution
|
PR-Q (3-PRI staggered ladder, audit C-5 Doppler unfolding):
|
||||||
can be derived automatically instead of hardcoded in RadarSettings.
|
- SHORT sub-frame: 1 us chirp / 175 us PRI
|
||||||
|
- MEDIUM sub-frame: 5 us chirp / 161 us PRI
|
||||||
|
- LONG sub-frame: 30 us chirp / 167 us PRI
|
||||||
|
|
||||||
Defaults match the AERIS-10 production system parameters from
|
Each sub-frame produces ``chirps_per_subframe`` Doppler bins
|
||||||
radar_scene.py / plfm_chirp_controller.v:
|
(16 → 48 total). Per-subframe v_unamb is ~+/-42 m/s; the host runs
|
||||||
100 MSPS DDC output, 20 MHz chirp BW, 30 us long chirp,
|
3-PRI Chinese-Remainder unfolding (see PR-Q.5
|
||||||
167 us long-chirp PRI, X-band 10.5 GHz carrier.
|
processing.unfold_velocity_crt) to recover targets out to
|
||||||
|
``extended_max_velocity_mps_crt``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sample_rate_hz: float = 100e6 # DDC output I/Q rate (matched filter input)
|
sample_rate_hz: float = 100e6 # DDC output I/Q rate (matched filter input)
|
||||||
bandwidth_hz: float = 20e6 # Chirp bandwidth (not used in range calc;
|
bandwidth_hz: float = 20e6 # Chirp bandwidth (time-bandwidth product / display)
|
||||||
# retained for time-bandwidth product / display)
|
chirp_duration_s: float = 30e-6 # LONG chirp ramp time (longest of the three)
|
||||||
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 = 512 # After decimation (3 km mode; 4096 in 20 km)
|
|
||||||
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 = 2048 # Pre-decimation FFT length
|
|
||||||
decimation_factor: int = 4 # 2048 → 512
|
|
||||||
|
|
||||||
|
# Per-subframe PRIs (PR-Q stagger; mirrors radar_params.vh
|
||||||
|
# RP_DEF_{SHORT,MEDIUM,LONG}_LISTEN_CYCLES + chirp cycles).
|
||||||
|
pri_short_s: float = 175e-6 # SHORT PRI (1 us chirp + 174 us listen)
|
||||||
|
pri_medium_s: float = 161e-6 # MEDIUM PRI (5 us chirp + 156 us listen)
|
||||||
|
pri_long_s: float = 167e-6 # LONG PRI (30 us chirp + 137 us listen)
|
||||||
|
|
||||||
|
center_freq_hz: float = 10.5e9 # X-band carrier (radar_scene.py F_CARRIER)
|
||||||
|
n_range_bins: int = 512 # After decimation (3 km mode; 4096 in 20 km)
|
||||||
|
n_doppler_bins: int = 48 # 3 sub-frames * 16 chirps (matches RP_NUM_DOPPLER_BINS)
|
||||||
|
chirps_per_subframe: int = 16 # Chirps in one Doppler sub-frame
|
||||||
|
num_subframes: int = 3 # SHORT, MEDIUM, LONG
|
||||||
|
fft_size: int = 2048 # Pre-decimation matched-filter FFT length
|
||||||
|
decimation_factor: int = 4 # 2048 -> 512
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Range
|
||||||
|
# ------------------------------------------------------------------
|
||||||
@property
|
@property
|
||||||
def range_resolution_m(self) -> float:
|
def range_resolution_m(self) -> float:
|
||||||
"""Meters per decimated range bin (matched-filter pulse compression).
|
"""Meters per decimated range bin (matched-filter pulse compression).
|
||||||
|
|
||||||
For FFT-based matched filtering, each IFFT output bin spans
|
Each IFFT output bin spans c / (2 * Fs); after decimation the bin
|
||||||
c / (2 * Fs) in range, where Fs is the I/Q sample rate at the
|
spacing grows by ``decimation_factor``.
|
||||||
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 = c / (2.0 * self.sample_rate_hz)
|
raw_bin = c / (2.0 * self.sample_rate_hz)
|
||||||
return raw_bin * self.decimation_factor
|
return raw_bin * self.decimation_factor
|
||||||
|
|
||||||
@property
|
|
||||||
def velocity_resolution_mps(self) -> float:
|
|
||||||
"""m/s per Doppler bin.
|
|
||||||
|
|
||||||
lambda / (2 * chirps_per_subframe * PRI), matching radar_scene.py.
|
|
||||||
"""
|
|
||||||
c = 299_792_458.0
|
|
||||||
wavelength = c / self.center_freq_hz
|
|
||||||
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:
|
||||||
"""Maximum unambiguous range in meters."""
|
"""Maximum unambiguous range in meters."""
|
||||||
return self.range_resolution_m * self.n_range_bins
|
return self.range_resolution_m * self.n_range_bins
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Velocity (per sub-frame)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _v_res(self, pri_s: float) -> float:
|
||||||
|
c = 299_792_458.0
|
||||||
|
wavelength = c / self.center_freq_hz
|
||||||
|
return wavelength / (2.0 * self.chirps_per_subframe * pri_s)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_velocity_mps(self) -> float:
|
def velocity_resolution_short_mps(self) -> float:
|
||||||
"""Maximum unambiguous velocity (±) in m/s."""
|
"""m/s per Doppler bin in the SHORT sub-frame."""
|
||||||
return self.velocity_resolution_mps * self.n_doppler_bins / 2.0
|
return self._v_res(self.pri_short_s)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def velocity_resolution_medium_mps(self) -> float:
|
||||||
|
"""m/s per Doppler bin in the MEDIUM sub-frame."""
|
||||||
|
return self._v_res(self.pri_medium_s)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def velocity_resolution_long_mps(self) -> float:
|
||||||
|
"""m/s per Doppler bin in the LONG sub-frame."""
|
||||||
|
return self._v_res(self.pri_long_s)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_velocity_short_mps(self) -> float:
|
||||||
|
"""Per-subframe SHORT v_unamb (+/-)."""
|
||||||
|
return self.velocity_resolution_short_mps * self.chirps_per_subframe / 2.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_velocity_medium_mps(self) -> float:
|
||||||
|
"""Per-subframe MEDIUM v_unamb (+/-)."""
|
||||||
|
return self.velocity_resolution_medium_mps * self.chirps_per_subframe / 2.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_velocity_long_mps(self) -> float:
|
||||||
|
"""Per-subframe LONG v_unamb (+/-)."""
|
||||||
|
return self.velocity_resolution_long_mps * self.chirps_per_subframe / 2.0
|
||||||
|
|
||||||
|
def extended_max_velocity_mps_crt(self, max_alias_k: int = 6) -> float:
|
||||||
|
"""CRT-extended unambiguous velocity ceiling (PR-Q C-5).
|
||||||
|
|
||||||
|
Three coprime PRIs let the host resolve aliases up to
|
||||||
|
``max_alias_k`` folds before the alias set itself becomes
|
||||||
|
ambiguous. Returns the velocity beyond which detections must
|
||||||
|
be flagged AMBIGUOUS even after CRT unfolding.
|
||||||
|
|
||||||
|
Ceiling is set by the largest per-subframe v_unamb (smallest
|
||||||
|
PRI) times the alias search depth. For PR-Q stagger
|
||||||
|
(175/161/167 us) with K=6 the practical ceiling is ~266 m/s,
|
||||||
|
well above typical UAS speeds (50-80 m/s).
|
||||||
|
"""
|
||||||
|
v_unamb = max(
|
||||||
|
self.max_velocity_short_mps,
|
||||||
|
self.max_velocity_medium_mps,
|
||||||
|
self.max_velocity_long_mps,
|
||||||
|
)
|
||||||
|
return v_unamb * max_alias_k
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ def extract_targets_from_frame(
|
|||||||
One target per detection cell.
|
One target per detection cell.
|
||||||
"""
|
"""
|
||||||
det_indices = np.argwhere(frame.detections > 0)
|
det_indices = np.argwhere(frame.detections > 0)
|
||||||
n_doppler = frame.detections.shape[1] if frame.detections.ndim == 2 else 32
|
n_doppler = frame.detections.shape[1] if frame.detections.ndim == 2 else 48
|
||||||
doppler_center = n_doppler // 2
|
doppler_center = n_doppler // 2
|
||||||
|
|
||||||
targets: list[RadarTarget] = []
|
targets: list[RadarTarget] = []
|
||||||
|
|||||||
@@ -189,7 +189,15 @@ class RadarDataWorker(QThread):
|
|||||||
# Extract detections from FPGA CFAR flags
|
# Extract detections from FPGA CFAR flags
|
||||||
det_indices = np.argwhere(frame.detections > 0)
|
det_indices = np.argwhere(frame.detections > 0)
|
||||||
r_res = self._waveform.range_resolution_m
|
r_res = self._waveform.range_resolution_m
|
||||||
v_res = self._waveform.velocity_resolution_mps
|
# PR-Q.4: per-subframe Doppler velocity is unfolded by the CRT
|
||||||
|
# extractor in PR-Q.5; until that lands, treat the 48-bin output
|
||||||
|
# as a single-PRI grid using the LONG-PRI v_res (most conservative
|
||||||
|
# — smallest v_unamb). This intentionally yields wrong velocities
|
||||||
|
# for SHORT/MEDIUM sub-frame bins until PR-Q.5 replaces this path
|
||||||
|
# with extract_targets_from_frame_crt.
|
||||||
|
v_res = self._waveform.velocity_resolution_long_mps
|
||||||
|
n_doppler = frame.detections.shape[1] if frame.detections.ndim == 2 else self._waveform.n_doppler_bins
|
||||||
|
doppler_center = n_doppler // 2
|
||||||
|
|
||||||
for idx in det_indices:
|
for idx in det_indices:
|
||||||
rbin, dbin = idx
|
rbin, dbin = idx
|
||||||
@@ -198,8 +206,7 @@ class RadarDataWorker(QThread):
|
|||||||
|
|
||||||
# Convert bin indices to physical units
|
# Convert bin indices to physical units
|
||||||
range_m = float(rbin) * r_res
|
range_m = float(rbin) * r_res
|
||||||
# Doppler: centre bin (16) = 0 m/s; positive bins = approaching
|
velocity_ms = float(dbin - doppler_center) * v_res
|
||||||
velocity_ms = float(dbin - 16) * v_res
|
|
||||||
|
|
||||||
# Apply pitch correction if GPS data available
|
# Apply pitch correction if GPS data available
|
||||||
raw_elev = 0.0 # FPGA doesn't send elevation per-detection
|
raw_elev = 0.0 # FPGA doesn't send elevation per-detection
|
||||||
@@ -564,11 +571,14 @@ class ReplayWorker(QThread):
|
|||||||
self.frameReady.emit(frame)
|
self.frameReady.emit(frame)
|
||||||
self.frameIndexChanged.emit(index, self._engine.total_frames)
|
self.frameIndexChanged.emit(index, self._engine.total_frames)
|
||||||
|
|
||||||
# Target extraction
|
# Target extraction. PR-Q.4: single LONG-PRI v_res placeholder;
|
||||||
|
# PR-Q.5 replaces this call with extract_targets_from_frame_crt
|
||||||
|
# which derives per-subframe velocity from the high 2 bits of
|
||||||
|
# doppler_bin and runs 3-PRI CRT unfolding.
|
||||||
targets = self._extract_targets(
|
targets = self._extract_targets(
|
||||||
frame,
|
frame,
|
||||||
range_resolution=self._waveform.range_resolution_m,
|
range_resolution=self._waveform.range_resolution_m,
|
||||||
velocity_resolution=self._waveform.velocity_resolution_mps,
|
velocity_resolution=self._waveform.velocity_resolution_long_mps,
|
||||||
gps=self._gps,
|
gps=self._gps,
|
||||||
)
|
)
|
||||||
self.targetsUpdated.emit(targets)
|
self.targetsUpdated.emit(targets)
|
||||||
|
|||||||
Reference in New Issue
Block a user