Merge branch 'some-additions'

export reference png added
This commit is contained in:
2026-06-23 14:01:08 +03:00
5 changed files with 150 additions and 0 deletions
+3
View File
@@ -224,3 +224,6 @@ __marimo__/
.streamlit/secrets.toml
python_app/runtime
SHARE_INTERNET_TO_PI.md
CLAUDE.md
./docs
@@ -3,6 +3,7 @@
from __future__ import annotations
from python_app.gui.preprocess_dialog import PreprocessDialog
from python_app.gui.trace_png_export import export_trace_png
from python_app.orchestration.preprocess_assets import (
PREPROCESS_ASSET_SPECS,
VISIBLE_PREPROCESS_ASSET_KEYS,
@@ -701,6 +702,12 @@ class AppWindowPreprocessMixin:
f"{display_name} sequence completed and saved: set={set_name}, "
f"radar_variants={len(saved_sets)} [{saved_summary}]"
)
self._export_multi_radar_preview_pngs(
session=session,
saved_sets=saved_sets,
kind=kind,
set_name=set_name,
)
else:
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
@@ -708,6 +715,50 @@ class AppWindowPreprocessMixin:
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to save preprocess set", exc)
def _export_multi_radar_preview_pngs(
self,
*,
session: MultiRadarSequentialCaptureSession,
saved_sets: list,
kind: str,
set_name: str,
) -> None:
"""Save amplitude/phase PNGs for every captured combo across all radar configs.
The live preview only shows one trace per config, so this persists a graph for
every config and every combo (port) under the store's ``preview_png/`` tree.
Iterating per config over its full trace set (not the per-batch preview trace)
is what makes matrix radars save all ports instead of only the last one. The
set is already saved by the time we get here, so any rendering failure is
logged but never aborts the save.
"""
channel = preprocess_asset_channel(kind)
display_name = preprocess_asset_display_name(kind)
saved_count = 0
for saved in saved_sets:
for trace in session.traces_for_radar_key(saved.radar_key):
combo_label = f"input={trace.combo.input} output={trace.combo.output}"
try:
png_path = self._store.preview_png_dir(kind, set_name, saved.radar_key) / (
f"i{trace.combo.input}_o{trace.combo.output}.png"
)
export_trace_png(
trace,
png_path,
channel=channel,
title=f"{display_name} | {saved.display_name} | {combo_label}",
)
saved_count += 1
except Exception as exc: # noqa: BLE001
self._log(
f"Failed to save preview PNG for {saved.display_name} ({combo_label}): {exc}"
)
if saved_count and saved_sets:
png_root = self._store.preview_png_dir(kind, set_name, saved_sets[0].radar_key).parent
self._log(
f"Saved {saved_count} preview PNG(s) for {len(saved_sets)} radar config(s) under {png_root}"
)
def _abort_capture_sequence(self, *, resume_pipeline: bool = True) -> None:
"""Abort active capture session and optionally resume pipeline."""
if self._capture_session is None:
+77
View File
@@ -0,0 +1,77 @@
"""Render captured traces (amplitude + phase) to standalone PNG files.
The preprocess preview pane only shows the *last* radar variant of each combo,
so during a multi-config reference capture the operator never sees the other
variants. This helper renders the same amplitude/phase plot pair used in the
preview into off-screen PNG files, letting us persist a graph for every config.
"""
from __future__ import annotations
import logging
from pathlib import Path
import numpy as np
import pyqtgraph as pg
from PyQt6.QtCore import QRectF
from pyqtgraph.exporters import ImageExporter
from python_app.models.dataset_model import TraceData
logger = logging.getLogger(__name__)
# Matches the preview pane styling in preprocess_dialog so saved PNGs and the
# on-screen preview look the same.
_BACKGROUND = "#101418"
_MAGNITUDE_PEN = pg.mkPen("#4cc9f0", width=1.8)
_PHASE_PEN = pg.mkPen("#f48c06", width=1.8)
_EXPORT_SIZE = (1400, 520)
def export_trace_png(
trace: TraceData,
output_path: Path,
*,
channel: str,
title: str,
) -> None:
"""Render one trace's amplitude (dB) and wrapped phase (deg) panes to a PNG.
Builds an off-screen ``GraphicsLayoutWidget`` (never shown), plots the same
curves as the live preview, and exports the scene as a PNG. Requires a
running ``QApplication`` (always true inside the GUI).
"""
samples = trace.s11 if channel == "s11" else trace.s21
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
phase_deg = np.degrees(np.angle(samples))
layout = pg.GraphicsLayoutWidget(show=False, size=_EXPORT_SIZE)
layout.setBackground(_BACKGROUND)
try:
layout.addLabel(title, row=0, col=0, colspan=2)
magnitude_plot = layout.addPlot(row=1, col=0)
magnitude_plot.showGrid(x=True, y=True, alpha=0.2)
magnitude_plot.setLabel("bottom", "Frequency", units="Hz")
magnitude_plot.setLabel("left", "Magnitude", units="dB")
magnitude_plot.plot(trace.frequency_hz, magnitude_db, pen=_MAGNITUDE_PEN)
phase_plot = layout.addPlot(row=1, col=1)
phase_plot.showGrid(x=True, y=True, alpha=0.2)
phase_plot.setLabel("bottom", "Frequency", units="Hz")
phase_plot.setLabel("left", "Phase", units="deg")
phase_plot.plot(trace.frequency_hz, phase_deg, pen=_PHASE_PEN)
# An off-screen widget never receives a resizeEvent, so its central
# GraphicsLayout keeps its small preferred size and both plots would be
# squeezed into the left edge of the image. Force the layout geometry to
# the target size, doing manually what a resizeEvent normally triggers.
layout.ci.setGeometry(QRectF(0, 0, _EXPORT_SIZE[0], _EXPORT_SIZE[1]))
output_path.parent.mkdir(parents=True, exist_ok=True)
exporter = ImageExporter(layout.scene())
exporter.parameters()["width"] = _EXPORT_SIZE[0]
exporter.export(str(output_path))
finally:
layout.close()
layout.deleteLater()
+10
View File
@@ -484,5 +484,15 @@ class NpzStore(StoreApi):
"""Return directory for set kind and radar key."""
return self._root_dir / kind / radar_key
def preview_png_dir(self, kind: str, set_name: str, radar_key: str) -> Path:
"""Return directory for preview PNGs of one set/radar variant.
Lives under ``preview_png/`` inside the store so saved graphs sit next to
the data they describe, grouped by set name then radar variant. The set
name is operator-supplied, so it is sanitized; ``radar_key`` is already
filesystem-safe by construction.
"""
return self._root_dir / "preview_png" / kind / sanitize_path_component(set_name) / radar_key
__all__ = ["NpzStore", "radar_key_from_config"]
@@ -313,6 +313,15 @@ class MultiRadarSequentialCaptureSession:
"""Return completed combo batches in capture order."""
return list(self._captured_batches)
def traces_for_radar_key(self, radar_key: str) -> list[TraceData]:
"""Return every captured trace (one per combo) for one radar variant.
Unlike a batch's ``traces`` (one entry per variant, holding only the
preview trace), this returns the full per-combo set, so matrix radars
expose all ports rather than just the last one.
"""
return list(self._traces_by_radar_key.get(radar_key, []))
def radar_variant_count(self) -> int:
"""Return how many radar variants are captured per combo."""
return len(self._radar_variants)