new GPR parameters and some UI fixes
This commit is contained in:
@@ -36,6 +36,8 @@ struct ProcessingLiveConfig {
|
||||
float gpr_stop_freq_mhz = 6000.0F;
|
||||
float gpr_speed_m_s = 0.0F;
|
||||
float gpr_look_angle_deg = 0.0F;
|
||||
float gpr_snr_thresh = 4.5F;
|
||||
float gpr_snr_comp_max = 25.0F;
|
||||
bool gpr_background_subtract_enabled = true;
|
||||
std::uint32_t gpr_background_mean_count = 10U;
|
||||
std::uint64_t history_command_seq = 0;
|
||||
|
||||
@@ -211,6 +211,18 @@ using Json = nlohmann::json;
|
||||
}
|
||||
config.gpr_look_angle_deg = static_cast<float>(found->get<double>());
|
||||
}
|
||||
if (const auto found = root.find("gpr_snr_thresh"); found != root.end()) {
|
||||
if (!found->is_number()) {
|
||||
throw std::runtime_error("processing.gpr_snr_thresh must be number");
|
||||
}
|
||||
config.gpr_snr_thresh = static_cast<float>(found->get<double>());
|
||||
}
|
||||
if (const auto found = root.find("gpr_snr_comp_max"); found != root.end()) {
|
||||
if (!found->is_number()) {
|
||||
throw std::runtime_error("processing.gpr_snr_comp_max must be number");
|
||||
}
|
||||
config.gpr_snr_comp_max = static_cast<float>(found->get<double>());
|
||||
}
|
||||
if (const auto found = root.find("gpr_background_subtract_enabled"); found != root.end()) {
|
||||
if (!found->is_boolean()) {
|
||||
throw std::runtime_error("processing.gpr_background_subtract_enabled must be bool");
|
||||
|
||||
@@ -20,8 +20,6 @@ constexpr double kPi = 3.14159265358979323846;
|
||||
constexpr double kSpeedOfLightMetersPerSec = 299'792'458.0;
|
||||
constexpr double kAccumulatorXMarginM = 2.0;
|
||||
constexpr double kAccumulatorZMinM = 0.20;
|
||||
constexpr double kSnrThresh = 4.5;
|
||||
constexpr double kSnrCompMax = 25.0;
|
||||
constexpr std::size_t kGridWidth = 300U;
|
||||
constexpr std::size_t kGridHeight = 300U;
|
||||
constexpr double kGaussianSigma = 3.0;
|
||||
@@ -1197,6 +1195,8 @@ auto GprProcessor::process_collection(
|
||||
kSpeedOfLightMetersPerSec / std::sqrt(std::max(1e-6, static_cast<double>(run_config.gpr.relative_permittivity)));
|
||||
const double start_hz = static_cast<double>(live_config.gpr_start_freq_mhz) * 1'000'000.0;
|
||||
const double stop_hz = static_cast<double>(live_config.gpr_stop_freq_mhz) * 1'000'000.0;
|
||||
const double snr_thresh = std::max(0.0, static_cast<double>(live_config.gpr_snr_thresh));
|
||||
const double snr_comp_max = std::max(0.0, static_cast<double>(live_config.gpr_snr_comp_max));
|
||||
|
||||
std::unordered_map<PairKey, AscanResult> ascans_by_pair{};
|
||||
double bandwidth_hz = 0.0;
|
||||
@@ -1248,7 +1248,8 @@ auto GprProcessor::process_collection(
|
||||
4.0,
|
||||
std::floor(((velocity_mps / (2.0 * bandwidth_hz)) / z_step) * 0.7)
|
||||
));
|
||||
const auto peak_indices = find_peak_indices(ascan.amplitude, min_index, max_index, noise * kSnrThresh, min_distance);
|
||||
const auto peak_indices =
|
||||
find_peak_indices(ascan.amplitude, min_index, max_index, noise * snr_thresh, min_distance);
|
||||
auto& peaks = peaks_by_pair[key];
|
||||
peaks.reserve(peak_indices.size());
|
||||
for (const auto peak_index : peak_indices) {
|
||||
@@ -1259,7 +1260,7 @@ auto GprProcessor::process_collection(
|
||||
attenuation / attenuation_at_depth(tx_index, rx_index, 3.0, selection.x_tx, selection.x_rx);
|
||||
const double snr_comp = std::min(
|
||||
snr_raw / (std::pow(attenuation_norm, static_cast<double>(live_config.gpr_comp_power)) + 1e-12),
|
||||
kSnrCompMax
|
||||
snr_comp_max
|
||||
);
|
||||
peaks.push_back(PeakRecord{
|
||||
.z_app = z_app,
|
||||
|
||||
@@ -123,6 +123,7 @@ class AppWindow(
|
||||
key: str(preprocess_asset_model(self._defaults_config, key).set_name)
|
||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||
}
|
||||
self._selected_preprocess_radar_key = self._radar_key(self._defaults_config)
|
||||
|
||||
def _init_capture_state(self) -> None:
|
||||
"""Initialize one-shot capture and sequence-control flags."""
|
||||
@@ -161,6 +162,8 @@ class AppWindow(
|
||||
self._gpr_selected_geometry = None
|
||||
self._phase_viewbox = None
|
||||
self._history_run_signature = None
|
||||
self._processor_run_signature = None
|
||||
self._active_processing_mode = "pass_through"
|
||||
self._radar_limits: dict[str, float | int] | None = None
|
||||
|
||||
def _history_limit_from_config(self) -> int:
|
||||
|
||||
@@ -11,6 +11,7 @@ from PyQt6.QtCore import QSignalBlocker
|
||||
from PyQt6.QtWidgets import QFileDialog
|
||||
|
||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
|
||||
from python_app.models.gui_profile_model import (
|
||||
GuiBscanStateModel,
|
||||
GuiDataActionsStateModel,
|
||||
@@ -37,6 +38,14 @@ from python_app.storage.npz_store import radar_key_from_config
|
||||
class AppWindowConfigMixin:
|
||||
"""Builds runtime config models from current UI state."""
|
||||
|
||||
def _validate_processing_mode_selection(self, mode: str) -> None:
|
||||
"""Validate requested processing mode against current stable/live GUI state."""
|
||||
validate_processing_mode_constraints(
|
||||
mode,
|
||||
self._build_config(),
|
||||
self._live_processing_config(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_csv_int_list(text: str) -> list[int]:
|
||||
"""Parse comma-separated integer selection list."""
|
||||
@@ -117,6 +126,16 @@ class AppWindowConfigMixin:
|
||||
values = sorted(geometry_values & combo_values) or sorted(geometry_values)
|
||||
return ",".join(str(value) for value in values)
|
||||
|
||||
@staticmethod
|
||||
def _default_gpr_visible_x_bounds_from_config(config: RunConfigModel) -> tuple[float, float]:
|
||||
"""Build default visible X-range for GPR object-only rendering."""
|
||||
x_values = [float(entry.x_m) for entry in config.gpr.tx_geometry]
|
||||
x_values.extend(float(entry.x_m) for entry in config.gpr.rx_geometry)
|
||||
if not x_values:
|
||||
return (-2.0, 2.0)
|
||||
margin_m = 2.0
|
||||
return (min(x_values) - margin_m, max(x_values) + margin_m)
|
||||
|
||||
@staticmethod
|
||||
def _history_limit_for_config(config: RunConfigModel) -> int:
|
||||
"""Return unified GUI history limit derived from config ring capacities."""
|
||||
@@ -133,6 +152,7 @@ class AppWindowConfigMixin:
|
||||
"""Build fallback GUI-only defaults for a stable run config."""
|
||||
default_combo = config.combos[0] if config.combos else ComboModel(input=0, output=0)
|
||||
default_mode = "single" if len(config.combos) == 1 else "text"
|
||||
default_gpr_x_min_m, default_gpr_x_max_m = self._default_gpr_visible_x_bounds_from_config(config)
|
||||
return GuiStateModel(
|
||||
switches=GuiSwitchStateModel(
|
||||
combo_mode=default_mode,
|
||||
@@ -167,8 +187,16 @@ class AppWindowConfigMixin:
|
||||
stop_freq_mhz=6000.0,
|
||||
speed_m_s=0.0,
|
||||
look_angle_deg=0.0,
|
||||
snr_thresh=4.5,
|
||||
snr_comp_max=25.0,
|
||||
background_subtract_enabled=True,
|
||||
background_mean_count=10,
|
||||
render_mode="heatmap",
|
||||
min_visible_pair_count=1,
|
||||
visible_x_min_m=default_gpr_x_min_m,
|
||||
visible_x_max_m=default_gpr_x_max_m,
|
||||
visible_z_min_m=0.0,
|
||||
visible_z_max_m=14.0,
|
||||
),
|
||||
),
|
||||
data_actions=GuiDataActionsStateModel(
|
||||
@@ -221,8 +249,16 @@ class AppWindowConfigMixin:
|
||||
stop_freq_mhz=float(self._gpr_stop_freq_mhz.value()),
|
||||
speed_m_s=float(self._gpr_speed_m_s.value()),
|
||||
look_angle_deg=float(self._gpr_look_angle_deg.value()),
|
||||
snr_thresh=float(self._gpr_snr_thresh.value()),
|
||||
snr_comp_max=float(self._gpr_snr_comp_max.value()),
|
||||
background_subtract_enabled=bool(self._gpr_background_subtract_enabled.isChecked()),
|
||||
background_mean_count=int(self._gpr_background_mean_count.value()),
|
||||
render_mode=self._gpr_render_mode.currentText(),
|
||||
min_visible_pair_count=int(self._gpr_min_visible_pair_count.value()),
|
||||
visible_x_min_m=float(self._gpr_visible_x_min_m.value()),
|
||||
visible_x_max_m=float(self._gpr_visible_x_max_m.value()),
|
||||
visible_z_min_m=float(self._gpr_visible_z_min_m.value()),
|
||||
visible_z_max_m=float(self._gpr_visible_z_max_m.value()),
|
||||
),
|
||||
),
|
||||
data_actions=GuiDataActionsStateModel(
|
||||
@@ -404,8 +440,16 @@ class AppWindowConfigMixin:
|
||||
self._gpr_stop_freq_mhz,
|
||||
self._gpr_speed_m_s,
|
||||
self._gpr_look_angle_deg,
|
||||
self._gpr_snr_thresh,
|
||||
self._gpr_snr_comp_max,
|
||||
self._gpr_background_subtract_enabled,
|
||||
self._gpr_background_mean_count,
|
||||
self._gpr_render_mode,
|
||||
self._gpr_min_visible_pair_count,
|
||||
self._gpr_visible_x_min_m,
|
||||
self._gpr_visible_x_max_m,
|
||||
self._gpr_visible_z_min_m,
|
||||
self._gpr_visible_z_max_m,
|
||||
self._save_count,
|
||||
self._save_path_input,
|
||||
self._save_name_input,
|
||||
@@ -466,10 +510,18 @@ class AppWindowConfigMixin:
|
||||
self._gpr_stop_freq_mhz.setValue(float(gui_state.processing.gpr.stop_freq_mhz))
|
||||
self._gpr_speed_m_s.setValue(float(gui_state.processing.gpr.speed_m_s))
|
||||
self._gpr_look_angle_deg.setValue(float(gui_state.processing.gpr.look_angle_deg))
|
||||
self._gpr_snr_thresh.setValue(float(gui_state.processing.gpr.snr_thresh))
|
||||
self._gpr_snr_comp_max.setValue(float(gui_state.processing.gpr.snr_comp_max))
|
||||
self._gpr_background_subtract_enabled.setChecked(
|
||||
bool(gui_state.processing.gpr.background_subtract_enabled)
|
||||
)
|
||||
self._gpr_background_mean_count.setValue(int(gui_state.processing.gpr.background_mean_count))
|
||||
self._set_combo_current_text(self._gpr_render_mode, gui_state.processing.gpr.render_mode)
|
||||
self._gpr_min_visible_pair_count.setValue(int(gui_state.processing.gpr.min_visible_pair_count))
|
||||
self._gpr_visible_x_min_m.setValue(float(gui_state.processing.gpr.visible_x_min_m))
|
||||
self._gpr_visible_x_max_m.setValue(float(gui_state.processing.gpr.visible_x_max_m))
|
||||
self._gpr_visible_z_min_m.setValue(float(gui_state.processing.gpr.visible_z_min_m))
|
||||
self._gpr_visible_z_max_m.setValue(float(gui_state.processing.gpr.visible_z_max_m))
|
||||
|
||||
self._save_count.setValue(int(gui_state.data_actions.save_count))
|
||||
self._save_path_input.setText(str(gui_state.data_actions.save_path))
|
||||
@@ -478,6 +530,7 @@ class AppWindowConfigMixin:
|
||||
self._defaults_config = config
|
||||
self._gui_defaults = gui_state
|
||||
self._selected_preprocess_sets = selected_preprocess_sets
|
||||
self._selected_preprocess_radar_key = self._radar_key(config)
|
||||
self._preprocess_set_name = str(gui_state.preprocess_dialog.set_name)
|
||||
self._apply_history_limit_from_config(config)
|
||||
self._gpr_geometry_signature = None
|
||||
@@ -552,6 +605,18 @@ class AppWindowConfigMixin:
|
||||
power_dbm=config.radar.sweep.power_dbm,
|
||||
)
|
||||
|
||||
def _radar_key_from_ui(self) -> str:
|
||||
"""Build current radar key directly from radar widgets only."""
|
||||
return radar_key_from_config(
|
||||
model_name=self._defaults_config.radar.model,
|
||||
serial=self._serial_input.text().strip(),
|
||||
sweep_start_hz=float(self._start_hz_input.text().strip()),
|
||||
sweep_stop_hz=float(self._stop_hz_input.text().strip()),
|
||||
sweep_points=int(self._points_input.text().strip()),
|
||||
ifbw_hz=float(self._ifbw_input.text().strip()),
|
||||
power_dbm=float(self._power_input.text().strip()),
|
||||
)
|
||||
|
||||
def _live_processing_config(self, *, history_command: str = "none") -> ProcessingLiveConfig:
|
||||
"""Build live processing config from current processing widgets."""
|
||||
self._sync_bscan_frequency_limits_with_radar()
|
||||
@@ -580,6 +645,8 @@ class AppWindowConfigMixin:
|
||||
gpr_stop_freq_mhz=float(self._gpr_stop_freq_mhz.value()),
|
||||
gpr_speed_m_s=float(self._gpr_speed_m_s.value()),
|
||||
gpr_look_angle_deg=float(self._gpr_look_angle_deg.value()),
|
||||
gpr_snr_thresh=float(self._gpr_snr_thresh.value()),
|
||||
gpr_snr_comp_max=float(self._gpr_snr_comp_max.value()),
|
||||
gpr_background_subtract_enabled=bool(self._gpr_background_subtract_enabled.isChecked()),
|
||||
gpr_background_mean_count=int(self._gpr_background_mean_count.value()),
|
||||
history_command_seq=int(self._history_command_seq),
|
||||
@@ -615,8 +682,33 @@ class AppWindowConfigMixin:
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to update live processing settings", exc)
|
||||
|
||||
def _on_gpr_visual_settings_changed(self, *_args) -> None:
|
||||
"""Redraw current GPR result using updated GUI-only render settings."""
|
||||
if self._processing_mode.currentText() != "gpr":
|
||||
return
|
||||
try:
|
||||
if self._result_history and self._draw_results(self._result_history[-1]):
|
||||
return
|
||||
self._clear_gpr_plot()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to update GPR render settings", exc)
|
||||
|
||||
def _on_processing_mode_changed(self, mode: str) -> None:
|
||||
"""Switch processing parameter page and refresh corresponding visualization."""
|
||||
previous_mode = getattr(self, "_active_processing_mode", "pass_through")
|
||||
if mode != previous_mode:
|
||||
try:
|
||||
self._validate_processing_mode_selection(mode)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
with QSignalBlocker(self._processing_mode):
|
||||
self._set_combo_current_text(self._processing_mode, previous_mode)
|
||||
self._show_error(
|
||||
f"Cannot switch processing mode to {mode}",
|
||||
details=str(exc) or type(exc).__name__,
|
||||
)
|
||||
return
|
||||
|
||||
self._active_processing_mode = mode
|
||||
mode_to_page = {
|
||||
"pass_through": 0,
|
||||
"bscan": 1,
|
||||
@@ -655,8 +747,12 @@ class AppWindowConfigMixin:
|
||||
f"freq={self._gpr_start_freq_mhz.value():g}..{self._gpr_stop_freq_mhz.value():g} MHz, "
|
||||
f"speed={self._gpr_speed_m_s.value():g} m/s, "
|
||||
f"look_angle={self._gpr_look_angle_deg.value():g} deg, "
|
||||
f"snr_thresh={self._gpr_snr_thresh.value():g}, "
|
||||
f"snr_comp_max={self._gpr_snr_comp_max.value():g}, "
|
||||
f"background_subtract={self._gpr_background_subtract_enabled.isChecked()}, "
|
||||
f"mean_count={self._gpr_background_mean_count.value()})"
|
||||
f"mean_count={self._gpr_background_mean_count.value()}, "
|
||||
f"render_mode={self._gpr_render_mode.currentText()}, "
|
||||
f"min_pairs={self._gpr_min_visible_pair_count.value()})"
|
||||
)
|
||||
|
||||
def _clear_history_mode_caches(self) -> None:
|
||||
@@ -665,6 +761,7 @@ class AppWindowConfigMixin:
|
||||
self._clear_bscan_plot_history()
|
||||
if hasattr(self, "_bscan_plot"):
|
||||
self._bscan_plot.clear()
|
||||
self._configure_bscan_plot_axes()
|
||||
self._clear_trace_plots()
|
||||
if hasattr(self, "_gpr_plot"):
|
||||
self._clear_gpr_plot()
|
||||
@@ -676,6 +773,7 @@ class AppWindowConfigMixin:
|
||||
self._sync_bscan_history_from_results()
|
||||
if not self._draw_bscan_heatmap_from_history():
|
||||
self._bscan_plot.clear()
|
||||
self._configure_bscan_plot_axes()
|
||||
return
|
||||
if self._processing_mode.currentText() == "gpr":
|
||||
if self._result_history and self._draw_results(self._result_history[-1]):
|
||||
@@ -686,10 +784,12 @@ class AppWindowConfigMixin:
|
||||
self._draw_results(self._result_history[-1])
|
||||
return
|
||||
self._bscan_plot.clear()
|
||||
self._configure_bscan_plot_axes()
|
||||
self._clear_trace_plots()
|
||||
|
||||
def _on_radar_identity_changed(self, *_args) -> None:
|
||||
"""Refresh device limits when radar identity/mode changes."""
|
||||
self._reset_preprocess_selection_after_radar_key_change()
|
||||
if self._radar_mode.currentText() != "native":
|
||||
self._apply_radar_limits_to_ui(None)
|
||||
return
|
||||
@@ -699,6 +799,7 @@ class AppWindowConfigMixin:
|
||||
|
||||
def _on_radar_sweep_limits_changed(self) -> None:
|
||||
"""Clamp processing frequency bounds after sweep start/stop edits."""
|
||||
self._reset_preprocess_selection_after_radar_key_change()
|
||||
if self._sync_processing_frequency_limits_with_radar():
|
||||
self._on_processing_live_settings_changed()
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ from python_app.orchestration.shm_reader import ShmRingReader
|
||||
class AppWindowPipelineMixin:
|
||||
"""Controls start/stop, readers, and periodic polling of pipeline rings."""
|
||||
|
||||
def _processor_requires_restart(self, run_signature: tuple[object, ...]) -> bool:
|
||||
"""Return whether alive `data_processor` was started with different stable run settings."""
|
||||
return self._supervisor.is_processor_running() and self._processor_run_signature != run_signature
|
||||
|
||||
def _start_single_capture(self) -> None:
|
||||
"""Start acquisition in single-capture mode."""
|
||||
self._start_run(single_capture=True)
|
||||
@@ -40,11 +44,15 @@ class AppWindowPipelineMixin:
|
||||
try:
|
||||
processor_was_running = self._supervisor.is_processor_running()
|
||||
config = self._build_config()
|
||||
run_signature = self._build_run_history_signature(config)
|
||||
if self._processor_requires_restart(run_signature):
|
||||
self._log("Restarting data_processor because stable run settings changed")
|
||||
self._stop_all_processes()
|
||||
processor_was_running = False
|
||||
if not processor_was_running:
|
||||
self._reset_runtime_history()
|
||||
|
||||
self._validate_processing_mode_constraints(config)
|
||||
run_signature = self._build_run_history_signature(config)
|
||||
radar_key = self._radar_key(config)
|
||||
|
||||
missing_assets = [
|
||||
@@ -102,6 +110,7 @@ class AppWindowPipelineMixin:
|
||||
self._raw_reader = ShmRingReader(config.rings.raw_tap.name)
|
||||
self._pre_reader = ShmRingReader(config.rings.preprocessed_tap.name)
|
||||
self._result_reader = ShmRingReader(config.rings.results.name)
|
||||
self._processor_run_signature = run_signature
|
||||
self._single_capture_active = single_capture
|
||||
self._single_capture_start_ns = None
|
||||
self._single_capture_seen_raw = False
|
||||
@@ -138,6 +147,7 @@ class AppWindowPipelineMixin:
|
||||
return
|
||||
|
||||
was_running = self._supervisor.is_running()
|
||||
processor_only_running = self._supervisor.is_processor_running() and not was_running
|
||||
if was_running:
|
||||
self._stop_run()
|
||||
|
||||
@@ -145,6 +155,15 @@ class AppWindowPipelineMixin:
|
||||
if self._radar_mode.currentText() == "native":
|
||||
self._refresh_radar_limits_from_device()
|
||||
config = self._build_config()
|
||||
run_signature = self._build_run_history_signature(config)
|
||||
if processor_only_running and self._processor_requires_restart(run_signature):
|
||||
self._stop_all_processes()
|
||||
self._reset_runtime_history()
|
||||
self._history_run_signature = None
|
||||
self._log(
|
||||
"Stable run settings changed; stopped data_processor because cached history no longer "
|
||||
"matches the current config"
|
||||
)
|
||||
self._prepare_radar_for_native_acquisition(config)
|
||||
self._log(
|
||||
"Radar settings applied: "
|
||||
@@ -213,6 +232,7 @@ class AppWindowPipelineMixin:
|
||||
self._single_capture_start_ns = None
|
||||
self._single_capture_seen_raw = False
|
||||
self._single_capture_target_collection_id = None
|
||||
self._processor_run_signature = None
|
||||
self._update_history_indicator()
|
||||
self._status_label.setText("Status: idle")
|
||||
if was_running:
|
||||
|
||||
@@ -359,6 +359,249 @@ class AppWindowPlotMixin:
|
||||
self._sync_bscan_history_from_results()
|
||||
return self._draw_bscan_heatmap_from_history()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _configure_bscan_plot_axes(self) -> None:
|
||||
"""Apply persistent B-scan plot axis labels and base view settings."""
|
||||
plot = self._bscan_plot
|
||||
plot_item = plot.getPlotItem()
|
||||
plot_item.showAxis("left", show=True)
|
||||
plot_item.showAxis("bottom", show=True)
|
||||
plot.setLabel("bottom", "Sweep #")
|
||||
plot.setLabel("left", "Range", units="m")
|
||||
view_box = plot.getViewBox()
|
||||
view_box.invertY(False)
|
||||
view_box.enableAutoRange(x=False, y=False)
|
||||
|
||||
def _draw_bscan_heatmap_from_history(self) -> bool:
|
||||
"""Render B-scan heatmap from currently cached history arrays."""
|
||||
display_key = self._pick_bscan_display_key()
|
||||
@@ -391,13 +634,7 @@ class AppWindowPlotMixin:
|
||||
image_item.setLevels(self._bscan_levels(sweeps, axis_mode))
|
||||
|
||||
self._bscan_plot.clear()
|
||||
view_box = self._bscan_plot.getViewBox()
|
||||
view_box.invertY(True)
|
||||
view_box.enableAutoRange(x=False, y=False)
|
||||
self._bscan_plot.getPlotItem().showAxis("left", show=True)
|
||||
self._bscan_plot.getPlotItem().showAxis("bottom", show=True)
|
||||
self._bscan_plot.setLabel("bottom", "Sweep #")
|
||||
self._bscan_plot.setLabel("left", "Depth", units="m")
|
||||
self._configure_bscan_plot_axes()
|
||||
self._bscan_plot.addItem(image_item)
|
||||
self._bscan_plot.setXRange(x_min, x_max, padding=0.02)
|
||||
self._bscan_plot.setYRange(depth_min, depth_max, padding=0.02)
|
||||
@@ -538,6 +775,7 @@ class AppWindowPlotMixin:
|
||||
"""Clear latest GPR plot surface."""
|
||||
if not hasattr(self, "_gpr_plot"):
|
||||
return
|
||||
self._configure_gpr_plot_axes()
|
||||
self._clear_gpr_point_labels()
|
||||
self._clear_gpr_region_labels()
|
||||
self._clear_gpr_region_masks()
|
||||
@@ -557,23 +795,27 @@ class AppWindowPlotMixin:
|
||||
self._gpr_region_centers_item.hide()
|
||||
self._gpr_plot.setTitle(f"GPR {self._gpr_config_mode.currentText()}")
|
||||
|
||||
def _ensure_gpr_plot_items(self) -> None:
|
||||
"""Create persistent GPR plot items once and reuse them on redraw."""
|
||||
if self._gpr_image_item is not None:
|
||||
return
|
||||
|
||||
def _configure_gpr_plot_axes(self) -> None:
|
||||
"""Apply persistent GPR plot axis labels and base view settings."""
|
||||
plot = self._gpr_plot
|
||||
plot_item = plot.getPlotItem()
|
||||
plot_item.showAxis("left", show=True)
|
||||
plot_item.showAxis("bottom", show=True)
|
||||
plot_item.setClipToView(True)
|
||||
plot.setLabel("bottom", "X", units="m")
|
||||
plot.setLabel("left", "Depth", units="m")
|
||||
|
||||
plot.setLabel("left", "Range", units="m")
|
||||
view_box = plot.getViewBox()
|
||||
view_box.invertY(False)
|
||||
view_box.enableAutoRange(x=False, y=False)
|
||||
|
||||
def _ensure_gpr_plot_items(self) -> None:
|
||||
"""Create persistent GPR plot items once and reuse them on redraw."""
|
||||
if self._gpr_image_item is not None:
|
||||
return
|
||||
|
||||
plot = self._gpr_plot
|
||||
self._configure_gpr_plot_axes()
|
||||
|
||||
if self._gpr_lookup_table is None:
|
||||
self._gpr_lookup_table = self._build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"])
|
||||
|
||||
@@ -694,6 +936,12 @@ class AppWindowPlotMixin:
|
||||
return self._gpr_selected_geometry
|
||||
|
||||
def _draw_gpr_map(self, collection: ResultCollection) -> bool:
|
||||
"""Draw latest collection-level GPR plot according to current render mode."""
|
||||
if self._gpr_render_mode.currentText() == "objects_only":
|
||||
return self._draw_gpr_objects_only(collection)
|
||||
return self._draw_gpr_heatmap(collection)
|
||||
|
||||
def _draw_gpr_heatmap(self, collection: ResultCollection) -> bool:
|
||||
"""Draw latest collection-level GPR accumulator and annotations."""
|
||||
accumulator_payload = self._collection_payload_by_name(collection, "gpr_accumulator", kind=3)
|
||||
if accumulator_payload is None:
|
||||
@@ -729,36 +977,9 @@ class AppWindowPlotMixin:
|
||||
self._gpr_image_item.show()
|
||||
|
||||
plot.setXRange(x_min, x_max, padding=0.02)
|
||||
plot.setYRange(min(0.0, y_min), y_max, padding=0.02)
|
||||
plot.setYRange(self._gpr_display_y_min(y_min, y_max), y_max, padding=0.02)
|
||||
|
||||
x_tx, x_rx = self._selected_gpr_geometry()
|
||||
if x_tx.size > 0:
|
||||
self._gpr_tx_item.setData(
|
||||
x=x_tx,
|
||||
y=np.zeros_like(x_tx),
|
||||
symbol="t",
|
||||
size=13,
|
||||
brush=pg.mkBrush("#ff595e"),
|
||||
pen=pg.mkPen("#ffca3a", width=1.0),
|
||||
)
|
||||
self._gpr_tx_item.show()
|
||||
else:
|
||||
self._gpr_tx_item.setData(x=[], y=[])
|
||||
self._gpr_tx_item.hide()
|
||||
|
||||
if x_rx.size > 0:
|
||||
self._gpr_rx_item.setData(
|
||||
x=x_rx,
|
||||
y=np.zeros_like(x_rx),
|
||||
symbol="t1",
|
||||
size=13,
|
||||
brush=pg.mkBrush("#4cc9f0"),
|
||||
pen=pg.mkPen("#e0fbfc", width=1.0),
|
||||
)
|
||||
self._gpr_rx_item.show()
|
||||
else:
|
||||
self._gpr_rx_item.setData(x=[], y=[])
|
||||
self._gpr_rx_item.hide()
|
||||
self._draw_gpr_geometry_markers()
|
||||
|
||||
points_payload = self._collection_payload_by_name(collection, "gpr_points", kind=4)
|
||||
if points_payload is not None and np.asarray(points_payload.table).size > 0:
|
||||
@@ -824,6 +1045,242 @@ class AppWindowPlotMixin:
|
||||
plot.setUpdatesEnabled(True)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _normalized_display_range(start: float, stop: float, *, minimum_span: float = 0.1) -> tuple[float, float]:
|
||||
"""Return ordered display bounds with a non-zero span."""
|
||||
lower = min(float(start), float(stop))
|
||||
upper = max(float(start), float(stop))
|
||||
if upper - lower >= minimum_span:
|
||||
return lower, upper
|
||||
center = 0.5 * (lower + upper)
|
||||
half_span = 0.5 * minimum_span
|
||||
return center - half_span, center + half_span
|
||||
|
||||
def _gpr_visible_object_bounds(self) -> tuple[float, float, float, float]:
|
||||
"""Return normalized object-only visible X/Z bounds from GUI controls."""
|
||||
x_min, x_max = self._normalized_display_range(
|
||||
float(self._gpr_visible_x_min_m.value()),
|
||||
float(self._gpr_visible_x_max_m.value()),
|
||||
minimum_span=0.1,
|
||||
)
|
||||
z_min, z_max = self._normalized_display_range(
|
||||
float(self._gpr_visible_z_min_m.value()),
|
||||
float(self._gpr_visible_z_max_m.value()),
|
||||
minimum_span=0.1,
|
||||
)
|
||||
return x_min, x_max, z_min, z_max
|
||||
|
||||
@staticmethod
|
||||
def _gpr_display_y_min(z_min: float, z_max: float) -> float:
|
||||
"""Return lower GPR display bound with a small negative margin for antenna markers."""
|
||||
lower = min(0.0, float(z_min))
|
||||
span = max(float(z_max) - float(z_min), 1e-6)
|
||||
marker_margin = max(span * 0.03, 0.06)
|
||||
return lower - marker_margin
|
||||
|
||||
def _draw_gpr_geometry_markers(self) -> None:
|
||||
"""Render selected Tx/Rx geometry markers on current GPR plot."""
|
||||
x_tx, x_rx = self._selected_gpr_geometry()
|
||||
if x_tx.size > 0:
|
||||
self._gpr_tx_item.setData(
|
||||
x=x_tx,
|
||||
y=np.zeros_like(x_tx),
|
||||
symbol="t",
|
||||
size=13,
|
||||
brush=pg.mkBrush("#ff595e"),
|
||||
pen=pg.mkPen("#ffca3a", width=1.0),
|
||||
)
|
||||
self._gpr_tx_item.show()
|
||||
else:
|
||||
self._gpr_tx_item.setData(x=[], y=[])
|
||||
self._gpr_tx_item.hide()
|
||||
|
||||
if x_rx.size > 0:
|
||||
self._gpr_rx_item.setData(
|
||||
x=x_rx,
|
||||
y=np.zeros_like(x_rx),
|
||||
symbol="t1",
|
||||
size=13,
|
||||
brush=pg.mkBrush("#4cc9f0"),
|
||||
pen=pg.mkPen("#e0fbfc", width=1.0),
|
||||
)
|
||||
self._gpr_rx_item.show()
|
||||
else:
|
||||
self._gpr_rx_item.setData(x=[], y=[])
|
||||
self._gpr_rx_item.hide()
|
||||
|
||||
@staticmethod
|
||||
def _format_gpr_object_label(x_m: float, z_m: float, pair_count: float) -> str:
|
||||
"""Format object-only annotation text with pair count and coordinates."""
|
||||
return f"{int(round(pair_count))} | x={x_m:.1f} | z={z_m:.1f}"
|
||||
|
||||
@staticmethod
|
||||
def _expanded_scene_rect(rect: QRectF, *, padding_px: float = 4.0) -> QRectF:
|
||||
"""Return scene rect padded to keep labels visually separated."""
|
||||
return rect.adjusted(-padding_px, -padding_px, padding_px, padding_px)
|
||||
|
||||
@staticmethod
|
||||
def _scene_rect_intersects_any(rect: QRectF, occupied_rects: list[QRectF]) -> bool:
|
||||
"""Return whether candidate label rect intersects any already placed label."""
|
||||
return any(rect.intersects(occupied_rect) for occupied_rect in occupied_rects)
|
||||
|
||||
@staticmethod
|
||||
def _gpr_object_label_candidates(
|
||||
x_m: float,
|
||||
z_m: float,
|
||||
*,
|
||||
x_span: float,
|
||||
z_span: float,
|
||||
) -> list[tuple[float, float, tuple[float, float]]]:
|
||||
"""Return candidate label placements around one object."""
|
||||
x_offset = max(x_span * 0.015, 0.02)
|
||||
z_offset = max(z_span * 0.02, 0.02)
|
||||
return [
|
||||
(x_m + x_offset, z_m - z_offset, (0.0, 1.0)),
|
||||
(x_m + x_offset, z_m + z_offset, (0.0, 0.0)),
|
||||
(x_m - x_offset, z_m - z_offset, (1.0, 1.0)),
|
||||
(x_m - x_offset, z_m + z_offset, (1.0, 0.0)),
|
||||
(x_m, z_m - (z_offset * 1.35), (0.5, 1.0)),
|
||||
(x_m, z_m + (z_offset * 1.35), (0.5, 0.0)),
|
||||
(x_m + (x_offset * 2.2), z_m - (z_offset * 1.5), (0.0, 1.0)),
|
||||
(x_m + (x_offset * 2.2), z_m + (z_offset * 1.5), (0.0, 0.0)),
|
||||
(x_m - (x_offset * 2.2), z_m - (z_offset * 1.5), (1.0, 1.0)),
|
||||
(x_m - (x_offset * 2.2), z_m + (z_offset * 1.5), (1.0, 0.0)),
|
||||
]
|
||||
|
||||
def _place_gpr_object_label(
|
||||
self,
|
||||
*,
|
||||
label: pg.TextItem,
|
||||
x_m: float,
|
||||
z_m: float,
|
||||
x_span: float,
|
||||
z_span: float,
|
||||
occupied_scene_rects: list[QRectF],
|
||||
) -> None:
|
||||
"""Place one object label using the first non-overlapping candidate position."""
|
||||
last_rect: QRectF | None = None
|
||||
for label_x, label_z, anchor in self._gpr_object_label_candidates(
|
||||
x_m,
|
||||
z_m,
|
||||
x_span=x_span,
|
||||
z_span=z_span,
|
||||
):
|
||||
label.setAnchor(anchor)
|
||||
label.setPos(label_x, label_z)
|
||||
candidate_rect = self._expanded_scene_rect(label.sceneBoundingRect())
|
||||
last_rect = candidate_rect
|
||||
if not self._scene_rect_intersects_any(candidate_rect, occupied_scene_rects):
|
||||
occupied_scene_rects.append(candidate_rect)
|
||||
return
|
||||
|
||||
if last_rect is not None:
|
||||
occupied_scene_rects.append(last_rect)
|
||||
|
||||
def _gpr_object_rows(self, collection: ResultCollection) -> np.ndarray:
|
||||
"""Return object rows as `[x_m, z_m, pair_count]` from current GPR result payload."""
|
||||
points_payload = self._collection_payload_by_name(collection, "gpr_points", kind=4)
|
||||
if points_payload is not None:
|
||||
points = np.asarray(points_payload.table, dtype=np.float32)
|
||||
if points.ndim == 2 and points.shape[1] >= 3:
|
||||
return points[:, :3]
|
||||
|
||||
centers_payload = self._collection_payload_by_name(collection, "gpr_region_centers", kind=4)
|
||||
if centers_payload is not None:
|
||||
centers = np.asarray(centers_payload.table, dtype=np.float32)
|
||||
if centers.ndim == 2 and centers.shape[1] >= 3:
|
||||
return centers[:, :3]
|
||||
|
||||
return np.zeros((0, 3), dtype=np.float32)
|
||||
|
||||
def _filtered_gpr_object_rows(self, collection: ResultCollection) -> np.ndarray:
|
||||
"""Return object rows filtered by minimum pair count and visible X/Z bounds."""
|
||||
rows = self._gpr_object_rows(collection)
|
||||
if rows.size == 0:
|
||||
return rows
|
||||
|
||||
x_min, x_max, z_min, z_max = self._gpr_visible_object_bounds()
|
||||
min_pair_count = float(self._gpr_min_visible_pair_count.value())
|
||||
finite_mask = np.all(np.isfinite(rows[:, :3]), axis=1)
|
||||
visible_mask = (
|
||||
finite_mask
|
||||
& (rows[:, 2] >= min_pair_count)
|
||||
& (rows[:, 0] >= x_min)
|
||||
& (rows[:, 0] <= x_max)
|
||||
& (rows[:, 1] >= z_min)
|
||||
& (rows[:, 1] <= z_max)
|
||||
)
|
||||
return rows[visible_mask]
|
||||
|
||||
def _draw_gpr_objects_only(self, collection: ResultCollection) -> bool:
|
||||
"""Draw only detected GPR objects inside configured X/Z bounds."""
|
||||
accumulator_payload = self._collection_payload_by_name(collection, "gpr_accumulator", kind=3)
|
||||
object_rows = self._filtered_gpr_object_rows(collection)
|
||||
if accumulator_payload is None and object_rows.size == 0:
|
||||
self._clear_gpr_plot()
|
||||
return False
|
||||
|
||||
x_min, x_max, z_min, z_max = self._gpr_visible_object_bounds()
|
||||
plot = self._gpr_plot
|
||||
plot.setUpdatesEnabled(False)
|
||||
try:
|
||||
self._ensure_gpr_plot_items()
|
||||
plot.getViewBox().invertY(False)
|
||||
self._clear_gpr_point_labels()
|
||||
self._clear_gpr_region_labels()
|
||||
self._clear_gpr_region_masks()
|
||||
|
||||
self._gpr_image_item.hide()
|
||||
self._draw_gpr_geometry_markers()
|
||||
self._gpr_region_centers_item.setData(x=[], y=[])
|
||||
self._gpr_region_centers_item.hide()
|
||||
|
||||
if object_rows.size > 0:
|
||||
self._gpr_points_item.setData(
|
||||
x=object_rows[:, 0],
|
||||
y=object_rows[:, 1],
|
||||
symbol="o",
|
||||
size=18,
|
||||
brush=pg.mkBrush("#ff4d4f"),
|
||||
pen=pg.mkPen("#ff4d4f", width=1.6),
|
||||
)
|
||||
self._gpr_points_item.show()
|
||||
|
||||
occupied_scene_rects: list[QRectF] = []
|
||||
x_span = x_max - x_min
|
||||
z_span = z_max - z_min
|
||||
for x_value, z_value, pair_count in object_rows:
|
||||
label = pg.TextItem(
|
||||
text=self._format_gpr_object_label(
|
||||
float(x_value),
|
||||
float(z_value),
|
||||
float(pair_count),
|
||||
),
|
||||
color="#ffd6d9",
|
||||
anchor=(0.0, 1.0),
|
||||
)
|
||||
label.setZValue(40)
|
||||
plot.addItem(label)
|
||||
self._place_gpr_object_label(
|
||||
label=label,
|
||||
x_m=float(x_value),
|
||||
z_m=float(z_value),
|
||||
x_span=x_span,
|
||||
z_span=z_span,
|
||||
occupied_scene_rects=occupied_scene_rects,
|
||||
)
|
||||
self._gpr_point_labels.append(label)
|
||||
else:
|
||||
self._gpr_points_item.setData(x=[], y=[])
|
||||
self._gpr_points_item.hide()
|
||||
|
||||
plot.setXRange(x_min, x_max, padding=0.0)
|
||||
plot.setYRange(self._gpr_display_y_min(z_min, z_max), z_max, padding=0.0)
|
||||
plot.setTitle(f"GPR {self._gpr_config_mode.currentText()} Objects Only")
|
||||
finally:
|
||||
plot.setUpdatesEnabled(True)
|
||||
return True
|
||||
|
||||
def _result_collection_has_trace(self, collection: ResultCollection) -> bool:
|
||||
"""Return `True` when collection contains at least one trace payload."""
|
||||
if collection.collection_payloads:
|
||||
|
||||
@@ -15,6 +15,67 @@ from python_app.workflows.sequential_capture_workflow import SequentialCaptureSe
|
||||
class AppWindowPreprocessMixin:
|
||||
"""Handles preprocess set management and sequential capture workflows."""
|
||||
|
||||
@staticmethod
|
||||
def _capture_log_entries_for_session(session: SequentialCaptureSession) -> list[str]:
|
||||
"""Build capture-log rows from current session traces."""
|
||||
display_name = preprocess_asset_display_name(session.kind)
|
||||
total_count = session.state().total_count
|
||||
entries: list[str] = []
|
||||
for index, trace in enumerate(session.captured_traces(), start=1):
|
||||
entries.append(
|
||||
f"{display_name}: {index}/{total_count} | "
|
||||
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
|
||||
)
|
||||
return entries
|
||||
|
||||
def _available_preprocess_sets_for_radar_key(self, radar_key: str) -> dict[str, list[str]]:
|
||||
"""Load available preprocess-set names for one radar key."""
|
||||
return {
|
||||
key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
|
||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||
}
|
||||
|
||||
def _reset_preprocess_selection_after_radar_key_change(self) -> None:
|
||||
"""Clear selected preprocess sets when radar-key-defining settings change."""
|
||||
try:
|
||||
radar_key = self._radar_key_from_ui()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
previous_radar_key = getattr(self, "_selected_preprocess_radar_key", radar_key)
|
||||
if radar_key == previous_radar_key:
|
||||
return
|
||||
|
||||
self._selected_preprocess_radar_key = radar_key
|
||||
cleared = {
|
||||
key: value
|
||||
for key, value in self._selected_preprocess_sets.items()
|
||||
if value
|
||||
}
|
||||
if not cleared:
|
||||
if self._preprocess_dialog is not None:
|
||||
self._refresh_sets()
|
||||
return
|
||||
|
||||
self._selected_preprocess_sets = {
|
||||
key: ""
|
||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||
}
|
||||
self._refresh_preprocess_summary_labels()
|
||||
|
||||
if self._preprocess_dialog is not None:
|
||||
self._preprocess_dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
|
||||
self._refresh_sets()
|
||||
|
||||
cleared_summary = ", ".join(
|
||||
f"{preprocess_asset_display_name(key)}={value}"
|
||||
for key, value in cleared.items()
|
||||
)
|
||||
self._log(
|
||||
"Preprocess selection reset after radar settings changed: "
|
||||
f"{previous_radar_key} -> {radar_key}; cleared {cleared_summary}"
|
||||
)
|
||||
|
||||
def _open_preprocess_panel(self) -> None:
|
||||
"""Open preprocessing dialog and refresh available sets."""
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
@@ -42,6 +103,9 @@ class AppWindowPreprocessMixin:
|
||||
dialog.selection_changed.connect(self._on_preprocess_selection_changed)
|
||||
dialog.start_sequence_requested.connect(self._start_capture_sequence)
|
||||
dialog.capture_next_requested.connect(self._capture_next_combo)
|
||||
dialog.capture_all_requested.connect(self._capture_all_remaining)
|
||||
dialog.undo_last_requested.connect(self._undo_last_capture)
|
||||
dialog.finalize_sequence_requested.connect(self._finalize_capture_sequence)
|
||||
dialog.abort_sequence_requested.connect(self._abort_capture_sequence)
|
||||
self._update_capture_dialog_state()
|
||||
return dialog
|
||||
@@ -72,14 +136,11 @@ class AppWindowPreprocessMixin:
|
||||
|
||||
def _refresh_sets(self) -> None:
|
||||
"""Refresh preprocess set lists for current radar key."""
|
||||
config = self._build_config()
|
||||
radar_key = self._radar_key(config)
|
||||
radar_key = self._radar_key_from_ui()
|
||||
self._selected_preprocess_radar_key = radar_key
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
|
||||
available_sets = {
|
||||
key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
|
||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||
}
|
||||
available_sets = self._available_preprocess_sets_for_radar_key(radar_key)
|
||||
dialog.set_available_sets(available_sets)
|
||||
|
||||
unavailable_selections: list[str] = []
|
||||
@@ -137,7 +198,9 @@ class AppWindowPreprocessMixin:
|
||||
self._capture_session = session
|
||||
|
||||
dialog.clear_capture_log()
|
||||
dialog.reset_preview()
|
||||
dialog.set_status(f"{display_name} sequence started")
|
||||
self._clear_trace_plots()
|
||||
self._update_capture_dialog_state()
|
||||
self._log(
|
||||
f"{display_name} sequence started: set={set_name}, radar_key={radar_key}, "
|
||||
@@ -155,47 +218,134 @@ class AppWindowPreprocessMixin:
|
||||
self._show_error("No active capture sequence")
|
||||
return
|
||||
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
|
||||
try:
|
||||
trace = session.capture_current_combo()
|
||||
self._record_preprocess_capture(session, trace)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to capture preprocess combo", exc)
|
||||
self._abort_capture_sequence()
|
||||
|
||||
def _capture_all_remaining(self) -> None:
|
||||
"""Capture all remaining combos for the active preprocess session."""
|
||||
session = self._capture_session
|
||||
if session is None:
|
||||
self._show_error("No active capture sequence")
|
||||
return
|
||||
if session.is_complete():
|
||||
self._show_error(
|
||||
"Capture sequence is already complete",
|
||||
details=self._capture_state_details(),
|
||||
)
|
||||
return
|
||||
|
||||
display_name = preprocess_asset_display_name(session.kind)
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
dialog.set_status(f"{display_name} batch capture started")
|
||||
self._log(
|
||||
f"{display_name} batch capture started: remaining="
|
||||
f"{session.state().total_count - session.state().captured_count}"
|
||||
)
|
||||
try:
|
||||
while not session.is_complete():
|
||||
trace = session.capture_current_combo()
|
||||
self._record_preprocess_capture(session, trace)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to capture preprocess combo", exc)
|
||||
self._abort_capture_sequence()
|
||||
|
||||
def _record_preprocess_capture(self, session: SequentialCaptureSession, trace) -> None:
|
||||
"""Update UI, preview, and logs after one successful preprocess capture."""
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
state = session.state()
|
||||
display_name = preprocess_asset_display_name(session.kind)
|
||||
channel = preprocess_asset_channel(session.kind)
|
||||
|
||||
dialog.append_capture_log_entry(
|
||||
kind=display_name,
|
||||
captured_count=state.captured_count,
|
||||
total_count=state.total_count,
|
||||
input_pos=trace.combo.input_pos,
|
||||
output_pos=trace.combo.output_pos,
|
||||
)
|
||||
dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
|
||||
self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
|
||||
self._log(
|
||||
f"{display_name} capture: {state.captured_count}/{state.total_count} | "
|
||||
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
|
||||
)
|
||||
|
||||
self._update_capture_dialog_state()
|
||||
if session.is_complete():
|
||||
dialog.set_status(f"{display_name} sequence complete. Review captures or save the set.")
|
||||
self._log(
|
||||
f"{display_name} sequence capture complete: "
|
||||
f"{state.captured_count}/{state.total_count}; waiting for save or undo"
|
||||
)
|
||||
|
||||
def _undo_last_capture(self) -> None:
|
||||
"""Remove the most recently captured combo and rewind capture cursor."""
|
||||
session = self._capture_session
|
||||
if session is None:
|
||||
self._show_error("No active capture sequence")
|
||||
return
|
||||
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
try:
|
||||
removed_trace = session.undo_last_capture()
|
||||
state = session.state()
|
||||
display_name = preprocess_asset_display_name(session.kind)
|
||||
channel = preprocess_asset_channel(session.kind)
|
||||
|
||||
dialog.append_capture_log_entry(
|
||||
kind=display_name,
|
||||
captured_count=state.captured_count,
|
||||
total_count=state.total_count,
|
||||
input_pos=trace.combo.input_pos,
|
||||
output_pos=trace.combo.output_pos,
|
||||
)
|
||||
|
||||
dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
|
||||
self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
|
||||
|
||||
self._log(
|
||||
f"{display_name} capture: {state.captured_count}/{state.total_count} | "
|
||||
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
|
||||
)
|
||||
|
||||
if session.is_complete():
|
||||
radar_key, collection = session.finalize(self._store)
|
||||
set_name = session.set_name
|
||||
kind = session.kind
|
||||
display_name = preprocess_asset_display_name(kind)
|
||||
self._cleanup_capture_session()
|
||||
|
||||
self._selected_preprocess_sets[kind] = set_name
|
||||
self._refresh_sets()
|
||||
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._resume_pipeline_if_needed()
|
||||
dialog.set_capture_log_entries(self._capture_log_entries_for_session(session))
|
||||
last_trace = session.last_captured_trace()
|
||||
if last_trace is None:
|
||||
dialog.reset_preview()
|
||||
dialog.set_status(f"{display_name} last capture removed. No captured combos remain.")
|
||||
self._clear_trace_plots()
|
||||
else:
|
||||
self._update_capture_dialog_state()
|
||||
dialog.draw_last_trace(last_trace, title=f"{display_name} last trace", channel=channel)
|
||||
dialog.set_status(f"{display_name} last capture removed. Ready to recapture.")
|
||||
self._draw_single_trace(last_trace, title=f"{display_name} last trace", channel=channel)
|
||||
|
||||
self._update_capture_dialog_state()
|
||||
self._log(
|
||||
f"{display_name} undo last capture: removed input={removed_trace.combo.input_pos} "
|
||||
f"output={removed_trace.combo.output_pos}; remaining={state.captured_count}/{state.total_count}"
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to capture preprocess combo", exc)
|
||||
self._abort_capture_sequence()
|
||||
self._show_exception("Failed to undo last preprocess capture", exc)
|
||||
|
||||
def _finalize_capture_sequence(self) -> None:
|
||||
"""Persist completed capture session into preprocess-set storage."""
|
||||
session = self._capture_session
|
||||
if session is None:
|
||||
self._show_error("No active capture sequence")
|
||||
return
|
||||
if not session.is_complete():
|
||||
self._show_error(
|
||||
"Capture sequence is not complete",
|
||||
details=(
|
||||
f"Captured {session.state().captured_count}/{session.state().total_count} combos. "
|
||||
"Finish the remaining captures before saving."
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
try:
|
||||
radar_key, collection = session.finalize(self._store)
|
||||
set_name = session.set_name
|
||||
kind = session.kind
|
||||
display_name = preprocess_asset_display_name(kind)
|
||||
self._cleanup_capture_session()
|
||||
|
||||
self._selected_preprocess_sets[kind] = set_name
|
||||
self._refresh_sets()
|
||||
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._resume_pipeline_if_needed()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to save preprocess set", exc)
|
||||
|
||||
def _abort_capture_sequence(self, *, resume_pipeline: bool = True) -> None:
|
||||
"""Abort active capture session and optionally resume pipeline."""
|
||||
@@ -222,6 +372,9 @@ class AppWindowPreprocessMixin:
|
||||
total_count=0,
|
||||
next_input=None,
|
||||
next_output=None,
|
||||
can_undo=False,
|
||||
can_finalize=False,
|
||||
can_capture_all=False,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -238,6 +391,9 @@ class AppWindowPreprocessMixin:
|
||||
total_count=state.total_count,
|
||||
next_input=next_input,
|
||||
next_output=next_output,
|
||||
can_undo=state.can_undo,
|
||||
can_finalize=state.is_complete,
|
||||
can_capture_all=(not state.is_complete and state.current_combo is not None),
|
||||
)
|
||||
|
||||
def _cleanup_capture_session(self) -> None:
|
||||
|
||||
@@ -77,7 +77,7 @@ class AppWindowUiMixin:
|
||||
|
||||
# Default view on startup is pass-through traces.
|
||||
self._plot_stack.setCurrentWidget(self._trace_plots_container)
|
||||
root_layout.addWidget(self._plot_stack, stretch=12)
|
||||
root_layout.addWidget(self._plot_stack, stretch=11)
|
||||
|
||||
@staticmethod
|
||||
def _create_plot_widget(*, background: str) -> pg.PlotWidget:
|
||||
@@ -89,6 +89,7 @@ class AppWindowUiMixin:
|
||||
# B-scan surface: one PlotWidget used as canvas for ImageItem heatmap.
|
||||
self._bscan_plot = self._create_plot_widget(background="#0f141c")
|
||||
self._bscan_plot.showGrid(x=True, y=True, alpha=0.2)
|
||||
self._configure_bscan_plot_axes()
|
||||
self._plot_stack.addWidget(self._bscan_plot)
|
||||
|
||||
def _build_trace_plot_page(self) -> None:
|
||||
@@ -131,6 +132,7 @@ class AppWindowUiMixin:
|
||||
"""Create GPR page in plot stack."""
|
||||
self._gpr_plot = self._create_plot_widget(background="#0f141c")
|
||||
self._gpr_plot.showGrid(x=True, y=True, alpha=0.2)
|
||||
self._configure_gpr_plot_axes()
|
||||
self._plot_stack.addWidget(self._gpr_plot)
|
||||
|
||||
def _build_settings_toggle(self, root_layout: QHBoxLayout) -> None:
|
||||
@@ -142,7 +144,7 @@ class AppWindowUiMixin:
|
||||
root_layout.addWidget(self._settings_toggle_button, stretch=0)
|
||||
|
||||
def _build_settings_panel(self, root_layout: QHBoxLayout, root: QWidget) -> None:
|
||||
"""Build right settings panel with controls, status labels, and log."""
|
||||
"""Build right settings panel with controls, history summary, and log."""
|
||||
self._settings_panel = QWidget(root)
|
||||
self._settings_panel.setMinimumWidth(610)
|
||||
right_layout = QVBoxLayout(self._settings_panel)
|
||||
@@ -151,6 +153,31 @@ class AppWindowUiMixin:
|
||||
|
||||
# Build log early so `_show_error()` can append text even during
|
||||
# subsequent group construction if something fails.
|
||||
self._log_panel = self._build_log_panel()
|
||||
|
||||
right_layout.addWidget(build_primary_actions_group(self), stretch=0)
|
||||
right_layout.addWidget(self._build_settings_scroll(), stretch=1)
|
||||
|
||||
self._status_label = QLabel("Status: idle", self._settings_panel)
|
||||
self._status_label.setObjectName("statusLabel")
|
||||
self._status_label.hide()
|
||||
|
||||
self._history_label = QLabel("History: raw=0, preprocessed=0, results=0", self._settings_panel)
|
||||
self._history_label.setObjectName("hintLabel")
|
||||
right_layout.addWidget(self._history_label)
|
||||
|
||||
right_layout.addWidget(self._log_panel, stretch=0)
|
||||
root_layout.addWidget(self._settings_panel, stretch=7)
|
||||
|
||||
def _build_log_panel(self) -> QWidget:
|
||||
"""Build runtime log block with collapsible log body."""
|
||||
self._log_panel_title = QLabel("Runtime Log", self._settings_panel)
|
||||
self._log_panel_title.setObjectName("hintLabel")
|
||||
|
||||
self._log_toggle_button = QPushButton(self._settings_panel)
|
||||
self._log_toggle_button.setObjectName("sectionToggleButton")
|
||||
self._log_toggle_button.clicked.connect(lambda: self._toggle_log_panel())
|
||||
|
||||
self._log_box = QTextEdit(self._settings_panel)
|
||||
self._log_box.setObjectName("runtimeLogBox")
|
||||
self._log_box.setReadOnly(True)
|
||||
@@ -158,19 +185,24 @@ class AppWindowUiMixin:
|
||||
self._log_box.setMinimumHeight(170)
|
||||
self._log_box.document().setMaximumBlockCount(1200)
|
||||
|
||||
right_layout.addWidget(build_primary_actions_group(self), stretch=0)
|
||||
right_layout.addWidget(self._build_settings_scroll(), stretch=1)
|
||||
panel = QWidget(self._settings_panel)
|
||||
panel_layout = QVBoxLayout(panel)
|
||||
panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||
panel_layout.setSpacing(6)
|
||||
|
||||
self._status_label = QLabel("Status: idle", self._settings_panel)
|
||||
self._status_label.setObjectName("statusLabel")
|
||||
right_layout.addWidget(self._status_label)
|
||||
header_row = QWidget(panel)
|
||||
header_layout = QHBoxLayout(header_row)
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.setSpacing(8)
|
||||
header_layout.addWidget(self._log_panel_title)
|
||||
header_layout.addStretch(1)
|
||||
header_layout.addWidget(self._log_toggle_button)
|
||||
|
||||
self._history_label = QLabel("History: raw=0, preprocessed=0, results=0", self._settings_panel)
|
||||
self._history_label.setObjectName("hintLabel")
|
||||
right_layout.addWidget(self._history_label)
|
||||
panel_layout.addWidget(header_row)
|
||||
panel_layout.addWidget(self._log_box)
|
||||
|
||||
right_layout.addWidget(self._log_box, stretch=0)
|
||||
root_layout.addWidget(self._settings_panel, stretch=7)
|
||||
self._toggle_log_panel(visible=True)
|
||||
return panel
|
||||
|
||||
def _build_settings_scroll(self) -> QScrollArea:
|
||||
"""Build scroll area with all control groups in display order."""
|
||||
@@ -178,7 +210,7 @@ class AppWindowUiMixin:
|
||||
# remains usable on smaller screens and with future extra controls.
|
||||
controls = QWidget(self._settings_panel)
|
||||
controls_layout = QVBoxLayout(controls)
|
||||
controls_layout.setContentsMargins(0, 0, 0, 0)
|
||||
controls_layout.setContentsMargins(0, 0, 12, 0)
|
||||
controls_layout.setSpacing(10)
|
||||
for group in self._build_control_groups():
|
||||
controls_layout.addWidget(group)
|
||||
@@ -193,8 +225,8 @@ class AppWindowUiMixin:
|
||||
def _build_control_groups(self) -> list[QGroupBox]:
|
||||
"""Create all settings groups in top-to-bottom order."""
|
||||
return [
|
||||
build_switch_group(self),
|
||||
build_data_actions_group(self),
|
||||
build_switch_group(self),
|
||||
build_preprocess_summary_group(self),
|
||||
build_processing_group(self),
|
||||
build_radar_group(self),
|
||||
@@ -216,6 +248,19 @@ class AppWindowUiMixin:
|
||||
self._settings_toggle_button.setText("<")
|
||||
self._settings_toggle_button.setToolTip("Show settings panel")
|
||||
|
||||
def _toggle_log_panel(self, *, visible: bool | None = None) -> None:
|
||||
"""Toggle runtime log body visibility or force a specific state."""
|
||||
if visible is None:
|
||||
visible = not self._log_box.isVisible()
|
||||
|
||||
self._log_box.setVisible(visible)
|
||||
if visible:
|
||||
self._log_toggle_button.setText("Collapse")
|
||||
self._log_toggle_button.setToolTip("Hide runtime log entries")
|
||||
else:
|
||||
self._log_toggle_button.setText("Expand")
|
||||
self._log_toggle_button.setToolTip("Show runtime log entries")
|
||||
|
||||
def _set_plot_mode(self, mode: str) -> None:
|
||||
"""Switch visible plot page according to processing mode.
|
||||
|
||||
|
||||
@@ -31,9 +31,6 @@ def build_data_actions_group(owner) -> QGroupBox:
|
||||
remove_last_button = QPushButton("Remove Last Measurement")
|
||||
remove_last_button.clicked.connect(owner._remove_last_runtime_history)
|
||||
remove_last_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
clear_history_button = QPushButton("Clear Runtime History")
|
||||
clear_history_button.clicked.connect(owner._clear_all_runtime_history)
|
||||
clear_history_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
owner._save_count = QSpinBox()
|
||||
owner._save_count.setMinimum(1)
|
||||
owner._save_count.setMaximum(10_000)
|
||||
@@ -44,8 +41,7 @@ def build_data_actions_group(owner) -> QGroupBox:
|
||||
button_grid.setVerticalSpacing(8)
|
||||
button_grid.addWidget(save_button, 0, 0)
|
||||
button_grid.addWidget(save_vna_json_button, 0, 1)
|
||||
button_grid.addWidget(remove_last_button, 1, 0)
|
||||
button_grid.addWidget(clear_history_button, 1, 1)
|
||||
button_grid.addWidget(remove_last_button, 1, 0, 1, 2)
|
||||
button_grid.setColumnStretch(0, 1)
|
||||
button_grid.setColumnStretch(1, 1)
|
||||
layout.addLayout(button_grid)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Shared layout helpers for section builders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeAlias
|
||||
|
||||
from PyQt6.QtWidgets import QFormLayout, QHBoxLayout, QWidget
|
||||
|
||||
|
||||
FormLabel: TypeAlias = str | QWidget
|
||||
FormRow: TypeAlias = QWidget | tuple[FormLabel, QWidget]
|
||||
|
||||
|
||||
def build_two_column_form_widget(
|
||||
parent: QWidget,
|
||||
rows: list[FormRow],
|
||||
*,
|
||||
split_index: int | None = None,
|
||||
) -> QWidget:
|
||||
"""Build one widget containing two side-by-side form columns."""
|
||||
panel = QWidget(parent)
|
||||
panel_layout = QHBoxLayout(panel)
|
||||
panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||
panel_layout.setSpacing(12)
|
||||
|
||||
left_column = QWidget(panel)
|
||||
left_form = _create_form_layout(left_column)
|
||||
right_column = QWidget(panel)
|
||||
right_form = _create_form_layout(right_column)
|
||||
|
||||
panel_layout.addWidget(left_column, stretch=1)
|
||||
panel_layout.addWidget(right_column, stretch=1)
|
||||
|
||||
actual_split_index = (len(rows) + 1) // 2 if split_index is None else split_index
|
||||
actual_split_index = max(0, min(actual_split_index, len(rows)))
|
||||
|
||||
for row in rows[:actual_split_index]:
|
||||
_add_form_row(left_form, row)
|
||||
for row in rows[actual_split_index:]:
|
||||
_add_form_row(right_form, row)
|
||||
return panel
|
||||
|
||||
|
||||
def _create_form_layout(parent: QWidget) -> QFormLayout:
|
||||
"""Create a compact growing form layout for one column."""
|
||||
form = QFormLayout(parent)
|
||||
form.setContentsMargins(0, 0, 0, 0)
|
||||
form.setHorizontalSpacing(10)
|
||||
form.setVerticalSpacing(6)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
return form
|
||||
|
||||
|
||||
def _add_form_row(form: QFormLayout, row: FormRow) -> None:
|
||||
"""Append one form row, with or without an explicit label."""
|
||||
if isinstance(row, tuple):
|
||||
label, field = row
|
||||
form.addRow(label, field)
|
||||
return
|
||||
form.addRow(row)
|
||||
@@ -2,20 +2,25 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt6.QtWidgets import QFormLayout, QGroupBox, QLabel
|
||||
from PyQt6.QtWidgets import QGroupBox, QLabel, QVBoxLayout
|
||||
|
||||
from python_app.gui.controllers.sections.layout_helpers import build_two_column_form_widget
|
||||
from python_app.orchestration.preprocess_assets import VISIBLE_PREPROCESS_ASSET_KEYS, preprocess_asset_display_name
|
||||
|
||||
|
||||
def build_preprocess_summary_group(owner) -> QGroupBox:
|
||||
"""Create selected preprocess-set summary section."""
|
||||
group = QGroupBox("Selected Preprocess Sets")
|
||||
form = QFormLayout(group)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
layout = QVBoxLayout(group)
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(8)
|
||||
|
||||
owner._selected_preprocess_labels = {}
|
||||
rows = []
|
||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS:
|
||||
label = QLabel("<not selected>")
|
||||
owner._selected_preprocess_labels[key] = label
|
||||
form.addRow(preprocess_asset_display_name(key), label)
|
||||
rows.append((preprocess_asset_display_name(key), label))
|
||||
|
||||
layout.addWidget(build_two_column_form_widget(group, rows, split_index=(len(rows) + 1) // 2))
|
||||
return group
|
||||
|
||||
@@ -45,7 +45,11 @@ def build_primary_actions_group(owner) -> QGroupBox:
|
||||
|
||||
preprocess_button = _expanding_button("Preprocessing")
|
||||
preprocess_button.clicked.connect(owner._open_preprocess_panel)
|
||||
layout.addWidget(preprocess_button, 2, 0, 1, 3)
|
||||
layout.addWidget(preprocess_button, 2, 0)
|
||||
|
||||
clear_history_button = _expanding_button("Clear Runtime History")
|
||||
clear_history_button.clicked.connect(owner._clear_all_runtime_history)
|
||||
layout.addWidget(clear_history_button, 2, 1, 1, 2)
|
||||
|
||||
layout.setColumnStretch(0, 1)
|
||||
layout.setColumnStretch(1, 1)
|
||||
|
||||
@@ -13,9 +13,13 @@ from PyQt6.QtWidgets import (
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QStackedWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from python_app.gui.controllers.sections.layout_helpers import FormRow, build_two_column_form_widget
|
||||
|
||||
|
||||
def _format_tx_geometry(owner) -> str:
|
||||
"""Render Tx geometry defaults into editable line-based text."""
|
||||
return "\n".join(
|
||||
@@ -32,6 +36,17 @@ def _format_rx_geometry(owner) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _build_processing_mode_page(parent: QStackedWidget, rows: list[FormRow], *, split_index: int | None = None) -> QWidget:
|
||||
"""Create one processing-mode page with a two-column form body."""
|
||||
page = QWidget(parent)
|
||||
page.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
page_layout = QVBoxLayout(page)
|
||||
page_layout.setContentsMargins(0, 0, 0, 0)
|
||||
page_layout.setSpacing(0)
|
||||
page_layout.addWidget(build_two_column_form_widget(page, rows, split_index=split_index))
|
||||
return page
|
||||
|
||||
|
||||
def build_processing_group(owner) -> QGroupBox:
|
||||
"""Create processing mode section with pass-through, B-scan, and GPR pages."""
|
||||
group = QGroupBox("Processing")
|
||||
@@ -49,11 +64,6 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._processing_mode_pages = QStackedWidget(group)
|
||||
owner._processing_mode_pages.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
|
||||
pass_through_page = QWidget(owner._processing_mode_pages)
|
||||
pass_through_page.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
pass_through_form = QFormLayout(pass_through_page)
|
||||
pass_through_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
|
||||
owner._show_magnitude_checkbox = QCheckBox("Show magnitude")
|
||||
owner._show_magnitude_checkbox.setChecked(bool(pass_defaults.show_magnitude))
|
||||
|
||||
@@ -77,18 +87,19 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
|
||||
owner._sync_pass_through_y_controls()
|
||||
|
||||
pass_through_form.addRow(owner._show_magnitude_checkbox)
|
||||
pass_through_form.addRow(owner._show_phase_checkbox)
|
||||
pass_through_form.addRow(owner._pass_through_fixed_y_enabled)
|
||||
pass_through_form.addRow("Y min dB", owner._pass_through_y_min_db)
|
||||
pass_through_form.addRow("Y max dB", owner._pass_through_y_max_db)
|
||||
pass_through_page = _build_processing_mode_page(
|
||||
owner._processing_mode_pages,
|
||||
[
|
||||
owner._show_magnitude_checkbox,
|
||||
owner._show_phase_checkbox,
|
||||
owner._pass_through_fixed_y_enabled,
|
||||
("Y min dB", owner._pass_through_y_min_db),
|
||||
("Y max dB", owner._pass_through_y_max_db),
|
||||
],
|
||||
split_index=3,
|
||||
)
|
||||
owner._processing_mode_pages.addWidget(pass_through_page)
|
||||
|
||||
bscan_page = QWidget(owner._processing_mode_pages)
|
||||
bscan_page.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
bscan_form = QFormLayout(bscan_page)
|
||||
bscan_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
|
||||
owner._bscan_axis = QComboBox()
|
||||
owner._bscan_axis.addItems(["abs", "real", "phase"])
|
||||
owner._set_combo_current_text(owner._bscan_axis, bscan_defaults.axis)
|
||||
@@ -123,18 +134,20 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._bscan_stop_freq_mhz.setSingleStep(10.0)
|
||||
owner._bscan_stop_freq_mhz.setValue(float(bscan_defaults.stop_freq_mhz))
|
||||
|
||||
bscan_form.addRow("Axis", owner._bscan_axis)
|
||||
bscan_form.addRow("Cut m", owner._bscan_cut_m)
|
||||
bscan_form.addRow("Max depth m", owner._bscan_max_depth_m)
|
||||
bscan_form.addRow("Gain", owner._bscan_gain)
|
||||
bscan_form.addRow("Start MHz", owner._bscan_start_freq_mhz)
|
||||
bscan_form.addRow("Stop MHz", owner._bscan_stop_freq_mhz)
|
||||
bscan_page = _build_processing_mode_page(
|
||||
owner._processing_mode_pages,
|
||||
[
|
||||
("Axis", owner._bscan_axis),
|
||||
("Cut m", owner._bscan_cut_m),
|
||||
("Max depth m", owner._bscan_max_depth_m),
|
||||
("Gain", owner._bscan_gain),
|
||||
("Start MHz", owner._bscan_start_freq_mhz),
|
||||
("Stop MHz", owner._bscan_stop_freq_mhz),
|
||||
],
|
||||
split_index=3,
|
||||
)
|
||||
owner._processing_mode_pages.addWidget(bscan_page)
|
||||
|
||||
gpr_page = QWidget(owner._processing_mode_pages)
|
||||
gpr_page.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
gpr_form = QFormLayout(gpr_page)
|
||||
gpr_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
gpr_defaults = owner._defaults_config.gpr
|
||||
|
||||
owner._gpr_config_mode = QComboBox()
|
||||
@@ -149,11 +162,11 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
|
||||
owner._gpr_tx_geometry_input = QPlainTextEdit(_format_tx_geometry(owner))
|
||||
owner._gpr_tx_geometry_input.setPlaceholderText("output_pos x_m")
|
||||
owner._gpr_tx_geometry_input.setMinimumHeight(88)
|
||||
owner._gpr_tx_geometry_input.setFixedHeight(78)
|
||||
|
||||
owner._gpr_rx_geometry_input = QPlainTextEdit(_format_rx_geometry(owner))
|
||||
owner._gpr_rx_geometry_input.setPlaceholderText("input_pos x_m")
|
||||
owner._gpr_rx_geometry_input.setMinimumHeight(120)
|
||||
owner._gpr_rx_geometry_input.setFixedHeight(78)
|
||||
|
||||
owner._gpr_input_positions_input = QLineEdit(str(gpr_live_defaults.input_positions))
|
||||
owner._gpr_input_positions_input.setPlaceholderText("0,1,2")
|
||||
@@ -203,6 +216,18 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._gpr_look_angle_deg.setSingleStep(0.1)
|
||||
owner._gpr_look_angle_deg.setValue(float(gpr_live_defaults.look_angle_deg))
|
||||
|
||||
owner._gpr_snr_thresh = QDoubleSpinBox()
|
||||
owner._gpr_snr_thresh.setDecimals(2)
|
||||
owner._gpr_snr_thresh.setRange(0.0, 1_000.0)
|
||||
owner._gpr_snr_thresh.setSingleStep(0.1)
|
||||
owner._gpr_snr_thresh.setValue(float(gpr_live_defaults.snr_thresh))
|
||||
|
||||
owner._gpr_snr_comp_max = QDoubleSpinBox()
|
||||
owner._gpr_snr_comp_max.setDecimals(2)
|
||||
owner._gpr_snr_comp_max.setRange(0.0, 1_000.0)
|
||||
owner._gpr_snr_comp_max.setSingleStep(0.5)
|
||||
owner._gpr_snr_comp_max.setValue(float(gpr_live_defaults.snr_comp_max))
|
||||
|
||||
owner._gpr_background_subtract_enabled = QCheckBox("Subtract mean of previous collections")
|
||||
owner._gpr_background_subtract_enabled.setChecked(bool(gpr_live_defaults.background_subtract_enabled))
|
||||
|
||||
@@ -210,21 +235,67 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._gpr_background_mean_count.setRange(0, 10_000)
|
||||
owner._gpr_background_mean_count.setValue(int(gpr_live_defaults.background_mean_count))
|
||||
|
||||
gpr_form.addRow("Config mode", owner._gpr_config_mode)
|
||||
gpr_form.addRow("Relative permittivity", owner._gpr_relative_permittivity)
|
||||
gpr_form.addRow("Tx geometry", owner._gpr_tx_geometry_input)
|
||||
gpr_form.addRow("Rx geometry", owner._gpr_rx_geometry_input)
|
||||
gpr_form.addRow("Input positions", owner._gpr_input_positions_input)
|
||||
gpr_form.addRow("Output positions", owner._gpr_output_positions_input)
|
||||
gpr_form.addRow("Min depth m", owner._gpr_min_depth_m)
|
||||
gpr_form.addRow("Max depth m", owner._gpr_max_depth_m)
|
||||
gpr_form.addRow("Comp power", owner._gpr_comp_power)
|
||||
gpr_form.addRow("Start MHz", owner._gpr_start_freq_mhz)
|
||||
gpr_form.addRow("Stop MHz", owner._gpr_stop_freq_mhz)
|
||||
gpr_form.addRow("Speed m/s", owner._gpr_speed_m_s)
|
||||
gpr_form.addRow("Look angle deg", owner._gpr_look_angle_deg)
|
||||
gpr_form.addRow(owner._gpr_background_subtract_enabled)
|
||||
gpr_form.addRow("Mean count", owner._gpr_background_mean_count)
|
||||
owner._gpr_render_mode = QComboBox()
|
||||
owner._gpr_render_mode.addItems(["heatmap", "objects_only"])
|
||||
owner._set_combo_current_text(owner._gpr_render_mode, gpr_live_defaults.render_mode)
|
||||
|
||||
owner._gpr_min_visible_pair_count = QSpinBox()
|
||||
owner._gpr_min_visible_pair_count.setRange(1, 10_000)
|
||||
owner._gpr_min_visible_pair_count.setValue(int(gpr_live_defaults.min_visible_pair_count))
|
||||
|
||||
owner._gpr_visible_x_min_m = QDoubleSpinBox()
|
||||
owner._gpr_visible_x_min_m.setDecimals(2)
|
||||
owner._gpr_visible_x_min_m.setRange(-100.0, 100.0)
|
||||
owner._gpr_visible_x_min_m.setSingleStep(0.1)
|
||||
owner._gpr_visible_x_min_m.setValue(float(gpr_live_defaults.visible_x_min_m))
|
||||
|
||||
owner._gpr_visible_x_max_m = QDoubleSpinBox()
|
||||
owner._gpr_visible_x_max_m.setDecimals(2)
|
||||
owner._gpr_visible_x_max_m.setRange(-100.0, 100.0)
|
||||
owner._gpr_visible_x_max_m.setSingleStep(0.1)
|
||||
owner._gpr_visible_x_max_m.setValue(float(gpr_live_defaults.visible_x_max_m))
|
||||
|
||||
owner._gpr_visible_z_min_m = QDoubleSpinBox()
|
||||
owner._gpr_visible_z_min_m.setDecimals(2)
|
||||
owner._gpr_visible_z_min_m.setRange(0.0, 100.0)
|
||||
owner._gpr_visible_z_min_m.setSingleStep(0.1)
|
||||
owner._gpr_visible_z_min_m.setValue(float(gpr_live_defaults.visible_z_min_m))
|
||||
|
||||
owner._gpr_visible_z_max_m = QDoubleSpinBox()
|
||||
owner._gpr_visible_z_max_m.setDecimals(2)
|
||||
owner._gpr_visible_z_max_m.setRange(0.0, 100.0)
|
||||
owner._gpr_visible_z_max_m.setSingleStep(0.1)
|
||||
owner._gpr_visible_z_max_m.setValue(float(gpr_live_defaults.visible_z_max_m))
|
||||
|
||||
gpr_page = _build_processing_mode_page(
|
||||
owner._processing_mode_pages,
|
||||
[
|
||||
("Config mode", owner._gpr_config_mode),
|
||||
("Relative permittivity", owner._gpr_relative_permittivity),
|
||||
("Input positions", owner._gpr_input_positions_input),
|
||||
("Output positions", owner._gpr_output_positions_input),
|
||||
("Min depth m", owner._gpr_min_depth_m),
|
||||
("Max depth m", owner._gpr_max_depth_m),
|
||||
("Comp power", owner._gpr_comp_power),
|
||||
("SNR thresh", owner._gpr_snr_thresh),
|
||||
("SNR comp max", owner._gpr_snr_comp_max),
|
||||
("Speed m/s", owner._gpr_speed_m_s),
|
||||
("Render mode", owner._gpr_render_mode),
|
||||
("Min visible pairs", owner._gpr_min_visible_pair_count),
|
||||
("Tx geometry", owner._gpr_tx_geometry_input),
|
||||
("Rx geometry", owner._gpr_rx_geometry_input),
|
||||
("Start MHz", owner._gpr_start_freq_mhz),
|
||||
("Stop MHz", owner._gpr_stop_freq_mhz),
|
||||
("Look angle deg", owner._gpr_look_angle_deg),
|
||||
("Visible X min m", owner._gpr_visible_x_min_m),
|
||||
("Visible X max m", owner._gpr_visible_x_max_m),
|
||||
("Visible Z min m", owner._gpr_visible_z_min_m),
|
||||
("Visible Z max m", owner._gpr_visible_z_max_m),
|
||||
owner._gpr_background_subtract_enabled,
|
||||
("Mean count", owner._gpr_background_mean_count),
|
||||
],
|
||||
split_index=11,
|
||||
)
|
||||
owner._processing_mode_pages.addWidget(gpr_page)
|
||||
|
||||
owner._processing_mode.currentTextChanged.connect(owner._on_processing_mode_changed)
|
||||
@@ -252,8 +323,16 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._gpr_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_snr_thresh.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_snr_comp_max.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_background_subtract_enabled.toggled.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_background_mean_count.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_render_mode.currentTextChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||
owner._gpr_min_visible_pair_count.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||
owner._gpr_visible_x_min_m.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||
owner._gpr_visible_x_max_m.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||
owner._gpr_visible_z_min_m.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||
owner._gpr_visible_z_max_m.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||
|
||||
owner._on_processing_mode_changed(owner._processing_mode.currentText())
|
||||
return group
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt6.QtWidgets import QComboBox, QFormLayout, QGroupBox, QLabel, QLineEdit
|
||||
from PyQt6.QtWidgets import QComboBox, QGroupBox, QLabel, QLineEdit, QVBoxLayout
|
||||
|
||||
from python_app.gui.controllers.sections.layout_helpers import build_two_column_form_widget
|
||||
|
||||
|
||||
def build_radar_group(owner) -> QGroupBox:
|
||||
"""Create radar settings controls and labels."""
|
||||
group = QGroupBox("Radar")
|
||||
form = QFormLayout(group)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
layout = QVBoxLayout(group)
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(8)
|
||||
defaults = owner._defaults_config.radar
|
||||
|
||||
owner._serial_input = QLineEdit(defaults.serial)
|
||||
@@ -30,6 +33,9 @@ def build_radar_group(owner) -> QGroupBox:
|
||||
owner._radar_mode.currentTextChanged.connect(owner._on_radar_identity_changed)
|
||||
owner._start_hz_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||
owner._stop_hz_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||
owner._points_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||
owner._ifbw_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||
owner._power_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||
|
||||
owner._radar_start_label = QLabel("Start Hz")
|
||||
owner._radar_stop_label = QLabel("Stop Hz")
|
||||
@@ -40,10 +46,18 @@ def build_radar_group(owner) -> QGroupBox:
|
||||
owner._radar_limits_hint = QLabel("Mock mode: device limits are not applied.")
|
||||
owner._radar_limits_hint.setObjectName("hintLabel")
|
||||
|
||||
form.addRow(owner._radar_start_label, owner._start_hz_input)
|
||||
form.addRow(owner._radar_stop_label, owner._stop_hz_input)
|
||||
form.addRow(owner._radar_points_label, owner._points_input)
|
||||
form.addRow(owner._radar_ifbw_label, owner._ifbw_input)
|
||||
form.addRow(owner._radar_power_label, owner._power_input)
|
||||
form.addRow(owner._radar_limits_hint)
|
||||
layout.addWidget(
|
||||
build_two_column_form_widget(
|
||||
group,
|
||||
[
|
||||
(owner._radar_start_label, owner._start_hz_input),
|
||||
(owner._radar_stop_label, owner._stop_hz_input),
|
||||
(owner._radar_points_label, owner._points_input),
|
||||
(owner._radar_ifbw_label, owner._ifbw_input),
|
||||
(owner._radar_power_label, owner._power_input),
|
||||
],
|
||||
split_index=3,
|
||||
)
|
||||
)
|
||||
layout.addWidget(owner._radar_limits_hint)
|
||||
return group
|
||||
|
||||
@@ -36,6 +36,9 @@ class PreprocessDialog(QDialog):
|
||||
selection_changed = pyqtSignal()
|
||||
start_sequence_requested = pyqtSignal(str)
|
||||
capture_next_requested = pyqtSignal()
|
||||
capture_all_requested = pyqtSignal()
|
||||
undo_last_requested = pyqtSignal()
|
||||
finalize_sequence_requested = pyqtSignal()
|
||||
abort_sequence_requested = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
@@ -97,6 +100,8 @@ class PreprocessDialog(QDialog):
|
||||
|
||||
for key in keys:
|
||||
combo = QComboBox(group)
|
||||
combo.setPlaceholderText("<not selected>")
|
||||
combo.setCurrentIndex(-1)
|
||||
combo.currentTextChanged.connect(self._emit_selection_changed)
|
||||
self._set_combos[key] = combo
|
||||
form.addRow(self._asset_row_label(key), combo)
|
||||
@@ -142,12 +147,27 @@ class PreprocessDialog(QDialog):
|
||||
self._capture_next_button.clicked.connect(self.capture_next_requested.emit)
|
||||
self._capture_next_button.setEnabled(False)
|
||||
|
||||
self._capture_all_button = QPushButton("Capture All Remaining", parent)
|
||||
self._capture_all_button.clicked.connect(self.capture_all_requested.emit)
|
||||
self._capture_all_button.setEnabled(False)
|
||||
|
||||
self._undo_last_button = QPushButton("Undo Last Capture", parent)
|
||||
self._undo_last_button.clicked.connect(self.undo_last_requested.emit)
|
||||
self._undo_last_button.setEnabled(False)
|
||||
|
||||
self._save_sequence_button = QPushButton("Save Captured Set", parent)
|
||||
self._save_sequence_button.clicked.connect(self.finalize_sequence_requested.emit)
|
||||
self._save_sequence_button.setEnabled(False)
|
||||
|
||||
self._abort_button = QPushButton("Abort Sequence", parent)
|
||||
self._abort_button.clicked.connect(self.abort_sequence_requested.emit)
|
||||
self._abort_button.setEnabled(False)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(self._capture_next_button)
|
||||
layout.addWidget(self._capture_all_button)
|
||||
layout.addWidget(self._undo_last_button)
|
||||
layout.addWidget(self._save_sequence_button)
|
||||
layout.addWidget(self._abort_button)
|
||||
layout.addStretch(1)
|
||||
return layout
|
||||
@@ -189,6 +209,10 @@ class PreprocessDialog(QDialog):
|
||||
"""Clear capture history text box."""
|
||||
self._capture_log.clear()
|
||||
|
||||
def set_capture_log_entries(self, entries: list[str]) -> None:
|
||||
"""Replace capture history text with provided rows."""
|
||||
self._capture_log.setPlainText("\n".join(entries))
|
||||
|
||||
def append_capture_log_entry(
|
||||
self,
|
||||
*,
|
||||
@@ -212,6 +236,9 @@ class PreprocessDialog(QDialog):
|
||||
total_count: int,
|
||||
next_input: int | None,
|
||||
next_output: int | None,
|
||||
can_undo: bool,
|
||||
can_finalize: bool,
|
||||
can_capture_all: bool,
|
||||
) -> None:
|
||||
"""Update sequence progress/status widgets."""
|
||||
if kind is None:
|
||||
@@ -219,20 +246,27 @@ class PreprocessDialog(QDialog):
|
||||
self._progress_label.setText("0 / 0")
|
||||
self._combo_label.setText("<none>")
|
||||
self._capture_next_button.setEnabled(False)
|
||||
self._capture_all_button.setEnabled(False)
|
||||
self._undo_last_button.setEnabled(False)
|
||||
self._save_sequence_button.setEnabled(False)
|
||||
self._abort_button.setEnabled(False)
|
||||
self._capture_all_button.setText("Capture All Remaining")
|
||||
return
|
||||
|
||||
active_label = preprocess_asset_display_name(kind) if kind in PREPROCESS_ASSET_SPECS else kind
|
||||
self._active_kind_label.setText(active_label)
|
||||
self._progress_label.setText(f"{captured_count} / {total_count}")
|
||||
self._undo_last_button.setEnabled(bool(can_undo))
|
||||
self._save_sequence_button.setEnabled(bool(can_finalize))
|
||||
self._capture_all_button.setEnabled(bool(can_capture_all))
|
||||
self._abort_button.setEnabled(True)
|
||||
self._capture_all_button.setText("Capture All Remaining")
|
||||
if next_input is None or next_output is None:
|
||||
self._combo_label.setText("<complete>")
|
||||
self._capture_next_button.setEnabled(False)
|
||||
self._abort_button.setEnabled(True)
|
||||
else:
|
||||
self._combo_label.setText(f"input={next_input}, output={next_output}")
|
||||
self._capture_next_button.setEnabled(True)
|
||||
self._abort_button.setEnabled(True)
|
||||
|
||||
def set_available_sets(self, available_sets: dict[str, list[str]]) -> None:
|
||||
"""Replace combo-box choices for all preprocess assets."""
|
||||
@@ -245,10 +279,11 @@ class PreprocessDialog(QDialog):
|
||||
"""Apply selected set names to all comboboxes and optionally emit update."""
|
||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS:
|
||||
selected_value = selected_sets.get(key, "")
|
||||
if not selected_value:
|
||||
continue
|
||||
combo = self._set_combos[key]
|
||||
with QSignalBlocker(combo):
|
||||
if not selected_value:
|
||||
combo.setCurrentIndex(-1)
|
||||
continue
|
||||
index = combo.findText(selected_value)
|
||||
if index < 0:
|
||||
combo.addItem(selected_value)
|
||||
@@ -261,6 +296,14 @@ class PreprocessDialog(QDialog):
|
||||
"""Set short human-readable status line."""
|
||||
self._status_label.setText(message)
|
||||
|
||||
def reset_preview(self) -> None:
|
||||
"""Clear preview surface and restore default empty-state text when possible."""
|
||||
if self._preview_plot is not None:
|
||||
self._preview_plot.clear()
|
||||
self._preview_plot.setTitle("")
|
||||
if self._preview_placeholder is not None:
|
||||
self._preview_placeholder.setText("Preview will appear after the first successful capture.")
|
||||
|
||||
def draw_last_trace(self, trace: TraceData, title: str, *, channel: str) -> None:
|
||||
"""Draw the latest captured sweep trace for the requested channel in dB scale."""
|
||||
samples = trace.s11 if channel == "s11" else trace.s21
|
||||
@@ -328,6 +371,7 @@ class PreprocessDialog(QDialog):
|
||||
combo.clear()
|
||||
combo.addItems(names)
|
||||
if not current_text:
|
||||
combo.setCurrentIndex(-1)
|
||||
return
|
||||
index = combo.findText(current_text)
|
||||
if index < 0:
|
||||
|
||||
@@ -27,6 +27,10 @@ def validate_processing_mode_constraints(
|
||||
if processing_mode != "gpr":
|
||||
return
|
||||
|
||||
configured_combos = {(int(combo.input), int(combo.output)) for combo in config.combos}
|
||||
if len(configured_combos) < 2:
|
||||
raise RuntimeError(f"GPR requires at least 2 distinct run combos (now {len(configured_combos)})")
|
||||
|
||||
available_inputs = sorted({int(entry.input_pos) for entry in config.gpr.rx_geometry})
|
||||
available_outputs = sorted({int(entry.output_pos) for entry in config.gpr.tx_geometry})
|
||||
if not available_inputs or not available_outputs:
|
||||
@@ -46,7 +50,6 @@ def validate_processing_mode_constraints(
|
||||
raise RuntimeError(f"GPR output positions are missing from geometry config: {missing_outputs}")
|
||||
|
||||
required_combos = {(int(input_pos), int(output_pos)) for input_pos in selected_inputs for output_pos in selected_outputs}
|
||||
configured_combos = {(int(combo.input), int(combo.output)) for combo in config.combos}
|
||||
missing_combos = sorted(required_combos - configured_combos)
|
||||
if missing_combos:
|
||||
raise RuntimeError(
|
||||
|
||||
@@ -71,6 +71,12 @@ QPushButton#settingsToggleButton {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QPushButton#sectionToggleButton {
|
||||
min-width: 84px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
QLineEdit,
|
||||
QPlainTextEdit,
|
||||
QTextEdit,
|
||||
|
||||
@@ -208,6 +208,18 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
||||
gui.processing.gpr.look_angle_deg,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
snr_thresh=_optional_float(
|
||||
gpr_object,
|
||||
"snr_thresh",
|
||||
gui.processing.gpr.snr_thresh,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
snr_comp_max=_optional_float(
|
||||
gpr_object,
|
||||
"snr_comp_max",
|
||||
gui.processing.gpr.snr_comp_max,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
background_subtract_enabled=_optional_bool(
|
||||
gpr_object,
|
||||
"background_subtract_enabled",
|
||||
@@ -220,12 +232,56 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
||||
gui.processing.gpr.background_mean_count,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
render_mode=_optional_string(
|
||||
gpr_object,
|
||||
"render_mode",
|
||||
gui.processing.gpr.render_mode,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
min_visible_pair_count=_optional_int(
|
||||
gpr_object,
|
||||
"min_visible_pair_count",
|
||||
gui.processing.gpr.min_visible_pair_count,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
visible_x_min_m=_optional_float(
|
||||
gpr_object,
|
||||
"visible_x_min_m",
|
||||
gui.processing.gpr.visible_x_min_m,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
visible_x_max_m=_optional_float(
|
||||
gpr_object,
|
||||
"visible_x_max_m",
|
||||
gui.processing.gpr.visible_x_max_m,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
visible_z_min_m=_optional_float(
|
||||
gpr_object,
|
||||
"visible_z_min_m",
|
||||
gui.processing.gpr.visible_z_min_m,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
visible_z_max_m=_optional_float(
|
||||
gpr_object,
|
||||
"visible_z_max_m",
|
||||
gui.processing.gpr.visible_z_max_m,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
),
|
||||
)
|
||||
if gui.processing.selected_mode not in {"pass_through", "bscan", "gpr"}:
|
||||
raise ValueError("gui.processing.selected_mode must be one of: pass_through, bscan, gpr")
|
||||
if gui.processing.bscan.axis not in {"abs", "real", "phase"}:
|
||||
raise ValueError("gui.processing.bscan.axis must be one of: abs, real, phase")
|
||||
if gui.processing.gpr.render_mode not in {"heatmap", "objects_only"}:
|
||||
raise ValueError("gui.processing.gpr.render_mode must be one of: heatmap, objects_only")
|
||||
if gui.processing.gpr.snr_thresh < 0.0:
|
||||
raise ValueError("gui.processing.gpr.snr_thresh must be >= 0")
|
||||
if gui.processing.gpr.snr_comp_max < 0.0:
|
||||
raise ValueError("gui.processing.gpr.snr_comp_max must be >= 0")
|
||||
if gui.processing.gpr.min_visible_pair_count < 1:
|
||||
raise ValueError("gui.processing.gpr.min_visible_pair_count must be >= 1")
|
||||
|
||||
data_actions_object = _as_dict(gui_object.get("data_actions"), "gui.data_actions")
|
||||
gui.data_actions = GuiDataActionsStateModel(
|
||||
@@ -302,8 +358,16 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]:
|
||||
"stop_freq_mhz": gui.processing.gpr.stop_freq_mhz,
|
||||
"speed_m_s": gui.processing.gpr.speed_m_s,
|
||||
"look_angle_deg": gui.processing.gpr.look_angle_deg,
|
||||
"snr_thresh": gui.processing.gpr.snr_thresh,
|
||||
"snr_comp_max": gui.processing.gpr.snr_comp_max,
|
||||
"background_subtract_enabled": gui.processing.gpr.background_subtract_enabled,
|
||||
"background_mean_count": gui.processing.gpr.background_mean_count,
|
||||
"render_mode": gui.processing.gpr.render_mode,
|
||||
"min_visible_pair_count": gui.processing.gpr.min_visible_pair_count,
|
||||
"visible_x_min_m": gui.processing.gpr.visible_x_min_m,
|
||||
"visible_x_max_m": gui.processing.gpr.visible_x_max_m,
|
||||
"visible_z_min_m": gui.processing.gpr.visible_z_min_m,
|
||||
"visible_z_max_m": gui.processing.gpr.visible_z_max_m,
|
||||
},
|
||||
},
|
||||
"data_actions": {
|
||||
|
||||
@@ -57,8 +57,16 @@ class GuiGprStateModel:
|
||||
stop_freq_mhz: float = 6000.0
|
||||
speed_m_s: float = 0.0
|
||||
look_angle_deg: float = 0.0
|
||||
snr_thresh: float = 4.5
|
||||
snr_comp_max: float = 25.0
|
||||
background_subtract_enabled: bool = True
|
||||
background_mean_count: int = 10
|
||||
render_mode: str = "heatmap"
|
||||
min_visible_pair_count: int = 1
|
||||
visible_x_min_m: float = -2.0
|
||||
visible_x_max_m: float = 2.0
|
||||
visible_z_min_m: float = 0.0
|
||||
visible_z_max_m: float = 14.0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
||||
@@ -32,6 +32,8 @@ class ProcessingLiveConfig:
|
||||
gpr_stop_freq_mhz: float = 6000.0
|
||||
gpr_speed_m_s: float = 0.0
|
||||
gpr_look_angle_deg: float = 0.0
|
||||
gpr_snr_thresh: float = 4.5
|
||||
gpr_snr_comp_max: float = 25.0
|
||||
gpr_background_subtract_enabled: bool = True
|
||||
gpr_background_mean_count: int = 10
|
||||
history_command_seq: int = 0
|
||||
@@ -72,6 +74,8 @@ class ProcessingLiveConfig:
|
||||
"gpr_stop_freq_mhz": float(self.gpr_stop_freq_mhz),
|
||||
"gpr_speed_m_s": float(self.gpr_speed_m_s),
|
||||
"gpr_look_angle_deg": float(self.gpr_look_angle_deg),
|
||||
"gpr_snr_thresh": float(self.gpr_snr_thresh),
|
||||
"gpr_snr_comp_max": float(self.gpr_snr_comp_max),
|
||||
"gpr_background_subtract_enabled": bool(self.gpr_background_subtract_enabled),
|
||||
"gpr_background_mean_count": int(self.gpr_background_mean_count),
|
||||
"history_command_seq": int(self.history_command_seq),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"last_profile_path": "/home/europa/Documents/radar_system/run_config.json"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"processor_mode": "gpr",
|
||||
"pass_through_channel": "s21",
|
||||
"pass_through_fixed_y_enabled": false,
|
||||
"pass_through_y_min_db": -100.0,
|
||||
"pass_through_y_max_db": 0.0,
|
||||
"bscan_axis": "abs",
|
||||
"bscan_channel": "s21",
|
||||
"bscan_cut_m": 0.824,
|
||||
"bscan_max_depth_m": 1.0,
|
||||
"bscan_gain": 1.0,
|
||||
"bscan_start_freq_mhz": 100.0,
|
||||
"bscan_stop_freq_mhz": 6000.0,
|
||||
"gpr_input_positions": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"gpr_output_positions": [
|
||||
0,
|
||||
3
|
||||
],
|
||||
"gpr_min_depth_m": 2.0,
|
||||
"gpr_max_depth_m": 14.0,
|
||||
"gpr_comp_power": 0.2,
|
||||
"gpr_start_freq_mhz": 3000.0,
|
||||
"gpr_stop_freq_mhz": 6000.0,
|
||||
"gpr_speed_m_s": 0.0,
|
||||
"gpr_look_angle_deg": 0.0,
|
||||
"gpr_background_subtract_enabled": false,
|
||||
"gpr_background_mean_count": 10,
|
||||
"history_command_seq": 0,
|
||||
"history_command": "none"
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"radar": {
|
||||
"model": "librevna",
|
||||
"serial": "",
|
||||
"driver_mode": "native",
|
||||
"mock_signal_hz": 5000000.0,
|
||||
"sweep": {
|
||||
"start_hz": 1000000.0,
|
||||
"stop_hz": 6000000000.0,
|
||||
"points": 201,
|
||||
"if_bandwidth_hz": 50000.0,
|
||||
"stimulus_power_dbm": -10.0
|
||||
}
|
||||
},
|
||||
"switches": {
|
||||
"port1": {
|
||||
"name": "port1",
|
||||
"driver_mode": "native",
|
||||
"driver": "h7992",
|
||||
"radar_port": 1,
|
||||
"positions": 4,
|
||||
"default_position": 0,
|
||||
"gpio_chip": "/dev/gpiochip0",
|
||||
"pin_a": 17,
|
||||
"pin_b": 27,
|
||||
"invert_logic": false
|
||||
},
|
||||
"port2": {
|
||||
"name": "port2",
|
||||
"driver_mode": "native",
|
||||
"driver": "h7992",
|
||||
"radar_port": 2,
|
||||
"positions": 4,
|
||||
"default_position": 0,
|
||||
"gpio_chip": "/dev/gpiochip0",
|
||||
"pin_a": 22,
|
||||
"pin_b": 23,
|
||||
"invert_logic": false
|
||||
}
|
||||
},
|
||||
"run": {
|
||||
"settling_ms": 0,
|
||||
"idle_sleep_ms": 2,
|
||||
"continuous": false,
|
||||
"processing_live_config_path": "/home/europa/Documents/radar_system/python_app/runtime/processing_live.json",
|
||||
"combos": [
|
||||
{
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 1,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 2,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 3,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 0,
|
||||
"output": 3
|
||||
},
|
||||
{
|
||||
"input": 1,
|
||||
"output": 3
|
||||
},
|
||||
{
|
||||
"input": 2,
|
||||
"output": 3
|
||||
},
|
||||
{
|
||||
"input": 3,
|
||||
"output": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
"preprocess": {
|
||||
"s21": {
|
||||
"calibration": {
|
||||
"set_name": "smoke_cal",
|
||||
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_calibration_bundle.bin"
|
||||
},
|
||||
"reference": {
|
||||
"set_name": "smoke_ref",
|
||||
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_reference_bundle.bin"
|
||||
}
|
||||
},
|
||||
"s11": {
|
||||
"calibration": {
|
||||
"open": {
|
||||
"set_name": "",
|
||||
"bundle_path": ""
|
||||
},
|
||||
"short": {
|
||||
"set_name": "",
|
||||
"bundle_path": ""
|
||||
},
|
||||
"load": {
|
||||
"set_name": "",
|
||||
"bundle_path": ""
|
||||
}
|
||||
},
|
||||
"reference": {
|
||||
"set_name": "",
|
||||
"bundle_path": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"gpr": {
|
||||
"mode": "point",
|
||||
"relative_permittivity": 1.0,
|
||||
"tx_geometry": [
|
||||
{
|
||||
"output_pos": 0,
|
||||
"x_m": 0.905
|
||||
},
|
||||
{
|
||||
"output_pos": 3,
|
||||
"x_m": -0.905
|
||||
}
|
||||
],
|
||||
"rx_geometry": [
|
||||
{
|
||||
"input_pos": 0,
|
||||
"x_m": -0.18
|
||||
},
|
||||
{
|
||||
"input_pos": 1,
|
||||
"x_m": 0.485
|
||||
},
|
||||
{
|
||||
"input_pos": 2,
|
||||
"x_m": -0.49
|
||||
},
|
||||
{
|
||||
"input_pos": 3,
|
||||
"x_m": 0.185
|
||||
}
|
||||
]
|
||||
},
|
||||
"rings": {
|
||||
"raw": {
|
||||
"name": "/radar_raw",
|
||||
"capacity": 50,
|
||||
"slot_size_bytes": 2097152
|
||||
},
|
||||
"raw_tap": {
|
||||
"name": "/radar_raw_tap",
|
||||
"capacity": 50,
|
||||
"slot_size_bytes": 2097152
|
||||
},
|
||||
"preprocessed": {
|
||||
"name": "/radar_preprocessed",
|
||||
"capacity": 50,
|
||||
"slot_size_bytes": 2097152
|
||||
},
|
||||
"preprocessed_tap": {
|
||||
"name": "/radar_preprocessed_tap",
|
||||
"capacity": 50,
|
||||
"slot_size_bytes": 2097152
|
||||
},
|
||||
"results": {
|
||||
"name": "/radar_results",
|
||||
"capacity": 50,
|
||||
"slot_size_bytes": 2097152
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -24,6 +24,8 @@ class SequentialCaptureState:
|
||||
captured_count: int
|
||||
total_count: int
|
||||
current_combo: ComboModel | None
|
||||
can_undo: bool
|
||||
is_complete: bool
|
||||
|
||||
|
||||
class SequentialCaptureSession:
|
||||
@@ -112,6 +114,8 @@ class SequentialCaptureSession:
|
||||
captured_count=len(self._traces),
|
||||
total_count=len(self._combos),
|
||||
current_combo=current_combo,
|
||||
can_undo=bool(self._traces),
|
||||
is_complete=self.is_complete(),
|
||||
)
|
||||
|
||||
def capture_current_combo(self) -> TraceData:
|
||||
@@ -138,6 +142,34 @@ class SequentialCaptureSession:
|
||||
self._next_index += 1
|
||||
return trace
|
||||
|
||||
def undo_last_capture(self) -> TraceData:
|
||||
"""Remove the most recently captured trace and rewind cursor by one combo."""
|
||||
if not self._opened:
|
||||
raise RuntimeError("Capture session is not opened")
|
||||
if not self._traces or self._next_index <= 0:
|
||||
raise RuntimeError("No captured combo is available to undo")
|
||||
|
||||
expected_combo = self._combos[self._next_index - 1]
|
||||
removed_trace = self._traces[-1]
|
||||
if (
|
||||
int(removed_trace.combo.input_pos) != int(expected_combo.input)
|
||||
or int(removed_trace.combo.output_pos) != int(expected_combo.output)
|
||||
):
|
||||
raise RuntimeError("Capture session state is inconsistent; last trace does not match rewind combo")
|
||||
self._next_index -= 1
|
||||
self._traces.pop()
|
||||
return removed_trace
|
||||
|
||||
def last_captured_trace(self) -> TraceData | None:
|
||||
"""Return the most recently captured trace, if any."""
|
||||
if not self._traces:
|
||||
return None
|
||||
return self._traces[-1]
|
||||
|
||||
def captured_traces(self) -> list[TraceData]:
|
||||
"""Return captured traces in acquisition order."""
|
||||
return list(self._traces)
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
"""Return `True` when all combos were captured."""
|
||||
return self._next_index >= len(self._combos)
|
||||
|
||||
Reference in New Issue
Block a user