feat(gui): PR-Q.5 — 3-PRI CRT Doppler unfolder + cluster extractor (C-5)
Add host-side 3-PRI Chinese-Remainder velocity unfolding and a cluster
extractor that reads the 48-bin Doppler frame, splits it into the 3
sub-frames (SHORT/MEDIUM/LONG), and resolves Doppler aliases across
coprime PRIs. Resolves the algorithm half of audit C-5; the data is
now in extract_targets_from_frame_crt's hands but workers still call
the legacy single-PRI extractor (PR-Q.6 wires it).
v7/processing.py:
- unfold_velocity_crt(v_meas, v_unamb, v_res, max_alias_k=6,
tol_factor=0.5) -> (v_est, confidence, alias_set). Brute-force
candidate search over PRI-0 fold depth, per-PRI half-bin
tolerance. Confidence: CONFIRMED (3-PRI unique), LIKELY (3-PRI
with 2 cands, or 2-PRI with unique cand), AMBIGUOUS (1-PRI, 3+
cands, 2-PRI multi-cand, or no fold within tol).
- extract_targets_from_frame_crt(frame, waveform, gps, max_alias_k):
groups detections by range bin, picks strongest bin per
(rbin, sf), decodes signed Doppler via sub_frame = dbin // 16 /
bin_in_sf = dbin % 16, calls unfold_velocity_crt, attaches
velocity_confidence and alias_set to RadarTarget. Falls back to
legacy extract_targets_from_frame for non-48-bin frames.
v7/models.py:
- RadarTarget gains velocity_confidence (str default "UNKNOWN") and
alias_set (list[float] | None).
v7/__init__.py:
- Re-exports unfold_velocity_crt + extract_targets_from_frame_crt.
test_v7.py (16 new tests, 0 failures):
- TestUnfoldVelocityCRT (8): zero-velocity CONFIRMED, below per-PRI
v_unamb CONFIRMED, above per-PRI (100 m/s) CONFIRMED, near CRT
ceiling (~261 m/s) CONFIRMED, negative velocity, 1-PRI AMBIGUOUS,
2-PRI LIKELY, inconsistent measurements AMBIGUOUS+fallback.
- TestExtractTargetsFromFrameCrt (8): 3-PRI CONFIRMED target,
LONG-only AMBIGUOUS (the 20-km blindspot regime), 2-PRI LIKELY,
strongest-bin picking, two targets at distinct ranges, legacy
32-bin frame fallback, no-detections empty, GPS georef.
Local: test_v7 100/0/0 (9 graceful skips), test_GUI_V65_Tk 117/0/2.