From 115c5f0778a991a9a5308e48368e8eebf4c2760c Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Sat, 2 May 2026 16:51:58 +0545 Subject: [PATCH] =?UTF-8?q?feat(gui):=20M-1=20/=20PR-Q.7=20=E2=80=94=20das?= =?UTF-8?q?hboard=20CRT=20confidence=20column=20+=20alias-fold=20tooltip?= =?UTF-8?q?=20(C-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CRT extractor (PR-Q.5/PR-Q.6) tags every target with a velocity_confidence ("CONFIRMED" / "LIKELY" / "AMBIGUOUS" / "UNKNOWN") and an optional alias_set of candidate v_true folds. Until now the operator-facing targets table on the Main View tab dropped that signal, so a single-PRI-only AMBIGUOUS reading looked identical to a 3-PRI CONFIRMED one. Changes: - Targets table column count 5 -> 6; new "Confidence" column between Velocity and Magnitude. - Module helper _confidence_display(label) -> (text, QColor): CONFIRMED green (DARK_SUCCESS) LIKELY amber (DARK_WARNING) AMBIGUOUS red (DARK_ERROR), prefixed with "? " so the row stands out even when the operator's eyes skip the colour. UNKNOWN gray (DARK_TEXT) — legacy 32-bin / no CRT. Unrecognised future labels fall through to UNKNOWN. - Velocity cell carries a tooltip listing the CRT alias_set folds when present, so hovering reveals all plausible v_true candidates. - QColor pulled in from PyQt6.QtGui for the foreground tint. Tests (TestDashboardConfidenceDisplay, +5): - CONFIRMED/LIKELY/AMBIGUOUS/UNKNOWN each map to expected text + colour. - AMBIGUOUS leads with "?" so it's visible without colour. - Unrecognised label "BANANA" falls back to UNKNOWN/gray. Regression: 237/237 (test_v7 120 + test_GUI_V65_Tk 117). Ruff clean. This closes audit M-1 / task PR-Q.7. The C-5 thread is end-to-end functional: RTL emits 3 sub-frames (PR-Q.1) -> cosim agrees (PR-Q.2) -> v7 models carry per-subframe params (PR-Q.4) -> processing.py runs CRT (PR-Q.5) -> workers route through it (PR-Q.6) -> dashboard surfaces the confidence (PR-Q.7). --- 9_Firmware/9_3_GUI/test_v7.py | 46 ++++++++++++++++++++++++ 9_Firmware/9_3_GUI/v7/dashboard.py | 56 ++++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/9_Firmware/9_3_GUI/test_v7.py b/9_Firmware/9_3_GUI/test_v7.py index cf786de..95a06a9 100644 --- a/9_Firmware/9_3_GUI/test_v7.py +++ b/9_Firmware/9_3_GUI/test_v7.py @@ -1581,6 +1581,52 @@ class TestWorkersRouteThroughCrt(unittest.TestCase): self.assertIs(worker._extract_targets, extract_targets_from_frame_crt) +# ============================================================================= +# Test: PR-Q.7 / audit M-1 — dashboard confidence column display helper +# ============================================================================= + +@unittest.skipUnless(_pyqt6_available(), "PyQt6 not installed") +class TestDashboardConfidenceDisplay(unittest.TestCase): + """_confidence_display maps RadarTarget.velocity_confidence to (text, QColor).""" + + def test_confirmed_is_green_no_prefix(self): + from v7.dashboard import _confidence_display + from v7.models import DARK_SUCCESS + text, color = _confidence_display("CONFIRMED") + self.assertEqual(text, "CONFIRMED") + self.assertEqual(color.name().upper(), DARK_SUCCESS.upper()) + + def test_likely_is_amber(self): + from v7.dashboard import _confidence_display + from v7.models import DARK_WARNING + text, color = _confidence_display("LIKELY") + self.assertEqual(text, "LIKELY") + self.assertEqual(color.name().upper(), DARK_WARNING.upper()) + + def test_ambiguous_gets_question_mark_prefix_and_red(self): + from v7.dashboard import _confidence_display + from v7.models import DARK_ERROR + text, color = _confidence_display("AMBIGUOUS") + self.assertTrue(text.startswith("?"), + "AMBIGUOUS must lead with '?' so it's visible without color") + self.assertIn("AMBIGUOUS", text) + self.assertEqual(color.name().upper(), DARK_ERROR.upper()) + + def test_unknown_falls_back_to_text_color(self): + from v7.dashboard import _confidence_display + from v7.models import DARK_TEXT + text, color = _confidence_display("UNKNOWN") + self.assertEqual(text, "UNKNOWN") + self.assertEqual(color.name().upper(), DARK_TEXT.upper()) + + def test_unrecognised_label_falls_through_to_unknown(self): + from v7.dashboard import _confidence_display + from v7.models import DARK_TEXT + text, color = _confidence_display("BANANA") + self.assertEqual(text, "UNKNOWN") + self.assertEqual(color.name().upper(), DARK_TEXT.upper()) + + # ============================================================================= # Helper: lazy import of v7.models # ============================================================================= diff --git a/9_Firmware/9_3_GUI/v7/dashboard.py b/9_Firmware/9_3_GUI/v7/dashboard.py index 50c2a09..1a1ad77 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -41,6 +41,7 @@ from PyQt6.QtWidgets import ( QPlainTextEdit, QStatusBar, QMessageBox, ) from PyQt6.QtCore import Qt, QLocale, QTimer, pyqtSignal, pyqtSlot, QObject +from PyQt6.QtGui import QColor from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg from matplotlib.figure import Figure @@ -87,6 +88,31 @@ def _make_dspin() -> QDoubleSpinBox: return sb +# Confidence label colors for the targets table (PR-Q.7 / audit M-1). +# CONFIRMED green — 3 sub-frames agreed via CRT +# LIKELY amber — 2 of 3 sub-frames agreed +# AMBIGUOUS red — only 1 sub-frame saw it; v_unamb-bounded reading +# UNKNOWN gray — legacy 32-bin frame (no CRT was attempted) +_CONFIDENCE_COLORS: dict[str, str] = { + "CONFIRMED": DARK_SUCCESS, + "LIKELY": DARK_WARNING, + "AMBIGUOUS": DARK_ERROR, + "UNKNOWN": DARK_TEXT, +} + + +def _confidence_display(confidence: str) -> tuple[str, QColor]: + """Map a RadarTarget.velocity_confidence string to (cell text, QColor). + + AMBIGUOUS gets a leading "?" so it stands out in a long target list even + if the operator's eyes skip the color cue. Unknown confidence labels + (e.g. from a future model) fall back to "UNKNOWN" gray. + """ + label = confidence if confidence in _CONFIDENCE_COLORS else "UNKNOWN" + text = f"? {label}" if label == "AMBIGUOUS" else label + return text, QColor(_CONFIDENCE_COLORS[label]) + + # ============================================================================= # Range-Doppler Canvas (matplotlib) # ============================================================================= @@ -486,9 +512,10 @@ class RadarDashboard(QMainWindow): tg_layout = QVBoxLayout(targets_group) self._targets_table_main = QTableWidget() - self._targets_table_main.setColumnCount(5) + self._targets_table_main.setColumnCount(6) self._targets_table_main.setHorizontalHeaderLabels([ - "Range (m)", "Velocity (m/s)", "Magnitude", "SNR (dB)", "Track ID", + "Range (m)", "Velocity (m/s)", "Confidence", + "Magnitude", "SNR (dB)", "Track ID", ]) self._targets_table_main.setAlternatingRowColors(True) self._targets_table_main.setSelectionBehavior( @@ -1961,16 +1988,31 @@ class RadarDashboard(QMainWindow): for row, t in enumerate(targets): self._targets_table_main.setItem( row, 0, QTableWidgetItem(f"{t.range:.0f}")) - self._targets_table_main.setItem( - row, 1, QTableWidgetItem(f"{t.velocity:.0f}")) + + # PR-Q.7: velocity cell carries an alias-set tooltip so the operator + # can hover to see all CRT candidate folds; useful when confidence + # is LIKELY/AMBIGUOUS and the displayed velocity is just the best + # of several plausible v_true values. + vel_item = QTableWidgetItem(f"{t.velocity:.0f}") + if t.alias_set: + fold_strs = ", ".join(f"{v:+.1f}" for v in t.alias_set) + vel_item.setToolTip(f"CRT alias folds (m/s): {fold_strs}") + self._targets_table_main.setItem(row, 1, vel_item) + + # PR-Q.7: confidence column. CONFIRMED = green, LIKELY = amber, + # AMBIGUOUS = red with leading "?", UNKNOWN = gray (legacy single-PRI). + conf_text, conf_color = _confidence_display(t.velocity_confidence) + conf_item = QTableWidgetItem(conf_text) + conf_item.setForeground(conf_color) + self._targets_table_main.setItem(row, 2, conf_item) mag_val = 10 ** (t.snr / 10) if t.snr > 0 else 0 self._targets_table_main.setItem( - row, 2, QTableWidgetItem(f"{mag_val:.0f}")) + row, 3, QTableWidgetItem(f"{mag_val:.0f}")) self._targets_table_main.setItem( - row, 3, QTableWidgetItem(f"{t.snr:.1f}")) + row, 4, QTableWidgetItem(f"{t.snr:.1f}")) self._targets_table_main.setItem( - row, 4, QTableWidgetItem(str(t.track_id))) + row, 5, QTableWidgetItem(str(t.track_id))) def _update_diagnostics(self): # Connection indicators