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."""
|
||||
|
||||
@@ -353,7 +353,7 @@ class AppWindowPipelineMixin:
|
||||
"""Reset runtime history and B-scan caches."""
|
||||
self._replace_runtime_history(retained_raw=[], retained_pre=[], retained_result=[])
|
||||
self._bscan_history_floor_collection_id = 0
|
||||
self._clear_bscan_plot_history()
|
||||
self._clear_history_mode_caches()
|
||||
self._update_history_indicator()
|
||||
|
||||
def _replace_runtime_history(
|
||||
@@ -385,4 +385,8 @@ class AppWindowPipelineMixin:
|
||||
|
||||
def _validate_processing_mode_constraints(self, config: RunConfigModel) -> None:
|
||||
"""Validate processing-mode constraints for run start."""
|
||||
validate_processing_mode_constraints(self._processing_mode.currentText(), config)
|
||||
validate_processing_mode_constraints(
|
||||
self._processing_mode.currentText(),
|
||||
config,
|
||||
self._live_processing_config(),
|
||||
)
|
||||
|
||||
@@ -36,6 +36,8 @@ class AppWindowPlotMixin:
|
||||
"""Draw collection based on currently selected processing mode."""
|
||||
if self._processing_mode.currentText() == "bscan":
|
||||
return self._draw_bscan_heatmap(collection)
|
||||
if self._processing_mode.currentText() == "gpr":
|
||||
return self._draw_gpr_map(collection)
|
||||
return self._draw_trace_lines(collection)
|
||||
|
||||
def _show_magnitude_curves(self) -> bool:
|
||||
@@ -46,9 +48,24 @@ class AppWindowPlotMixin:
|
||||
"""Return whether phase curves should be rendered."""
|
||||
return self._show_phase_checkbox.isChecked()
|
||||
|
||||
def _pass_through_fixed_y_range(self) -> tuple[bool, float, float]:
|
||||
"""Return normalized magnitude Y-range override for pass-through mode."""
|
||||
y_min = float(self._pass_through_y_min_db.value())
|
||||
y_max = float(self._pass_through_y_max_db.value())
|
||||
return bool(self._pass_through_fixed_y_enabled.isChecked()), min(y_min, y_max), max(y_min, y_max)
|
||||
|
||||
def _configure_pass_through_magnitude_axis(self, plot: pg.PlotWidget) -> None:
|
||||
"""Apply pass-through magnitude-axis autorange or fixed Y window."""
|
||||
fixed_y_enabled, y_min, y_max = self._pass_through_fixed_y_range()
|
||||
view_box = plot.getViewBox()
|
||||
view_box.invertY(False)
|
||||
view_box.enableAutoRange(x=True, y=not fixed_y_enabled)
|
||||
if fixed_y_enabled:
|
||||
plot.setYRange(y_min, y_max, padding=0.0)
|
||||
|
||||
def _on_trace_visibility_changed(self, *_args) -> None:
|
||||
"""Redraw pass-through traces when magnitude/phase toggles changed."""
|
||||
if self._processing_mode.currentText() == "bscan":
|
||||
if self._processing_mode.currentText() in {"bscan", "gpr"}:
|
||||
return
|
||||
if self._result_history:
|
||||
self._draw_results(self._result_history[-1])
|
||||
@@ -98,8 +115,7 @@ class AppWindowPlotMixin:
|
||||
|
||||
if show_magnitude:
|
||||
mag_item = magnitude_plot.getPlotItem()
|
||||
magnitude_plot.getViewBox().invertY(False)
|
||||
magnitude_plot.getViewBox().enableAutoRange(x=True, y=True)
|
||||
self._configure_pass_through_magnitude_axis(magnitude_plot)
|
||||
mag_item.showAxis("left", show=True)
|
||||
mag_item.showAxis("bottom", show=not show_phase)
|
||||
magnitude_plot.setLabel("left", "Magnitude", units="dB")
|
||||
@@ -219,6 +235,8 @@ class AppWindowPlotMixin:
|
||||
phase_plot.setXRange(x_min, x_max, padding=0.02)
|
||||
if show_phase:
|
||||
phase_plot.setYRange(-180.0, 180.0, padding=0.02)
|
||||
if show_magnitude:
|
||||
self._configure_pass_through_magnitude_axis(magnitude_plot)
|
||||
return has_data
|
||||
|
||||
@staticmethod
|
||||
@@ -481,8 +499,290 @@ class AppWindowPlotMixin:
|
||||
"""Hide right axis and clear phase overlay when phase is not rendered."""
|
||||
self._clear_phase_overlay()
|
||||
|
||||
def _clear_gpr_plot(self) -> None:
|
||||
"""Clear latest GPR plot surface."""
|
||||
if not hasattr(self, "_gpr_plot"):
|
||||
return
|
||||
self._clear_gpr_point_labels()
|
||||
self._clear_gpr_region_labels()
|
||||
self._clear_gpr_region_masks()
|
||||
if self._gpr_image_item is not None:
|
||||
self._gpr_image_item.hide()
|
||||
if self._gpr_tx_item is not None:
|
||||
self._gpr_tx_item.setData(x=[], y=[])
|
||||
self._gpr_tx_item.hide()
|
||||
if self._gpr_rx_item is not None:
|
||||
self._gpr_rx_item.setData(x=[], y=[])
|
||||
self._gpr_rx_item.hide()
|
||||
if self._gpr_points_item is not None:
|
||||
self._gpr_points_item.setData(x=[], y=[])
|
||||
self._gpr_points_item.hide()
|
||||
if self._gpr_region_centers_item is not None:
|
||||
self._gpr_region_centers_item.setData(x=[], y=[])
|
||||
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
|
||||
|
||||
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")
|
||||
|
||||
view_box = plot.getViewBox()
|
||||
view_box.invertY(True)
|
||||
view_box.enableAutoRange(x=False, y=False)
|
||||
|
||||
if self._gpr_lookup_table is None:
|
||||
self._gpr_lookup_table = self._build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"])
|
||||
|
||||
self._gpr_image_item = pg.ImageItem(axisOrder="row-major")
|
||||
self._gpr_image_item.setZValue(0)
|
||||
self._gpr_image_item.hide()
|
||||
plot.addItem(self._gpr_image_item)
|
||||
|
||||
self._gpr_tx_item = pg.ScatterPlotItem()
|
||||
self._gpr_tx_item.setZValue(20)
|
||||
self._gpr_tx_item.hide()
|
||||
plot.addItem(self._gpr_tx_item)
|
||||
|
||||
self._gpr_rx_item = pg.ScatterPlotItem()
|
||||
self._gpr_rx_item.setZValue(20)
|
||||
self._gpr_rx_item.hide()
|
||||
plot.addItem(self._gpr_rx_item)
|
||||
|
||||
self._gpr_points_item = pg.ScatterPlotItem()
|
||||
self._gpr_points_item.setZValue(30)
|
||||
self._gpr_points_item.hide()
|
||||
plot.addItem(self._gpr_points_item)
|
||||
|
||||
self._gpr_region_centers_item = pg.ScatterPlotItem()
|
||||
self._gpr_region_centers_item.setZValue(30)
|
||||
self._gpr_region_centers_item.hide()
|
||||
plot.addItem(self._gpr_region_centers_item)
|
||||
|
||||
def _clear_gpr_point_labels(self) -> None:
|
||||
"""Remove dynamic point-score labels from GPR plot."""
|
||||
for item in self._gpr_point_labels:
|
||||
try:
|
||||
self._gpr_plot.removeItem(item)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
self._gpr_point_labels.clear()
|
||||
|
||||
def _clear_gpr_region_labels(self) -> None:
|
||||
"""Remove dynamic region labels from GPR plot."""
|
||||
for item in self._gpr_region_center_labels:
|
||||
try:
|
||||
self._gpr_plot.removeItem(item)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
self._gpr_region_center_labels.clear()
|
||||
|
||||
def _clear_gpr_region_masks(self) -> None:
|
||||
"""Remove dynamic region contour carriers from GPR plot."""
|
||||
for item in self._gpr_region_mask_items:
|
||||
try:
|
||||
self._gpr_plot.removeItem(item)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
self._gpr_region_mask_items.clear()
|
||||
self._gpr_region_contours.clear()
|
||||
|
||||
@staticmethod
|
||||
def _collection_payload_by_name(collection: ResultCollection, name: str, kind: int | None = None):
|
||||
"""Return first collection payload matching name and optional kind."""
|
||||
for payload in collection.collection_payloads:
|
||||
if payload.processing_name != name:
|
||||
continue
|
||||
if kind is not None and int(payload.kind) != int(kind):
|
||||
continue
|
||||
return payload
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _collection_payloads_by_prefix(collection: ResultCollection, prefix: str, kind: int | None = None):
|
||||
"""Return collection payloads matching processing-name prefix."""
|
||||
payloads = []
|
||||
for payload in collection.collection_payloads:
|
||||
if not str(payload.processing_name).startswith(prefix):
|
||||
continue
|
||||
if kind is not None and int(payload.kind) != int(kind):
|
||||
continue
|
||||
payloads.append(payload)
|
||||
return payloads
|
||||
|
||||
def _selected_gpr_geometry(self) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Resolve selected Tx/Rx geometry arrays for current GPR selection."""
|
||||
requested_inputs = tuple(self._parse_csv_int_list(self._gpr_input_positions_input.text()))
|
||||
requested_outputs = tuple(self._parse_csv_int_list(self._gpr_output_positions_input.text()))
|
||||
signature = (
|
||||
self._gpr_tx_geometry_input.toPlainText(),
|
||||
self._gpr_rx_geometry_input.toPlainText(),
|
||||
requested_inputs,
|
||||
requested_outputs,
|
||||
)
|
||||
if signature == self._gpr_geometry_signature and self._gpr_selected_geometry is not None:
|
||||
return self._gpr_selected_geometry
|
||||
|
||||
tx_entries = self._parse_gpr_tx_geometry_text(signature[0])
|
||||
rx_entries = self._parse_gpr_rx_geometry_text(signature[1])
|
||||
requested_input_set = set(requested_inputs)
|
||||
requested_output_set = set(requested_outputs)
|
||||
|
||||
rx_entries = sorted(rx_entries, key=lambda entry: int(entry.input_pos))
|
||||
tx_entries = sorted(tx_entries, key=lambda entry: int(entry.output_pos))
|
||||
if requested_input_set:
|
||||
rx_entries = [entry for entry in rx_entries if int(entry.input_pos) in requested_input_set]
|
||||
if requested_output_set:
|
||||
tx_entries = [entry for entry in tx_entries if int(entry.output_pos) in requested_output_set]
|
||||
|
||||
x_tx = np.asarray([float(entry.x_m) for entry in tx_entries], dtype=np.float32)
|
||||
x_rx = np.asarray([float(entry.x_m) for entry in rx_entries], dtype=np.float32)
|
||||
self._gpr_geometry_signature = signature
|
||||
self._gpr_selected_geometry = (x_tx, x_rx)
|
||||
return self._gpr_selected_geometry
|
||||
|
||||
def _draw_gpr_map(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:
|
||||
self._clear_gpr_plot()
|
||||
return False
|
||||
|
||||
image = np.asarray(accumulator_payload.image, dtype=np.float32)
|
||||
x_axis = np.asarray(accumulator_payload.image_x_axis, dtype=np.float32)
|
||||
y_axis = np.asarray(accumulator_payload.image_y_axis, dtype=np.float32)
|
||||
if image.ndim != 2 or image.size == 0 or x_axis.size == 0 or y_axis.size == 0:
|
||||
self._clear_gpr_plot()
|
||||
return False
|
||||
|
||||
x_min = float(x_axis[0])
|
||||
x_max = float(x_axis[-1])
|
||||
y_min = float(y_axis[0])
|
||||
y_max = float(y_axis[-1])
|
||||
rect = QRectF(x_min, y_min, max(x_max - x_min, 1e-6), max(y_max - y_min, 1e-6))
|
||||
|
||||
plot = self._gpr_plot
|
||||
plot.setUpdatesEnabled(False)
|
||||
try:
|
||||
self._ensure_gpr_plot_items()
|
||||
self._clear_gpr_point_labels()
|
||||
self._clear_gpr_region_labels()
|
||||
self._clear_gpr_region_masks()
|
||||
|
||||
self._gpr_image_item.setImage(image, autoLevels=False)
|
||||
self._gpr_image_item.setRect(rect)
|
||||
self._gpr_image_item.setLookupTable(self._gpr_lookup_table)
|
||||
self._gpr_image_item.setLevels((float(np.min(image)), float(np.max(image) + 1e-6)))
|
||||
self._gpr_image_item.show()
|
||||
|
||||
plot.setXRange(x_min, x_max, padding=0.02)
|
||||
plot.setYRange(y_min, 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()
|
||||
|
||||
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:
|
||||
points = np.asarray(points_payload.table, dtype=np.float32)
|
||||
self._gpr_points_item.setData(
|
||||
x=points[:, 0],
|
||||
y=points[:, 1],
|
||||
symbol="d",
|
||||
size=11,
|
||||
brush=pg.mkBrush("#ffffff"),
|
||||
pen=pg.mkPen("#111111", width=1.1),
|
||||
)
|
||||
self._gpr_points_item.show()
|
||||
for x_value, y_value, score in points:
|
||||
label = pg.TextItem(text=f"{float(score):.0f}", color="#ffffff", anchor=(0.0, 1.0))
|
||||
label.setZValue(40)
|
||||
label.setPos(float(x_value), float(y_value))
|
||||
plot.addItem(label)
|
||||
self._gpr_point_labels.append(label)
|
||||
else:
|
||||
self._gpr_points_item.setData(x=[], y=[])
|
||||
self._gpr_points_item.hide()
|
||||
|
||||
region_centers_payload = self._collection_payload_by_name(collection, "gpr_region_centers", kind=4)
|
||||
if region_centers_payload is not None and np.asarray(region_centers_payload.table).size > 0:
|
||||
centers = np.asarray(region_centers_payload.table, dtype=np.float32)
|
||||
self._gpr_region_centers_item.setData(
|
||||
x=centers[:, 0],
|
||||
y=centers[:, 1],
|
||||
symbol="o",
|
||||
size=10,
|
||||
brush=pg.mkBrush("#80ed99"),
|
||||
pen=pg.mkPen("#081c15", width=1.1),
|
||||
)
|
||||
self._gpr_region_centers_item.show()
|
||||
for row in centers:
|
||||
label = pg.TextItem(text=f"{float(row[2]):.0f}", color="#d8f3dc", anchor=(0.0, 1.0))
|
||||
label.setZValue(40)
|
||||
label.setPos(float(row[0]), float(row[1]))
|
||||
plot.addItem(label)
|
||||
self._gpr_region_center_labels.append(label)
|
||||
else:
|
||||
self._gpr_region_centers_item.setData(x=[], y=[])
|
||||
self._gpr_region_centers_item.hide()
|
||||
|
||||
for payload in self._collection_payloads_by_prefix(collection, "gpr_region_mask_", kind=3):
|
||||
mask = np.asarray(payload.image, dtype=np.float32)
|
||||
if mask.ndim != 2 or mask.size == 0:
|
||||
continue
|
||||
mask_image = pg.ImageItem(axisOrder="row-major")
|
||||
mask_image.setZValue(5)
|
||||
mask_image.setImage(mask, autoLevels=False)
|
||||
mask_image.setRect(rect)
|
||||
mask_image.setOpacity(0.0)
|
||||
plot.addItem(mask_image)
|
||||
contour = pg.IsocurveItem(data=mask, level=0.5, pen=pg.mkPen("#4cc9f0", width=1.3))
|
||||
contour.setParentItem(mask_image)
|
||||
self._gpr_region_mask_items.append(mask_image)
|
||||
self._gpr_region_contours.append(contour)
|
||||
|
||||
plot.setTitle(f"GPR {self._gpr_config_mode.currentText()}")
|
||||
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:
|
||||
return True
|
||||
for block in collection.blocks:
|
||||
for payload in block.payloads:
|
||||
if payload.kind == 1 and payload.trace.size > 0:
|
||||
@@ -503,8 +803,7 @@ class AppWindowPlotMixin:
|
||||
return
|
||||
|
||||
if show_magnitude:
|
||||
magnitude_plot.getViewBox().invertY(False)
|
||||
magnitude_plot.getViewBox().enableAutoRange(x=True, y=True)
|
||||
self._configure_pass_through_magnitude_axis(magnitude_plot)
|
||||
magnitude_plot.getPlotItem().showAxis("bottom", show=not show_phase)
|
||||
magnitude_plot.setLabel("left", "Magnitude", units="dB")
|
||||
magnitude_plot.setTitle(title)
|
||||
@@ -548,5 +847,6 @@ class AppWindowPlotMixin:
|
||||
x_max = float(np.max(trace.frequency_hz))
|
||||
if show_magnitude:
|
||||
magnitude_plot.setXRange(x_min, x_max, padding=0.02)
|
||||
self._configure_pass_through_magnitude_axis(magnitude_plot)
|
||||
if show_phase:
|
||||
phase_plot.setXRange(x_min, x_max, padding=0.02)
|
||||
|
||||
@@ -111,7 +111,7 @@ class AppWindowSnapshotMixin:
|
||||
|
||||
self._replace_runtime_history(retained_raw=[], retained_pre=[], retained_result=[])
|
||||
self._bscan_history_floor_collection_id = 0
|
||||
self._clear_bscan_plot_history()
|
||||
self._clear_history_mode_caches()
|
||||
|
||||
# Clear processor-side replay cache so newly rendered B-scan starts clean.
|
||||
self._write_live_processing_config(history_command="clear_all", bump_history_seq=True)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Layout is intentionally split into two independent plot surfaces:
|
||||
- single `PlotWidget` for B-scan heatmap rendering;
|
||||
- single `PlotWidget` for GPR accumulator/annotation rendering;
|
||||
- stacked magnitude/phase `PlotWidget`s for pass-through traces.
|
||||
|
||||
`_set_plot_mode()` switches between these surfaces via `QStackedWidget`.
|
||||
@@ -26,6 +27,7 @@ import pyqtgraph as pg
|
||||
|
||||
from python_app.gui.controllers.sections import (
|
||||
build_data_actions_group,
|
||||
build_gpr_config_group,
|
||||
build_hardware_actions_group,
|
||||
build_pipeline_group,
|
||||
build_preprocess_summary_group,
|
||||
@@ -72,6 +74,7 @@ class AppWindowUiMixin:
|
||||
# We create both upfront and only switch active page at runtime.
|
||||
self._plot_stack = QStackedWidget(root)
|
||||
self._build_bscan_plot_page()
|
||||
self._build_gpr_plot_page()
|
||||
self._build_trace_plot_page()
|
||||
|
||||
# Default view on startup is pass-through traces.
|
||||
@@ -121,6 +124,12 @@ class AppWindowUiMixin:
|
||||
|
||||
self._plot_stack.addWidget(self._trace_plots_container)
|
||||
|
||||
def _build_gpr_plot_page(self) -> None:
|
||||
"""Create GPR page in plot stack."""
|
||||
self._gpr_plot = pg.PlotWidget(background="#0f141c")
|
||||
self._gpr_plot.showGrid(x=True, y=True, alpha=0.2)
|
||||
self._plot_stack.addWidget(self._gpr_plot)
|
||||
|
||||
def _build_settings_toggle(self, root_layout: QHBoxLayout) -> None:
|
||||
"""Create narrow button used to collapse or show settings panel."""
|
||||
self._settings_toggle_button = QPushButton("<")
|
||||
@@ -182,6 +191,7 @@ class AppWindowUiMixin:
|
||||
build_data_actions_group(self),
|
||||
build_preprocess_summary_group(self),
|
||||
build_processing_group(self),
|
||||
build_gpr_config_group(self),
|
||||
build_radar_group(self),
|
||||
build_switch_group(self),
|
||||
]
|
||||
@@ -205,12 +215,16 @@ class AppWindowUiMixin:
|
||||
def _set_plot_mode(self, mode: str) -> None:
|
||||
"""Switch visible plot page according to processing mode.
|
||||
|
||||
`bscan` -> show `self._bscan_plot` (single heatmap surface)
|
||||
otherwise -> show `self._trace_plots_container` (magnitude + phase)
|
||||
`bscan` -> show `self._bscan_plot`
|
||||
`gpr` -> show `self._gpr_plot`
|
||||
otherwise -> show `self._trace_plots_container`
|
||||
"""
|
||||
if mode == "bscan":
|
||||
self._plot_stack.setCurrentWidget(self._bscan_plot)
|
||||
return
|
||||
if mode == "gpr":
|
||||
self._plot_stack.setCurrentWidget(self._gpr_plot)
|
||||
return
|
||||
self._plot_stack.setCurrentWidget(self._trace_plots_container)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Composable UI section builders used by AppWindow UI mixin."""
|
||||
|
||||
from python_app.gui.controllers.sections.data_actions_section import build_data_actions_group
|
||||
from python_app.gui.controllers.sections.gpr_config_section import build_gpr_config_group
|
||||
from python_app.gui.controllers.sections.hardware_actions_section import build_hardware_actions_group
|
||||
from python_app.gui.controllers.sections.pipeline_section import build_pipeline_group
|
||||
from python_app.gui.controllers.sections.preprocess_summary_section import build_preprocess_summary_group
|
||||
@@ -10,6 +11,7 @@ from python_app.gui.controllers.sections.switch_section import build_switch_grou
|
||||
|
||||
__all__ = [
|
||||
"build_data_actions_group",
|
||||
"build_gpr_config_group",
|
||||
"build_hardware_actions_group",
|
||||
"build_pipeline_group",
|
||||
"build_preprocess_summary_group",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Builder for stable GPR configuration section."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt6.QtWidgets import QComboBox, QDoubleSpinBox, QFormLayout, QGroupBox, QPlainTextEdit
|
||||
|
||||
|
||||
def _format_tx_geometry(owner) -> str:
|
||||
"""Render Tx geometry defaults into editable line-based text."""
|
||||
return "\n".join(
|
||||
f"{int(entry.output_pos)} {float(entry.x_m):g}"
|
||||
for entry in owner._defaults_config.gpr.tx_geometry
|
||||
)
|
||||
|
||||
|
||||
def _format_rx_geometry(owner) -> str:
|
||||
"""Render Rx geometry defaults into editable line-based text."""
|
||||
return "\n".join(
|
||||
f"{int(entry.input_pos)} {float(entry.x_m):g}"
|
||||
for entry in owner._defaults_config.gpr.rx_geometry
|
||||
)
|
||||
|
||||
|
||||
def build_gpr_config_group(owner) -> QGroupBox:
|
||||
"""Create stable GPR config controls backed by run_config.json."""
|
||||
group = QGroupBox("GPR Config")
|
||||
form = QFormLayout(group)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
defaults = owner._defaults_config.gpr
|
||||
|
||||
owner._gpr_config_mode = QComboBox()
|
||||
owner._gpr_config_mode.addItems(["point", "extended"])
|
||||
owner._set_combo_current_text(owner._gpr_config_mode, defaults.mode)
|
||||
|
||||
owner._gpr_relative_permittivity = QDoubleSpinBox()
|
||||
owner._gpr_relative_permittivity.setDecimals(4)
|
||||
owner._gpr_relative_permittivity.setRange(0.0001, 1000.0)
|
||||
owner._gpr_relative_permittivity.setSingleStep(0.05)
|
||||
owner._gpr_relative_permittivity.setValue(float(defaults.relative_permittivity))
|
||||
|
||||
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_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)
|
||||
|
||||
form.addRow("Mode", owner._gpr_config_mode)
|
||||
form.addRow("Relative Permittivity", owner._gpr_relative_permittivity)
|
||||
form.addRow("Tx Geometry", owner._gpr_tx_geometry_input)
|
||||
form.addRow("Rx Geometry", owner._gpr_rx_geometry_input)
|
||||
return group
|
||||
@@ -9,21 +9,39 @@ from PyQt6.QtWidgets import (
|
||||
QFormLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QStackedWidget,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
def _default_gpr_input_positions(owner) -> str:
|
||||
"""Build default live input-position selection from stable GPR config."""
|
||||
geometry_values = {int(entry.input_pos) for entry in owner._defaults_config.gpr.rx_geometry}
|
||||
combo_values = {int(combo.input) for combo in owner._defaults_config.combos}
|
||||
values = sorted(geometry_values & combo_values) or sorted(geometry_values)
|
||||
return ",".join(str(value) for value in values)
|
||||
|
||||
|
||||
def _default_gpr_output_positions(owner) -> str:
|
||||
"""Build default live output-position selection from stable GPR config."""
|
||||
geometry_values = {int(entry.output_pos) for entry in owner._defaults_config.gpr.tx_geometry}
|
||||
combo_values = {int(combo.output) for combo in owner._defaults_config.combos}
|
||||
values = sorted(geometry_values & combo_values) or sorted(geometry_values)
|
||||
return ",".join(str(value) for value in values)
|
||||
|
||||
|
||||
def build_processing_group(owner) -> QGroupBox:
|
||||
"""Create processing mode section with pass-through and B-scan pages."""
|
||||
"""Create processing mode section with pass-through, B-scan, and GPR pages."""
|
||||
group = QGroupBox("Processing")
|
||||
form = QFormLayout(group)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
|
||||
owner._processing_mode = QComboBox()
|
||||
owner._processing_mode.addItems(["pass_through", "bscan"])
|
||||
owner._processing_mode.addItems(["pass_through", "bscan", "gpr"])
|
||||
|
||||
owner._processing_mode_pages = QStackedWidget(group)
|
||||
owner._processing_mode_pages.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
@@ -51,10 +69,35 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._show_phase_checkbox = QCheckBox("Show phase")
|
||||
owner._show_phase_checkbox.setChecked(True)
|
||||
|
||||
owner._pass_through_fixed_y_enabled = QCheckBox("Fix magnitude Y range")
|
||||
owner._pass_through_fixed_y_enabled.setChecked(False)
|
||||
|
||||
owner._pass_through_y_min_db = QDoubleSpinBox()
|
||||
owner._pass_through_y_min_db.setDecimals(1)
|
||||
owner._pass_through_y_min_db.setRange(-240.0, 240.0)
|
||||
owner._pass_through_y_min_db.setSingleStep(1.0)
|
||||
owner._pass_through_y_min_db.setValue(-100.0)
|
||||
|
||||
owner._pass_through_y_max_db = QDoubleSpinBox()
|
||||
owner._pass_through_y_max_db.setDecimals(1)
|
||||
owner._pass_through_y_max_db.setRange(-240.0, 240.0)
|
||||
owner._pass_through_y_max_db.setSingleStep(1.0)
|
||||
owner._pass_through_y_max_db.setValue(0.0)
|
||||
|
||||
def sync_pass_through_y_controls() -> None:
|
||||
enabled = owner._pass_through_fixed_y_enabled.isChecked()
|
||||
owner._pass_through_y_min_db.setEnabled(enabled)
|
||||
owner._pass_through_y_max_db.setEnabled(enabled)
|
||||
|
||||
sync_pass_through_y_controls()
|
||||
|
||||
pass_through_form.addRow("Gain dB (live)", owner._processing_gain_db)
|
||||
pass_through_form.addRow("Phase deg (live)", owner._processing_phase_deg)
|
||||
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)
|
||||
owner._processing_mode_pages.addWidget(pass_through_page)
|
||||
|
||||
bscan_page = QWidget(owner._processing_mode_pages)
|
||||
@@ -115,11 +158,86 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
bscan_form.addRow(bscan_actions)
|
||||
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)
|
||||
|
||||
owner._gpr_input_positions_input = QLineEdit(_default_gpr_input_positions(owner))
|
||||
owner._gpr_input_positions_input.setPlaceholderText("0,1,2")
|
||||
|
||||
owner._gpr_output_positions_input = QLineEdit(_default_gpr_output_positions(owner))
|
||||
owner._gpr_output_positions_input.setPlaceholderText("0,1")
|
||||
|
||||
owner._gpr_min_depth_m = QDoubleSpinBox()
|
||||
owner._gpr_min_depth_m.setDecimals(2)
|
||||
owner._gpr_min_depth_m.setRange(0.0, 50.0)
|
||||
owner._gpr_min_depth_m.setSingleStep(0.1)
|
||||
owner._gpr_min_depth_m.setValue(2.0)
|
||||
|
||||
owner._gpr_max_depth_m = QDoubleSpinBox()
|
||||
owner._gpr_max_depth_m.setDecimals(2)
|
||||
owner._gpr_max_depth_m.setRange(0.1, 50.0)
|
||||
owner._gpr_max_depth_m.setSingleStep(0.1)
|
||||
owner._gpr_max_depth_m.setValue(14.0)
|
||||
|
||||
owner._gpr_comp_power = QDoubleSpinBox()
|
||||
owner._gpr_comp_power.setDecimals(3)
|
||||
owner._gpr_comp_power.setRange(0.0, 5.0)
|
||||
owner._gpr_comp_power.setSingleStep(0.05)
|
||||
owner._gpr_comp_power.setValue(0.2)
|
||||
|
||||
owner._gpr_start_freq_mhz = QDoubleSpinBox()
|
||||
owner._gpr_start_freq_mhz.setDecimals(1)
|
||||
owner._gpr_start_freq_mhz.setRange(100.0, 8800.0)
|
||||
owner._gpr_start_freq_mhz.setSingleStep(10.0)
|
||||
owner._gpr_start_freq_mhz.setValue(3000.0)
|
||||
|
||||
owner._gpr_stop_freq_mhz = QDoubleSpinBox()
|
||||
owner._gpr_stop_freq_mhz.setDecimals(1)
|
||||
owner._gpr_stop_freq_mhz.setRange(100.0, 8800.0)
|
||||
owner._gpr_stop_freq_mhz.setSingleStep(10.0)
|
||||
owner._gpr_stop_freq_mhz.setValue(6000.0)
|
||||
|
||||
owner._gpr_background_subtract_enabled = QCheckBox("Subtract mean of previous collections")
|
||||
owner._gpr_background_subtract_enabled.setChecked(True)
|
||||
|
||||
owner._gpr_background_mean_count = QSpinBox()
|
||||
owner._gpr_background_mean_count.setRange(0, 10_000)
|
||||
owner._gpr_background_mean_count.setValue(10)
|
||||
|
||||
owner._gpr_clear_history_button = QPushButton("Clear GPR History")
|
||||
owner._gpr_clear_history_button.clicked.connect(owner._on_gpr_clear_history_clicked)
|
||||
owner._gpr_remove_last_button = QPushButton("Remove Last Measurement")
|
||||
owner._gpr_remove_last_button.clicked.connect(owner._on_gpr_remove_last_measurement_clicked)
|
||||
gpr_actions = QWidget(owner._processing_mode_pages)
|
||||
gpr_actions_layout = QHBoxLayout(gpr_actions)
|
||||
gpr_actions_layout.setContentsMargins(0, 0, 0, 0)
|
||||
gpr_actions_layout.setSpacing(8)
|
||||
gpr_actions_layout.addWidget(owner._gpr_remove_last_button)
|
||||
gpr_actions_layout.addWidget(owner._gpr_clear_history_button)
|
||||
|
||||
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(owner._gpr_background_subtract_enabled)
|
||||
gpr_form.addRow("Mean count", owner._gpr_background_mean_count)
|
||||
gpr_form.addRow(gpr_actions)
|
||||
owner._processing_mode_pages.addWidget(gpr_page)
|
||||
|
||||
owner._processing_mode.currentTextChanged.connect(owner._on_processing_mode_changed)
|
||||
owner._processing_gain_db.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._processing_phase_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._show_magnitude_checkbox.toggled.connect(owner._on_trace_visibility_changed)
|
||||
owner._show_phase_checkbox.toggled.connect(owner._on_trace_visibility_changed)
|
||||
owner._pass_through_fixed_y_enabled.toggled.connect(sync_pass_through_y_controls)
|
||||
owner._pass_through_fixed_y_enabled.toggled.connect(owner._on_processing_live_settings_changed)
|
||||
owner._pass_through_y_min_db.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._pass_through_y_max_db.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
form.addRow("Mode", owner._processing_mode)
|
||||
form.addRow(owner._processing_mode_pages)
|
||||
|
||||
@@ -129,6 +247,15 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._bscan_gain.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._bscan_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._bscan_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_input_positions_input.editingFinished.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_output_positions_input.editingFinished.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_min_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_max_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_comp_power.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_stop_freq_mhz.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._on_processing_mode_changed(owner._processing_mode.currentText())
|
||||
return group
|
||||
|
||||
Reference in New Issue
Block a user