Refactor radar system
This commit is contained in:
@@ -2,17 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack
|
||||
import math
|
||||
import time
|
||||
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from PyQt6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDoubleSpinBox,
|
||||
QLineEdit,
|
||||
QPlainTextEdit,
|
||||
QSpinBox,
|
||||
)
|
||||
|
||||
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
|
||||
from python_app.gui.antenna_geometry_editor import AntennaGeometryEditor
|
||||
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
|
||||
|
||||
_GPR_MODES = ("gpr", "legacy_gpr")
|
||||
@@ -56,6 +60,9 @@ _WEB_LIVE_SCHEMA = [
|
||||
("bscan_start_freq_mhz", "B-scan", ("bscan",), _attr("_bscan_start_freq_mhz")),
|
||||
("bscan_stop_freq_mhz", "B-scan", ("bscan",), _attr("_bscan_stop_freq_mhz")),
|
||||
("legacy_gpr_mode", "Mode", ("legacy_gpr",), _attr("_legacy_gpr_config_mode")),
|
||||
("relative_permittivity", "Geometry & medium", ("legacy_gpr",), _attr("_gpr_relative_permittivity")),
|
||||
("tx_geometry", "Geometry & medium", _GPR_LIKE_MODES, _attr("_gpr_tx_geometry_input")),
|
||||
("rx_geometry", "Geometry & medium", _GPR_LIKE_MODES, _attr("_gpr_rx_geometry_input")),
|
||||
("gpr_input_positions", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_input_positions_input", {"legacy_gpr": "_legacy_gpr_input_positions_input", "sar3d": "_sar3d_input_positions_input"})),
|
||||
("gpr_output_positions", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_output_positions_input", {"legacy_gpr": "_legacy_gpr_output_positions_input", "sar3d": "_sar3d_output_positions_input"})),
|
||||
("gpr_min_depth_m", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_min_depth_m", {"legacy_gpr": "_legacy_gpr_min_depth_m", "sar3d": "_sar3d_min_depth_m"})),
|
||||
@@ -107,6 +114,7 @@ _NON_WIDGET_LIVE_FIELDS = frozenset({
|
||||
"reprocess_current_result",
|
||||
"history_command",
|
||||
"history_command_seq",
|
||||
"settings_revision",
|
||||
})
|
||||
|
||||
# Display toggles: GUI-only rendering choices (kept in the GUI profile, not the live
|
||||
@@ -118,21 +126,10 @@ _WEB_DISPLAY_SCHEMA = [
|
||||
("combo_filter", "Display", ("pass_through",), _attr("_pass_through_combo_filter_input")),
|
||||
]
|
||||
|
||||
# Stable run_config fields (GPR medium and antenna geometry). They live in run_config and
|
||||
# take effect only when the pipeline (re)starts — not hot-reloaded — so the web marks them
|
||||
# "applies on Start" and editing them just updates the widget for the next start.
|
||||
_WEB_STABLE_SCHEMA = [
|
||||
# Coherent BP fixes the medium to eps_r = 1 (Horns_motion_3libre.py), so relative
|
||||
# permittivity is a legacy-GPR-only knob; BP ignores it.
|
||||
("relative_permittivity", "Geometry & medium", ("legacy_gpr",), _attr("_gpr_relative_permittivity")),
|
||||
("tx_geometry", "Geometry & medium", _GPR_LIKE_MODES, _attr("_gpr_tx_geometry_input")),
|
||||
("rx_geometry", "Geometry & medium", _GPR_LIKE_MODES, _attr("_gpr_rx_geometry_input")),
|
||||
]
|
||||
|
||||
# Every field the web form renders and may write back, across all three categories.
|
||||
# Every field the web form renders and may write back.
|
||||
_WEB_FORM_GETTERS = {
|
||||
field: getter
|
||||
for source in (_WEB_LIVE_SCHEMA, _WEB_DISPLAY_SCHEMA, _WEB_STABLE_SCHEMA)
|
||||
for source in (_WEB_LIVE_SCHEMA, _WEB_DISPLAY_SCHEMA)
|
||||
for field, _group, _modes, getter in source
|
||||
}
|
||||
|
||||
@@ -157,8 +154,9 @@ def _web_field_schema(field: str, group: str, widget) -> dict | None:
|
||||
return {**base, "kind": "float", "value": float(widget.value()),
|
||||
"min": float(widget.minimum()), "max": float(widget.maximum()),
|
||||
"step": float(widget.singleStep()), "decimals": int(widget.decimals())}
|
||||
if isinstance(widget, QPlainTextEdit):
|
||||
return {**base, "kind": "textarea", "value": widget.toPlainText()}
|
||||
if isinstance(widget, AntennaGeometryEditor):
|
||||
return {**base, "kind": "geometry", "value": widget.records(),
|
||||
"position_field": widget.position_field, "position_count": widget.position_count}
|
||||
if isinstance(widget, QLineEdit):
|
||||
return {**base, "kind": "text", "value": widget.text()}
|
||||
return None
|
||||
@@ -180,48 +178,47 @@ def _read_live_widget_value(widget):
|
||||
return float(widget.value())
|
||||
if isinstance(widget, QLineEdit):
|
||||
return widget.text()
|
||||
if isinstance(widget, AntennaGeometryEditor):
|
||||
return widget.records()
|
||||
raise TypeError(f"Unsupported live-settings widget: {type(widget).__name__}")
|
||||
|
||||
|
||||
def _build_web_settings_schema(window) -> list[dict]:
|
||||
"""Build the settings schema for the active mode straight from the Qt widgets.
|
||||
|
||||
Covers all three widget categories — live settings, GUI display toggles, and stable
|
||||
run_config fields — so the web form shows exactly what the desktop processing panel
|
||||
does. Stable fields are tagged ``applies_on_start`` (they take effect on the next run).
|
||||
Covers live settings and GUI display toggles, matching the desktop panel.
|
||||
"""
|
||||
mode = window._processing_mode.currentText()
|
||||
schema: list[dict] = []
|
||||
for source, extra in (
|
||||
(_WEB_LIVE_SCHEMA, {}),
|
||||
(_WEB_DISPLAY_SCHEMA, {}),
|
||||
(_WEB_STABLE_SCHEMA, {"applies_on_start": True}),
|
||||
):
|
||||
for source in (_WEB_LIVE_SCHEMA, _WEB_DISPLAY_SCHEMA):
|
||||
for field, group, modes, getter in source:
|
||||
if modes is not None and mode not in modes:
|
||||
continue
|
||||
entry = _web_field_schema(field, group, getter(window))
|
||||
if entry is not None:
|
||||
schema.append({**entry, **extra})
|
||||
schema.append(entry)
|
||||
return schema
|
||||
|
||||
|
||||
def _set_web_live_field(window, field: str, value) -> None:
|
||||
"""Write one web value into the desktop widget that feeds ``field`` (any category)."""
|
||||
getter = _WEB_FORM_GETTERS.get(field)
|
||||
if getter is None:
|
||||
return
|
||||
widget = getter(window)
|
||||
def _set_widget_value(window, widget, value) -> None:
|
||||
"""Validate and write a web value, also used when rolling back a rejected batch."""
|
||||
if isinstance(widget, QComboBox):
|
||||
if widget.findText(str(value)) < 0:
|
||||
raise ValueError(f"Unknown setting option: {value}")
|
||||
window._set_combo_current_text(widget, str(value))
|
||||
elif isinstance(widget, QCheckBox):
|
||||
widget.setChecked(bool(value))
|
||||
elif isinstance(widget, QSpinBox):
|
||||
widget.setValue(int(float(value)))
|
||||
elif isinstance(widget, QDoubleSpinBox):
|
||||
widget.setValue(float(value))
|
||||
elif isinstance(widget, QPlainTextEdit):
|
||||
widget.setPlainText(str(value))
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError("Checkbox value must be true or false")
|
||||
widget.setChecked(value)
|
||||
elif isinstance(widget, (QSpinBox, QDoubleSpinBox)):
|
||||
numeric = float(value)
|
||||
if not math.isfinite(numeric) or not widget.minimum() <= numeric <= widget.maximum():
|
||||
raise ValueError("Numeric setting is outside its allowed range")
|
||||
if isinstance(widget, QSpinBox) and numeric != int(numeric):
|
||||
raise ValueError("Setting requires an integer")
|
||||
widget.setValue(int(numeric) if isinstance(widget, QSpinBox) else numeric)
|
||||
elif isinstance(widget, AntennaGeometryEditor):
|
||||
widget.set_records(value)
|
||||
elif isinstance(widget, QLineEdit):
|
||||
widget.setText(",".join(str(int(v)) for v in value) if isinstance(value, list) else str(value))
|
||||
|
||||
@@ -297,6 +294,7 @@ class AppWindowLiveProcessingMixin:
|
||||
reprocess_current_result=bool(reprocess_current_result),
|
||||
history_command=str(history_command),
|
||||
history_command_seq=int(self._history_command_seq),
|
||||
settings_revision=getattr(self, "_processing_settings_revision", 0),
|
||||
**values,
|
||||
)
|
||||
|
||||
@@ -315,12 +313,15 @@ class AppWindowLiveProcessingMixin:
|
||||
"""
|
||||
if bump_history_seq:
|
||||
self._history_command_seq += 1
|
||||
self._processing_settings_revision = time.monotonic_ns()
|
||||
self._live_config_writer.write(
|
||||
self._live_processing_config(
|
||||
history_command=history_command,
|
||||
reprocess_current_result=not self._can_reprocess_history(),
|
||||
)
|
||||
)
|
||||
if getattr(self, "_await_processing_revision", False):
|
||||
self._required_processing_revision = self._processing_settings_revision
|
||||
|
||||
def _apply_web_live_settings(self, fields: dict) -> None:
|
||||
"""Apply web-requested live settings through the SAME path as a desktop edit.
|
||||
@@ -339,6 +340,10 @@ class AppWindowLiveProcessingMixin:
|
||||
"""
|
||||
try:
|
||||
history_command = str(fields.get("history_command", "none"))
|
||||
if history_command not in {"none", "clear_all", "remove_last"}:
|
||||
raise ValueError("Unknown history command")
|
||||
if history_command != "none" and set(fields) != {"history_command"}:
|
||||
raise ValueError("History commands must be sent separately from settings")
|
||||
if history_command == "clear_all":
|
||||
self._clear_all_runtime_history()
|
||||
return
|
||||
@@ -356,15 +361,30 @@ class AppWindowLiveProcessingMixin:
|
||||
self._log_debug(f"Applying web live settings: fields={sorted(settings)}.")
|
||||
self._suppress_live_settings_handler = True
|
||||
try:
|
||||
# processor_mode first: dual-sourced gpr_* fields route to the gpr or
|
||||
# legacy_gpr widget based on the active mode.
|
||||
if "processor_mode" in settings:
|
||||
_set_web_live_field(self, "processor_mode", settings.pop("processor_mode"))
|
||||
for name, value in settings.items():
|
||||
_set_web_live_field(self, name, value)
|
||||
with ExitStack() as blockers:
|
||||
snapshots = []
|
||||
try:
|
||||
# Set the mode under a blocker before resolving per-mode fields.
|
||||
ordered = sorted(settings, key=lambda name: name != "processor_mode")
|
||||
for name in ordered:
|
||||
widget = _WEB_FORM_GETTERS[name](self)
|
||||
blockers.enter_context(QSignalBlocker(widget))
|
||||
snapshots.append((widget, _read_live_widget_value(widget)))
|
||||
_set_widget_value(self, widget, settings[name])
|
||||
self._validate_processing_mode_selection(self._processing_mode.currentText())
|
||||
except Exception:
|
||||
for widget, value in reversed(snapshots):
|
||||
_set_widget_value(self, widget, value)
|
||||
raise
|
||||
finally:
|
||||
self._suppress_live_settings_handler = False
|
||||
self._on_processing_live_settings_changed()
|
||||
self._sync_socket_speed_controls()
|
||||
if {"tx_geometry", "rx_geometry", "relative_permittivity"} & settings.keys():
|
||||
self._await_processing_revision = True
|
||||
if "processor_mode" in settings:
|
||||
self._on_processing_mode_changed(self._processing_mode.currentText())
|
||||
else:
|
||||
self._on_processing_live_settings_changed()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_exception("Failed to apply web live settings", exc)
|
||||
|
||||
@@ -372,6 +392,12 @@ class AppWindowLiveProcessingMixin:
|
||||
"""Return the live-settings schema for the active mode (read from widgets)."""
|
||||
return _build_web_settings_schema(self)
|
||||
|
||||
def _on_gpr_geometry_changed(self) -> None:
|
||||
if getattr(self, "_suppress_live_settings_handler", False):
|
||||
return
|
||||
self._await_processing_revision = True
|
||||
self._on_processing_live_settings_changed()
|
||||
|
||||
def _on_processing_live_settings_changed(self, *_args) -> None:
|
||||
"""Handle live-processing setting changes and trigger redraw when needed."""
|
||||
# Web-apply sets many widgets at once; their individual change signals are
|
||||
@@ -399,11 +425,12 @@ class AppWindowLiveProcessingMixin:
|
||||
collection = latest
|
||||
if collection is None and self._result_history:
|
||||
collection = self._result_history[-1]
|
||||
if collection is None or not self._draw_results(collection):
|
||||
if current_mode == "sar3d":
|
||||
self._clear_sar3d_plot()
|
||||
else:
|
||||
self._clear_gpr_plot()
|
||||
if current_mode == "sar3d":
|
||||
self._clear_sar3d_plot()
|
||||
if collection is not None:
|
||||
self._draw_results(collection)
|
||||
elif collection is None or not self._draw_results(collection):
|
||||
self._clear_gpr_plot()
|
||||
elif self._result_history:
|
||||
self._draw_results(self._result_history[-1])
|
||||
else:
|
||||
@@ -589,9 +616,9 @@ class AppWindowLiveProcessingMixin:
|
||||
self._clear_gpr_plot()
|
||||
return
|
||||
if self._processing_mode.currentText() == "sar3d":
|
||||
if self._result_history and self._draw_results(self._result_history[-1]):
|
||||
return
|
||||
self._clear_sar3d_plot()
|
||||
if self._result_history:
|
||||
self._draw_results(self._result_history[-1])
|
||||
return
|
||||
if self._result_history:
|
||||
self._draw_results(self._result_history[-1])
|
||||
|
||||
@@ -10,6 +10,8 @@ from pathlib import Path
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from PyQt6.QtWidgets import QFileDialog
|
||||
|
||||
from python_app.file_io import atomic_write_json
|
||||
from python_app.gui.controllers.app_window_config.state_builders import GUI_SAVE_HISTORY_LIMIT
|
||||
from python_app.models.gui_profile_model import GuiProfileModel
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.preprocess_assets import (
|
||||
@@ -27,8 +29,7 @@ class AppWindowConfigProfileIOMixin:
|
||||
raise FileExistsError(f"Config profile output already exists: {output_path}")
|
||||
|
||||
profile = self._build_gui_profile()
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(json.dumps(profile.to_dict(), indent=2), encoding="utf-8")
|
||||
atomic_write_json(output_path, profile.to_dict(), durable=True)
|
||||
return profile
|
||||
|
||||
def _set_combo_selection_mode(self, mode: str) -> None:
|
||||
@@ -42,6 +43,7 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._single_combo_input.setEnabled(True)
|
||||
self._run_combos_select_button.setEnabled(False)
|
||||
self._single_combo_select_button.setEnabled(False)
|
||||
self._next_combo_button.setEnabled(True)
|
||||
return
|
||||
|
||||
text_selected = mode != "single"
|
||||
@@ -52,6 +54,32 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._single_combo_input.setEnabled(not text_selected)
|
||||
self._run_combos_select_button.setEnabled(True)
|
||||
self._single_combo_select_button.setEnabled(True)
|
||||
self._next_combo_button.setEnabled(not text_selected)
|
||||
|
||||
def _select_display_combo(self, input_pos: int, output_pos: int) -> None:
|
||||
"""Select an available pair and redraw its cached B-scan immediately."""
|
||||
key = (int(input_pos), int(output_pos))
|
||||
if key not in self._available_display_combo_keys():
|
||||
raise ValueError(f"Unknown channel combination: {key[0]}:{key[1]}")
|
||||
self._single_combo_input.setText(str(key[0]))
|
||||
self._single_combo_output.setText(str(key[1]))
|
||||
self._on_bscan_display_combo_changed()
|
||||
|
||||
def _select_next_combo(self) -> None:
|
||||
"""Advance output first, then input, wrapping after the last configured pair."""
|
||||
available = self._available_display_combo_keys()
|
||||
if not available:
|
||||
return
|
||||
try:
|
||||
current = (int(self._single_combo_input.text()), int(self._single_combo_output.text()))
|
||||
index = available.index(current) + 1
|
||||
except ValueError:
|
||||
index = 0
|
||||
self._select_display_combo(*available[index % len(available)])
|
||||
|
||||
def _available_display_combo_keys(self) -> list[tuple[int, int]]:
|
||||
"""Return configured pairs ordered by input, then output for display navigation."""
|
||||
return sorted((combo.input, combo.output) for combo in self._defaults_config.build_runtime_combos())
|
||||
|
||||
def _is_matrix_radar_model_selected(self) -> bool:
|
||||
"""Return whether the loaded config targets a matrix-mode radar acquisition."""
|
||||
@@ -124,11 +152,10 @@ class AppWindowConfigProfileIOMixin:
|
||||
C++ replay window still follows ring capacities, since it bounds how much
|
||||
of the history the processor can re-publish coherently.
|
||||
"""
|
||||
save_history_limit = self._save_history_limit_for_config(config)
|
||||
bscan_cpp_replay_window = self._cpp_bscan_replay_window_for_config(config)
|
||||
self._raw_history = deque(self._raw_history, maxlen=save_history_limit)
|
||||
self._pre_history = deque(self._pre_history, maxlen=save_history_limit)
|
||||
self._result_history = deque(self._result_history, maxlen=save_history_limit)
|
||||
self._raw_history = deque(self._raw_history, maxlen=GUI_SAVE_HISTORY_LIMIT)
|
||||
self._pre_history = deque(self._pre_history, maxlen=GUI_SAVE_HISTORY_LIMIT)
|
||||
self._result_history = deque(self._result_history, maxlen=GUI_SAVE_HISTORY_LIMIT)
|
||||
self._bscan_cpp_replay_window = bscan_cpp_replay_window
|
||||
self._clear_bscan_plot_history()
|
||||
|
||||
@@ -351,7 +378,6 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._sar3d_comp_range_power,
|
||||
self._sar3d_use_compensation,
|
||||
self._sar3d_normalize_each_frame,
|
||||
self._save_count,
|
||||
self._record_count,
|
||||
self._save_path_input,
|
||||
self._save_name_input,
|
||||
@@ -457,18 +483,10 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._bscan_history_window.setValue(int(gui_state.processing.bscan.history_window_scans))
|
||||
|
||||
self._gpr_relative_permittivity.setValue(float(config.gpr.relative_permittivity))
|
||||
self._gpr_tx_geometry_input.setPlainText(
|
||||
"\n".join(
|
||||
f"{int(entry.output_pos)} {float(entry.x_m):g}"
|
||||
for entry in config.gpr.tx_geometry
|
||||
)
|
||||
)
|
||||
self._gpr_rx_geometry_input.setPlainText(
|
||||
"\n".join(
|
||||
f"{int(entry.input_pos)} {float(entry.x_m):g}"
|
||||
for entry in config.gpr.rx_geometry
|
||||
)
|
||||
)
|
||||
self._gpr_tx_geometry_input.set_position_count(config.output_switch.positions)
|
||||
self._gpr_rx_geometry_input.set_position_count(config.input_switch.positions)
|
||||
self._gpr_tx_geometry_input.set_records(config.gpr.tx_geometry)
|
||||
self._gpr_rx_geometry_input.set_records(config.gpr.rx_geometry)
|
||||
self._gpr_input_positions_input.setText(str(gui_state.processing.gpr.input_positions))
|
||||
self._gpr_output_positions_input.setText(str(gui_state.processing.gpr.output_positions))
|
||||
self._gpr_min_depth_m.setValue(float(gui_state.processing.gpr.min_depth_m))
|
||||
@@ -555,7 +573,6 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._sar3d_normalize_each_frame.setChecked(bool(sar3d_state.normalize_each_frame))
|
||||
self._sync_socket_speed_controls()
|
||||
|
||||
self._save_count.setValue(int(gui_state.data_actions.save_count))
|
||||
self._record_count.setValue(int(gui_state.data_actions.record_count))
|
||||
self._save_path_input.setText(str(gui_state.data_actions.save_path))
|
||||
self._save_name_input.setText(str(gui_state.data_actions.save_name))
|
||||
@@ -590,6 +607,7 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._apply_initial_radar_limits()
|
||||
if self._preprocess_dialog is not None:
|
||||
self._refresh_sets()
|
||||
self._await_processing_revision = True
|
||||
self._on_processing_mode_changed(gui_state.processing.selected_mode)
|
||||
self._update_history_indicator()
|
||||
self._remember_active_profile_path(profile_path)
|
||||
|
||||
@@ -28,9 +28,6 @@ class AppWindowRadarLimitsMixin:
|
||||
else:
|
||||
radar_service = create_single_radar_service(config)
|
||||
|
||||
if isinstance(radar_service, LibreVnaService) and not radar_service.driver_available:
|
||||
raise RuntimeError("LibreVNA Python driver is not available for device limits query")
|
||||
|
||||
limits = radar_service.read_device_limits()
|
||||
return self._apply_radar_limits_to_ui(limits)
|
||||
|
||||
|
||||
@@ -81,52 +81,6 @@ class AppWindowConfigStateBuildersMixin:
|
||||
env[key] = value
|
||||
return env
|
||||
|
||||
@staticmethod
|
||||
def _parse_geometry_line_coordinates(parts: list[str]) -> tuple[float, float, float]:
|
||||
"""Parse 1/2/3 trailing coordinate fields into (x, y, z); missing axes default to 0."""
|
||||
x_m = float(parts[0])
|
||||
y_m = float(parts[1]) if len(parts) >= 2 else 0.0
|
||||
z_m = float(parts[2]) if len(parts) >= 3 else 0.0
|
||||
return x_m, y_m, z_m
|
||||
|
||||
@staticmethod
|
||||
def _parse_gpr_tx_geometry_text(text: str) -> list[GprTxGeometryModel]:
|
||||
"""Parse line-based Tx geometry editor text: `output_pos x_m [y_m] [z_m]`."""
|
||||
entries: list[GprTxGeometryModel] = []
|
||||
for line_number, raw_line in enumerate(text.splitlines(), start=1):
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 2 or len(parts) > 4:
|
||||
raise ValueError(
|
||||
f"Invalid Tx geometry line {line_number}: expected `output_pos x_m [y_m] [z_m]`"
|
||||
)
|
||||
x_m, y_m, z_m = AppWindowConfigStateBuildersMixin._parse_geometry_line_coordinates(parts[1:])
|
||||
entries.append(
|
||||
GprTxGeometryModel(output_pos=int(parts[0]), x_m=x_m, y_m=y_m, z_m=z_m)
|
||||
)
|
||||
return entries
|
||||
|
||||
@staticmethod
|
||||
def _parse_gpr_rx_geometry_text(text: str) -> list[GprRxGeometryModel]:
|
||||
"""Parse line-based Rx geometry editor text: `input_pos x_m [y_m] [z_m]`."""
|
||||
entries: list[GprRxGeometryModel] = []
|
||||
for line_number, raw_line in enumerate(text.splitlines(), start=1):
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 2 or len(parts) > 4:
|
||||
raise ValueError(
|
||||
f"Invalid Rx geometry line {line_number}: expected `input_pos x_m [y_m] [z_m]`"
|
||||
)
|
||||
x_m, y_m, z_m = AppWindowConfigStateBuildersMixin._parse_geometry_line_coordinates(parts[1:])
|
||||
entries.append(
|
||||
GprRxGeometryModel(input_pos=int(parts[0]), x_m=x_m, y_m=y_m, z_m=z_m)
|
||||
)
|
||||
return entries
|
||||
|
||||
@staticmethod
|
||||
def _format_combos_text_from_config(config: RunConfigModel) -> str:
|
||||
"""Render configured combos for UI text editor, keeping full matrix as empty."""
|
||||
@@ -192,18 +146,6 @@ class AppWindowConfigStateBuildersMixin:
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _save_history_limit_for_config(config: RunConfigModel) -> int:
|
||||
"""Return maxlen for GUI snapshot-save deques.
|
||||
|
||||
Independent of ring capacities — see :data:`GUI_SAVE_HISTORY_LIMIT`.
|
||||
The `config` argument is kept for symmetry with
|
||||
:meth:`_cpp_bscan_replay_window_for_config` and possible future per-profile
|
||||
overrides.
|
||||
"""
|
||||
del config
|
||||
return GUI_SAVE_HISTORY_LIMIT
|
||||
|
||||
def _default_gui_state_for_config(self, config: RunConfigModel) -> GuiStateModel:
|
||||
"""Build fallback GUI-only defaults for a stable run config."""
|
||||
default_combo = config.combos[0] if config.combos else ComboModel(input=0, output=0)
|
||||
@@ -217,74 +159,17 @@ class AppWindowConfigStateBuildersMixin:
|
||||
single_output=str(int(default_combo.output)),
|
||||
),
|
||||
processing=GuiProcessingStateModel(
|
||||
selected_mode="pass_through",
|
||||
pass_through=GuiPassThroughStateModel(
|
||||
show_magnitude=True,
|
||||
show_phase=True,
|
||||
unwrap_phase=False,
|
||||
combo_filter="",
|
||||
fixed_y_enabled=False,
|
||||
y_min_db=-100.0,
|
||||
y_max_db=0.0,
|
||||
),
|
||||
bscan=GuiBscanStateModel(
|
||||
axis="abs",
|
||||
cut_m=0.824,
|
||||
max_depth_m=1.0,
|
||||
gain=1.0,
|
||||
start_freq_mhz=100.0,
|
||||
stop_freq_mhz=8800.0,
|
||||
subtract_mean_ascan=False,
|
||||
),
|
||||
gpr=GuiGprStateModel(
|
||||
input_positions=self._default_gpr_input_positions_from_config(config),
|
||||
output_positions=self._default_gpr_output_positions_from_config(config),
|
||||
min_depth_m=2.0,
|
||||
max_depth_m=14.0,
|
||||
range_comp_power=0.1,
|
||||
object_min_frac=0.7,
|
||||
score_mode="combined",
|
||||
motion_mode="int_minus",
|
||||
look_angle_deg=0.0,
|
||||
direction_sign=1.0,
|
||||
speed_m_s=0.0,
|
||||
ignore_socket_speed_enabled=False,
|
||||
max_detected_objects_to_draw=5,
|
||||
draw_top_m_objects=2,
|
||||
object_approach_min_frames=3,
|
||||
start_freq_mhz=3000.0,
|
||||
stop_freq_mhz=6000.0,
|
||||
background_subtract_enabled=True,
|
||||
background_mean_count=10,
|
||||
remove_sidelobe_objects_enabled=True,
|
||||
imaging_plane_y_m=0.0,
|
||||
render_mode="heatmap",
|
||||
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,
|
||||
),
|
||||
legacy_gpr=GuiLegacyGprStateModel(
|
||||
input_positions=self._default_gpr_input_positions_from_config(config),
|
||||
output_positions=self._default_gpr_output_positions_from_config(config),
|
||||
min_depth_m=2.0,
|
||||
max_depth_m=14.0,
|
||||
comp_power=0.2,
|
||||
start_freq_mhz=3000.0,
|
||||
stop_freq_mhz=6000.0,
|
||||
speed_m_s=0.0,
|
||||
ignore_socket_speed_enabled=False,
|
||||
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,
|
||||
),
|
||||
sar3d=GuiSar3dStateModel(
|
||||
input_positions=self._default_gpr_input_positions_from_config(config),
|
||||
@@ -292,15 +177,7 @@ class AppWindowConfigStateBuildersMixin:
|
||||
),
|
||||
),
|
||||
data_actions=GuiDataActionsStateModel(
|
||||
save_count=10,
|
||||
save_path=str(self._project_root / "python_app/data/snapshots"),
|
||||
save_name="snapshot_manual",
|
||||
record_count=100,
|
||||
),
|
||||
preprocess_dialog=GuiPreprocessDialogStateModel(
|
||||
set_name="set_001",
|
||||
radar_config_dir="",
|
||||
use_all_radar_configs=False,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -442,7 +319,6 @@ class AppWindowConfigStateBuildersMixin:
|
||||
),
|
||||
),
|
||||
data_actions=GuiDataActionsStateModel(
|
||||
save_count=int(self._save_count.value()),
|
||||
save_path=self._save_path_input.text().strip(),
|
||||
save_name=self._save_name_input.text().strip(),
|
||||
record_count=int(self._record_count.value()),
|
||||
@@ -502,8 +378,8 @@ class AppWindowConfigStateBuildersMixin:
|
||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS:
|
||||
preprocess_asset_model(config, key).set_name = self._selected_preprocess_sets.get(key, "")
|
||||
config.gpr.relative_permittivity = float(self._gpr_relative_permittivity.value())
|
||||
config.gpr.tx_geometry = self._parse_gpr_tx_geometry_text(self._gpr_tx_geometry_input.toPlainText())
|
||||
config.gpr.rx_geometry = self._parse_gpr_rx_geometry_text(self._gpr_rx_geometry_input.toPlainText())
|
||||
config.gpr.tx_geometry = [GprTxGeometryModel(**entry) for entry in self._gpr_tx_geometry_input.records()]
|
||||
config.gpr.rx_geometry = [GprRxGeometryModel(**entry) for entry in self._gpr_rx_geometry_input.records()]
|
||||
validate_gpr_model(
|
||||
config.gpr,
|
||||
input_switch_positions=config.input_switch.positions,
|
||||
|
||||
Reference in New Issue
Block a user