added reference png export
This commit is contained in:
@@ -224,3 +224,6 @@ __marimo__/
|
|||||||
.streamlit/secrets.toml
|
.streamlit/secrets.toml
|
||||||
python_app/runtime
|
python_app/runtime
|
||||||
SHARE_INTERNET_TO_PI.md
|
SHARE_INTERNET_TO_PI.md
|
||||||
|
|
||||||
|
CLAUDE.md
|
||||||
|
./docs
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from python_app.gui.preprocess_dialog import PreprocessDialog
|
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 (
|
from python_app.orchestration.preprocess_assets import (
|
||||||
PREPROCESS_ASSET_SPECS,
|
PREPROCESS_ASSET_SPECS,
|
||||||
VISIBLE_PREPROCESS_ASSET_KEYS,
|
VISIBLE_PREPROCESS_ASSET_KEYS,
|
||||||
@@ -701,6 +702,12 @@ class AppWindowPreprocessMixin:
|
|||||||
f"{display_name} sequence completed and saved: set={set_name}, "
|
f"{display_name} sequence completed and saved: set={set_name}, "
|
||||||
f"radar_variants={len(saved_sets)} [{saved_summary}]"
|
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:
|
else:
|
||||||
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
|
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}")
|
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
|
except Exception as exc: # noqa: BLE001
|
||||||
self._show_exception("Failed to save preprocess set", exc)
|
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 the last config of each combo, so this persists a
|
||||||
|
graph for the other configs too, under the store's ``preview_png/`` tree. 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)
|
||||||
|
# batch.traces and saved_sets are both ordered by the session's radar
|
||||||
|
# variants, so index i refers to the same config in both.
|
||||||
|
saved_count = 0
|
||||||
|
for batch in session.captured_batches():
|
||||||
|
for trace, saved in zip(batch.traces, saved_sets):
|
||||||
|
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:
|
def _abort_capture_sequence(self, *, resume_pipeline: bool = True) -> None:
|
||||||
"""Abort active capture session and optionally resume pipeline."""
|
"""Abort active capture session and optionally resume pipeline."""
|
||||||
if self._capture_session is None:
|
if self._capture_session is None:
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -411,5 +411,15 @@ class NpzStore(StoreApi):
|
|||||||
"""Return directory for set kind and radar key."""
|
"""Return directory for set kind and radar key."""
|
||||||
return self._root_dir / kind / 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"]
|
__all__ = ["NpzStore", "radar_key_from_config"]
|
||||||
|
|||||||
Reference in New Issue
Block a user