added GPR
This commit is contained in:
@@ -4,7 +4,8 @@ from __future__ import annotations
|
||||
|
||||
from python_app.gui.runtime.history import remove_last_aligned_histories
|
||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||
from python_app.models.run_config_model import ComboModel, RunConfigModel
|
||||
from python_app.models.run_config_model import ComboModel, GprRxGeometryModel, GprTxGeometryModel, RunConfigModel
|
||||
from python_app.models.run_config_validation import validate_gpr_model
|
||||
from python_app.orchestration.config_writer import parse_combos_from_text
|
||||
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
|
||||
from python_app.storage.npz_store import radar_key_from_config
|
||||
@@ -13,6 +14,58 @@ from python_app.storage.npz_store import radar_key_from_config
|
||||
class AppWindowConfigMixin:
|
||||
"""Builds runtime config models from current UI state."""
|
||||
|
||||
@staticmethod
|
||||
def _parse_csv_int_list(text: str) -> list[int]:
|
||||
"""Parse comma-separated integer selection list."""
|
||||
cleaned = text.strip()
|
||||
if not cleaned:
|
||||
return []
|
||||
values: list[int] = []
|
||||
for part in cleaned.split(","):
|
||||
token = part.strip()
|
||||
if not token:
|
||||
continue
|
||||
values.append(int(token))
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def _parse_gpr_tx_geometry_text(text: str) -> list[GprTxGeometryModel]:
|
||||
"""Parse line-based Tx geometry editor text."""
|
||||
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:
|
||||
raise ValueError(f"Invalid Tx geometry line {line_number}: expected `output_pos x_m`")
|
||||
entries.append(
|
||||
GprTxGeometryModel(
|
||||
output_pos=int(parts[0]),
|
||||
x_m=float(parts[1]),
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
@staticmethod
|
||||
def _parse_gpr_rx_geometry_text(text: str) -> list[GprRxGeometryModel]:
|
||||
"""Parse line-based Rx geometry editor text."""
|
||||
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:
|
||||
raise ValueError(f"Invalid Rx geometry line {line_number}: expected `input_pos x_m`")
|
||||
entries.append(
|
||||
GprRxGeometryModel(
|
||||
input_pos=int(parts[0]),
|
||||
x_m=float(parts[1]),
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
def _save_current_config(self) -> None:
|
||||
"""Persist currently selected GUI settings into root run_config.json."""
|
||||
try:
|
||||
@@ -68,6 +121,15 @@ class AppWindowConfigMixin:
|
||||
|
||||
config.preprocess.calibration_set = self._selected_calibration_set
|
||||
config.preprocess.reference_set = self._selected_reference_set
|
||||
config.gpr.mode = self._gpr_config_mode.currentText()
|
||||
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())
|
||||
validate_gpr_model(
|
||||
config.gpr,
|
||||
input_switch_positions=config.input_switch.positions,
|
||||
output_switch_positions=config.output_switch.positions,
|
||||
)
|
||||
return config
|
||||
|
||||
def _radar_key(self, config: RunConfigModel) -> str:
|
||||
@@ -85,16 +147,31 @@ class AppWindowConfigMixin:
|
||||
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()
|
||||
self._sync_gpr_frequency_limits_with_radar()
|
||||
y_min_db = float(self._pass_through_y_min_db.value())
|
||||
y_max_db = float(self._pass_through_y_max_db.value())
|
||||
return ProcessingLiveConfig(
|
||||
processor_mode=self._processing_mode.currentText(),
|
||||
gain_db=float(self._processing_gain_db.value()),
|
||||
phase_deg=float(self._processing_phase_deg.value()),
|
||||
pass_through_fixed_y_enabled=bool(self._pass_through_fixed_y_enabled.isChecked()),
|
||||
pass_through_y_min_db=min(y_min_db, y_max_db),
|
||||
pass_through_y_max_db=max(y_min_db, y_max_db),
|
||||
bscan_axis=self._bscan_axis.currentText(),
|
||||
bscan_cut_m=float(self._bscan_cut_m.value()),
|
||||
bscan_max_depth_m=float(self._bscan_max_depth_m.value()),
|
||||
bscan_gain=float(self._bscan_gain.value()),
|
||||
bscan_start_freq_mhz=float(self._bscan_start_freq_mhz.value()),
|
||||
bscan_stop_freq_mhz=float(self._bscan_stop_freq_mhz.value()),
|
||||
gpr_input_positions=self._parse_csv_int_list(self._gpr_input_positions_input.text()),
|
||||
gpr_output_positions=self._parse_csv_int_list(self._gpr_output_positions_input.text()),
|
||||
gpr_min_depth_m=float(self._gpr_min_depth_m.value()),
|
||||
gpr_max_depth_m=float(self._gpr_max_depth_m.value()),
|
||||
gpr_comp_power=float(self._gpr_comp_power.value()),
|
||||
gpr_start_freq_mhz=float(self._gpr_start_freq_mhz.value()),
|
||||
gpr_stop_freq_mhz=float(self._gpr_stop_freq_mhz.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),
|
||||
history_command=str(history_command),
|
||||
)
|
||||
@@ -109,10 +186,18 @@ class AppWindowConfigMixin:
|
||||
"""Handle live-processing setting changes and trigger redraw when needed."""
|
||||
try:
|
||||
self._write_live_processing_config()
|
||||
if self._processing_mode.currentText() == "bscan":
|
||||
current_mode = self._processing_mode.currentText()
|
||||
if current_mode == "bscan":
|
||||
self._drain_results_until_quiet(timeout_s=0.25, poll_s=0.01)
|
||||
self._sync_bscan_history_from_results()
|
||||
self._draw_bscan_heatmap_from_history()
|
||||
elif current_mode == "gpr":
|
||||
self._drain_results_until_quiet(timeout_s=0.35, poll_s=0.01)
|
||||
if self._result_history:
|
||||
if not self._draw_results(self._result_history[-1]):
|
||||
self._clear_gpr_plot()
|
||||
else:
|
||||
self._clear_gpr_plot()
|
||||
elif self._result_history:
|
||||
self._draw_results(self._result_history[-1])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
@@ -123,6 +208,7 @@ class AppWindowConfigMixin:
|
||||
mode_to_page = {
|
||||
"pass_through": 0,
|
||||
"bscan": 1,
|
||||
"gpr": 2,
|
||||
}
|
||||
self._set_plot_mode(mode)
|
||||
self._processing_mode_pages.setCurrentIndex(mode_to_page.get(mode, 0))
|
||||
@@ -134,17 +220,31 @@ class AppWindowConfigMixin:
|
||||
|
||||
def _on_bscan_clear_history_clicked(self) -> None:
|
||||
"""Permanently clear all runtime histories, ring backlogs, and B-scan cache."""
|
||||
self._apply_bscan_history_deletion(remove_last_only=False)
|
||||
self._apply_history_mode_deletion(mode_label="B-scan", remove_last_only=False)
|
||||
|
||||
def _on_bscan_remove_last_sweep_clicked(self) -> None:
|
||||
"""Permanently delete the latest sweep from runtime histories and rings."""
|
||||
self._apply_bscan_history_deletion(remove_last_only=True)
|
||||
self._apply_history_mode_deletion(mode_label="B-scan", remove_last_only=True)
|
||||
|
||||
def _apply_bscan_history_deletion(self, *, remove_last_only: bool) -> None:
|
||||
"""Apply destructive B-scan history deletion via C++ processor history commands."""
|
||||
def _on_gpr_clear_history_clicked(self) -> None:
|
||||
"""Permanently clear all runtime histories, ring backlogs, and GPR cache."""
|
||||
self._apply_history_mode_deletion(mode_label="GPR", remove_last_only=False)
|
||||
|
||||
def _on_gpr_remove_last_measurement_clicked(self) -> None:
|
||||
"""Permanently delete the latest measurement from runtime histories and rings."""
|
||||
self._apply_history_mode_deletion(mode_label="GPR", remove_last_only=True)
|
||||
|
||||
def _clear_history_mode_caches(self) -> None:
|
||||
"""Drop mode-specific cached render state."""
|
||||
self._clear_bscan_plot_history()
|
||||
if hasattr(self, "_gpr_plot"):
|
||||
self._clear_gpr_plot()
|
||||
|
||||
def _apply_history_mode_deletion(self, *, mode_label: str, remove_last_only: bool) -> None:
|
||||
"""Apply destructive history deletion via C++ processor history commands."""
|
||||
resume_acquisition = self._supervisor.is_running()
|
||||
history_command = "remove_last" if remove_last_only else "clear_all"
|
||||
action = "last sweep removed" if remove_last_only else "history fully cleared"
|
||||
action = "last measurement removed" if remove_last_only else "history fully cleared"
|
||||
dropped_results = 0
|
||||
|
||||
try:
|
||||
@@ -169,7 +269,7 @@ class AppWindowConfigMixin:
|
||||
retained_pre=retained_pre,
|
||||
retained_result=retained_result,
|
||||
)
|
||||
self._clear_bscan_plot_history()
|
||||
self._clear_history_mode_caches()
|
||||
|
||||
self._write_live_processing_config(history_command=history_command, bump_history_seq=True)
|
||||
|
||||
@@ -180,9 +280,9 @@ class AppWindowConfigMixin:
|
||||
self._redraw_after_history_deletion()
|
||||
if resume_acquisition:
|
||||
self._start_run()
|
||||
self._log(f"B-scan {action}; dropped pending results={dropped_results}")
|
||||
self._log(f"{mode_label} {action}; dropped pending results={dropped_results}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._show_error(f"Failed to delete B-scan history: {exc}")
|
||||
self._show_error(f"Failed to delete {mode_label} history: {exc}")
|
||||
|
||||
def _redraw_after_history_deletion(self) -> None:
|
||||
"""Refresh plot immediately after destructive history deletion."""
|
||||
@@ -192,6 +292,11 @@ class AppWindowConfigMixin:
|
||||
if not self._draw_bscan_heatmap_from_history():
|
||||
self._bscan_plot.clear()
|
||||
return
|
||||
if self._processing_mode.currentText() == "gpr":
|
||||
if self._result_history and self._draw_results(self._result_history[-1]):
|
||||
return
|
||||
self._clear_gpr_plot()
|
||||
return
|
||||
if self._result_history:
|
||||
self._draw_results(self._result_history[-1])
|
||||
return
|
||||
@@ -208,8 +313,8 @@ class AppWindowConfigMixin:
|
||||
self._on_processing_live_settings_changed()
|
||||
|
||||
def _on_radar_sweep_limits_changed(self) -> None:
|
||||
"""Clamp B-scan frequency bounds after sweep start/stop edits."""
|
||||
if self._sync_bscan_frequency_limits_with_radar():
|
||||
"""Clamp processing frequency bounds after sweep start/stop edits."""
|
||||
if self._sync_processing_frequency_limits_with_radar():
|
||||
self._on_processing_live_settings_changed()
|
||||
|
||||
def _refresh_radar_limits_from_device(self) -> bool:
|
||||
@@ -301,7 +406,7 @@ class AppWindowConfigMixin:
|
||||
or prev_power != self._power_input.text().strip()
|
||||
)
|
||||
|
||||
self._sync_bscan_frequency_limits_with_radar()
|
||||
self._sync_processing_frequency_limits_with_radar()
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
@@ -326,16 +431,16 @@ class AppWindowConfigMixin:
|
||||
widget.setText(str(value))
|
||||
return value
|
||||
|
||||
def _sync_bscan_frequency_limits_with_radar(self) -> bool:
|
||||
"""Synchronize B-scan start/stop MHz widget ranges with radar sweep bounds."""
|
||||
required_widgets = (
|
||||
"_start_hz_input",
|
||||
"_stop_hz_input",
|
||||
"_bscan_start_freq_mhz",
|
||||
"_bscan_stop_freq_mhz",
|
||||
)
|
||||
def _sync_processing_frequency_limits_with_radar(self) -> bool:
|
||||
"""Synchronize all processing frequency widgets with radar sweep bounds."""
|
||||
bscan_changed = self._sync_bscan_frequency_limits_with_radar()
|
||||
gpr_changed = self._sync_gpr_frequency_limits_with_radar()
|
||||
return bscan_changed or gpr_changed
|
||||
|
||||
def _sync_frequency_spinboxes_with_radar(self, widget_names: tuple[str, ...]) -> bool:
|
||||
"""Synchronize one or more MHz spin boxes with current radar sweep bounds."""
|
||||
required_widgets = ("_start_hz_input", "_stop_hz_input", *widget_names)
|
||||
if not all(hasattr(self, widget_name) for widget_name in required_widgets):
|
||||
# Processing callbacks can fire while UI groups are still being built.
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -348,28 +453,41 @@ class AppWindowConfigMixin:
|
||||
radar_max_mhz = max(radar_start_hz, radar_stop_hz) / 1_000_000.0
|
||||
|
||||
changed = False
|
||||
for widget in (self._bscan_start_freq_mhz, self._bscan_stop_freq_mhz):
|
||||
widgets = [getattr(self, widget_name) for widget_name in widget_names]
|
||||
for widget in widgets:
|
||||
if widget.minimum() != radar_min_mhz or widget.maximum() != radar_max_mhz:
|
||||
changed = True
|
||||
widget.blockSignals(True)
|
||||
widget.setRange(radar_min_mhz, radar_max_mhz)
|
||||
widget.blockSignals(False)
|
||||
|
||||
clamped_start_mhz = min(max(self._bscan_start_freq_mhz.value(), radar_min_mhz), radar_max_mhz)
|
||||
clamped_stop_mhz = min(max(self._bscan_stop_freq_mhz.value(), radar_min_mhz), radar_max_mhz)
|
||||
if clamped_start_mhz != self._bscan_start_freq_mhz.value():
|
||||
changed = True
|
||||
self._bscan_start_freq_mhz.blockSignals(True)
|
||||
self._bscan_start_freq_mhz.setValue(clamped_start_mhz)
|
||||
self._bscan_start_freq_mhz.blockSignals(False)
|
||||
if clamped_stop_mhz != self._bscan_stop_freq_mhz.value():
|
||||
changed = True
|
||||
self._bscan_stop_freq_mhz.blockSignals(True)
|
||||
self._bscan_stop_freq_mhz.setValue(clamped_stop_mhz)
|
||||
self._bscan_stop_freq_mhz.blockSignals(False)
|
||||
|
||||
for widget in widgets:
|
||||
clamped_value = min(max(widget.value(), radar_min_mhz), radar_max_mhz)
|
||||
if clamped_value != widget.value():
|
||||
changed = True
|
||||
widget.blockSignals(True)
|
||||
widget.setValue(clamped_value)
|
||||
widget.blockSignals(False)
|
||||
return changed
|
||||
|
||||
def _sync_bscan_frequency_limits_with_radar(self) -> bool:
|
||||
"""Synchronize B-scan start/stop MHz widget ranges with radar sweep bounds."""
|
||||
return self._sync_frequency_spinboxes_with_radar(
|
||||
(
|
||||
"_bscan_start_freq_mhz",
|
||||
"_bscan_stop_freq_mhz",
|
||||
)
|
||||
)
|
||||
|
||||
def _sync_gpr_frequency_limits_with_radar(self) -> bool:
|
||||
"""Synchronize GPR start/stop MHz widget ranges with radar sweep bounds."""
|
||||
return self._sync_frequency_spinboxes_with_radar(
|
||||
(
|
||||
"_gpr_start_freq_mhz",
|
||||
"_gpr_stop_freq_mhz",
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _switches_are_effectively_static(config: RunConfigModel) -> bool:
|
||||
"""Return `True` when switch setup effectively yields one fixed combo."""
|
||||
|
||||
Reference in New Issue
Block a user