added white theme, some features to processing and saving

This commit is contained in:
Ayzen
2026-04-13 10:44:01 +03:00
parent 61685d7a4d
commit 5d2f95f959
20 changed files with 295 additions and 48 deletions
@@ -94,10 +94,23 @@ struct S11PreprocessConfig {
PreprocessAssetConfig reference{}; PreprocessAssetConfig reference{};
}; };
struct PreprocessNotchBandConfig {
float low_hz = 0.0F;
float high_hz = 0.0F;
};
struct PreprocessNotchConfig {
bool enabled = false;
std::vector<PreprocessNotchBandConfig> bands_hz{};
float taper_width_hz = 40'000'000.0F;
std::string taper_type = "cosine";
};
struct PreprocessConfig { struct PreprocessConfig {
// Channel-specific preprocessing assets selected by Python GUI layer. // Channel-specific preprocessing assets selected by Python GUI layer.
S21PreprocessConfig s21{}; S21PreprocessConfig s21{};
S11PreprocessConfig s11{}; S11PreprocessConfig s11{};
PreprocessNotchConfig notch{};
}; };
struct GprTxGeometry { struct GprTxGeometry {
@@ -154,6 +154,46 @@ using Json = nlohmann::json;
return asset; return asset;
} }
[[nodiscard]] auto parse_preprocess_notch(
const Json& object,
const std::string& context
) -> PreprocessNotchConfig {
PreprocessNotchConfig notch{};
notch.enabled = optional_bool(object, "enabled", false);
notch.taper_width_hz = optional_f32(object, "taper_width_hz", 40'000'000.0F);
notch.taper_type = optional_string(object, "taper_type", "cosine");
if (const auto* bands_value = optional_field(object, "bands_hz"); bands_value != nullptr) {
const auto* bands_array = as_array(*bands_value, context + ".bands_hz");
notch.bands_hz.reserve(bands_array->size());
for (const auto& band_value : *bands_array) {
const auto* band_array = as_array(band_value, context + ".bands_hz[]");
if (band_array->size() != 2U) {
throw std::runtime_error(context + ".bands_hz[] must contain exactly two numbers");
}
PreprocessNotchBandConfig band{};
band.low_hz = static_cast<float>(as_number((*band_array)[0], context + ".bands_hz[][0]"));
band.high_hz = static_cast<float>(as_number((*band_array)[1], context + ".bands_hz[][1]"));
notch.bands_hz.push_back(band);
}
}
if (notch.taper_width_hz < 0.0F) {
throw std::runtime_error(context + ".taper_width_hz must be >= 0");
}
if (notch.taper_type != "cosine" && notch.taper_type != "hard") {
throw std::runtime_error(context + ".taper_type must be 'cosine' or 'hard'");
}
for (const auto& band : notch.bands_hz) {
if (band.high_hz < band.low_hz) {
throw std::runtime_error(context + ".bands_hz[] high_hz must be >= low_hz");
}
}
return notch;
}
[[nodiscard]] auto parse_driver_mode(const std::string& value) -> DriverMode { [[nodiscard]] auto parse_driver_mode(const std::string& value) -> DriverMode {
if (value == "mock") { if (value == "mock") {
return DriverMode::Mock; return DriverMode::Mock;
@@ -466,6 +506,10 @@ auto load_run_config(const std::string& path) -> RunConfig {
config.preprocess.s11.calibration.load = config.preprocess.s11.calibration.load =
parse_preprocess_asset(*s11_calibration_obj, "load", "preprocess.s11.calibration"); parse_preprocess_asset(*s11_calibration_obj, "load", "preprocess.s11.calibration");
config.preprocess.s11.reference = parse_preprocess_asset(*s11_obj, "reference", "preprocess.s11"); config.preprocess.s11.reference = parse_preprocess_asset(*s11_obj, "reference", "preprocess.s11");
if (const auto* notch_value = optional_field(*preprocess_obj, "notch"); notch_value != nullptr) {
config.preprocess.notch = parse_preprocess_notch(*as_object(*notch_value, "preprocess.notch"), "preprocess.notch");
}
} }
if (const auto* gpr_value = optional_field(*root_obj, "gpr"); gpr_value != nullptr) { if (const auto* gpr_value = optional_field(*root_obj, "gpr"); gpr_value != nullptr) {
@@ -1,11 +1,94 @@
#include "data_preprocessor.hpp" #include "data_preprocessor.hpp"
#include <algorithm>
#include <chrono> #include <chrono>
#include <cmath>
#include <span>
#include <stdexcept> #include <stdexcept>
#include <string_view>
#include <thread> #include <thread>
#include <vector> #include <vector>
namespace radar::preprocessing { namespace radar::preprocessing {
namespace {
constexpr double kPi = 3.14159265358979323846;
[[nodiscard]] auto should_apply_notch(const config::PreprocessNotchConfig& notch) -> bool {
return notch.enabled && !notch.bands_hz.empty();
}
[[nodiscard]] auto build_notch_mask(
std::span<const float> frequency_hz,
const config::PreprocessNotchConfig& notch
) -> std::vector<float> {
std::vector<float> mask(frequency_hz.size(), 1.0F);
if (!should_apply_notch(notch)) {
return mask;
}
const bool hard_taper = notch.taper_type == "hard";
const double taper_width_hz = std::max(0.0, static_cast<double>(notch.taper_width_hz));
for (const auto& band : notch.bands_hz) {
const double low_hz = static_cast<double>(band.low_hz);
const double high_hz = static_cast<double>(band.high_hz);
const double trans_low_hz = low_hz - taper_width_hz;
const double trans_high_hz = high_hz + taper_width_hz;
for (std::size_t index = 0U; index < frequency_hz.size(); ++index) {
const double frequency = static_cast<double>(frequency_hz[index]);
if (frequency >= low_hz && frequency <= high_hz) {
mask[index] = 0.0F;
continue;
}
if (hard_taper || taper_width_hz <= 0.0) {
if (frequency >= trans_low_hz && frequency <= trans_high_hz) {
mask[index] = 0.0F;
}
continue;
}
if (frequency >= trans_low_hz && frequency < low_hz) {
const double arg = kPi * (frequency - trans_low_hz) / taper_width_hz;
mask[index] *= static_cast<float>(0.5 * (1.0 - std::cos(arg)));
continue;
}
if (frequency > high_hz && frequency <= trans_high_hz) {
const double arg = kPi * (frequency - high_hz) / taper_width_hz;
mask[index] *= static_cast<float>(0.5 * (1.0 + std::cos(arg)));
}
}
}
return mask;
}
void apply_notch_mask(std::vector<ipc::Complex32>& samples, std::span<const float> mask) {
const std::size_t point_count = std::min(samples.size(), mask.size());
for (std::size_t index = 0U; index < point_count; ++index) {
samples[index].re *= mask[index];
samples[index].im *= mask[index];
}
}
[[nodiscard]] auto apply_notch_filter(
const ipc::SweepTraceBlock& trace,
const config::PreprocessNotchConfig& notch
) -> ipc::SweepTraceBlock {
if (!should_apply_notch(notch)) {
return trace;
}
ipc::SweepTraceBlock filtered = trace;
const auto mask = build_notch_mask(filtered.frequency_hz, notch);
apply_notch_mask(filtered.s21, mask);
apply_notch_mask(filtered.s11, mask);
return filtered;
}
} // namespace
DataPreprocessor::DataPreprocessor( DataPreprocessor::DataPreprocessor(
const config::RunConfig& config, const config::RunConfig& config,
@@ -78,7 +161,7 @@ auto DataPreprocessor::preprocess_collection(const ipc::RawSweepCollection& raw_
// Pipeline order is fixed: calibration first, then reference subtraction. // Pipeline order is fixed: calibration first, then reference subtraction.
const auto calibrated = calibration_master_.apply_to_trace(raw_trace); const auto calibrated = calibration_master_.apply_to_trace(raw_trace);
const auto referenced = reference_master_.apply_to_trace(calibrated); const auto referenced = reference_master_.apply_to_trace(calibrated);
preprocessed.traces.push_back(referenced); preprocessed.traces.push_back(apply_notch_filter(referenced, config_.preprocess.notch));
} }
return preprocessed; return preprocessed;
@@ -168,7 +168,8 @@ class AppWindowLiveProcessingMixin:
f"cut={self._bscan_cut_m.value():g} m, " f"cut={self._bscan_cut_m.value():g} m, "
f"max_depth={self._bscan_max_depth_m.value():g} m, " f"max_depth={self._bscan_max_depth_m.value():g} m, "
f"gain={self._bscan_gain.value():g}, " f"gain={self._bscan_gain.value():g}, "
f"freq={self._bscan_start_freq_mhz.value():g}..{self._bscan_stop_freq_mhz.value():g} MHz)" f"freq={self._bscan_start_freq_mhz.value():g}..{self._bscan_stop_freq_mhz.value():g} MHz, "
f"subtract_mean_ascan={self._bscan_subtract_mean_ascan.isChecked()})"
) )
elif mode == "gpr": elif mode == "gpr":
self._log( self._log(
@@ -169,6 +169,7 @@ class AppWindowConfigProfileIOMixin:
self._bscan_gain, self._bscan_gain,
self._bscan_start_freq_mhz, self._bscan_start_freq_mhz,
self._bscan_stop_freq_mhz, self._bscan_stop_freq_mhz,
self._bscan_subtract_mean_ascan,
self._gpr_config_mode, self._gpr_config_mode,
self._gpr_relative_permittivity, self._gpr_relative_permittivity,
self._gpr_tx_geometry_input, self._gpr_tx_geometry_input,
@@ -228,6 +229,7 @@ class AppWindowConfigProfileIOMixin:
self._bscan_gain.setValue(float(gui_state.processing.bscan.gain)) self._bscan_gain.setValue(float(gui_state.processing.bscan.gain))
self._bscan_start_freq_mhz.setValue(float(gui_state.processing.bscan.start_freq_mhz)) self._bscan_start_freq_mhz.setValue(float(gui_state.processing.bscan.start_freq_mhz))
self._bscan_stop_freq_mhz.setValue(float(gui_state.processing.bscan.stop_freq_mhz)) self._bscan_stop_freq_mhz.setValue(float(gui_state.processing.bscan.stop_freq_mhz))
self._bscan_subtract_mean_ascan.setChecked(bool(gui_state.processing.bscan.subtract_mean_ascan))
self._set_combo_current_text(self._gpr_config_mode, str(config.gpr.mode)) self._set_combo_current_text(self._gpr_config_mode, str(config.gpr.mode))
self._gpr_relative_permittivity.setValue(float(config.gpr.relative_permittivity)) self._gpr_relative_permittivity.setValue(float(config.gpr.relative_permittivity))
@@ -157,6 +157,7 @@ class AppWindowConfigStateBuildersMixin:
gain=1.0, gain=1.0,
start_freq_mhz=100.0, start_freq_mhz=100.0,
stop_freq_mhz=8800.0, stop_freq_mhz=8800.0,
subtract_mean_ascan=False,
), ),
gpr=GuiGprStateModel( gpr=GuiGprStateModel(
input_positions=self._default_gpr_input_positions_from_config(config), input_positions=self._default_gpr_input_positions_from_config(config),
@@ -235,6 +236,7 @@ class AppWindowConfigStateBuildersMixin:
gain=float(self._bscan_gain.value()), gain=float(self._bscan_gain.value()),
start_freq_mhz=float(self._bscan_start_freq_mhz.value()), start_freq_mhz=float(self._bscan_start_freq_mhz.value()),
stop_freq_mhz=float(self._bscan_stop_freq_mhz.value()), stop_freq_mhz=float(self._bscan_stop_freq_mhz.value()),
subtract_mean_ascan=bool(self._bscan_subtract_mean_ascan.isChecked()),
), ),
gpr=GuiGprStateModel( gpr=GuiGprStateModel(
input_positions=self._gpr_input_positions_input.text().strip(), input_positions=self._gpr_input_positions_input.text().strip(),
@@ -39,6 +39,8 @@ def _result_tail(
def build_bscan_signature( def build_bscan_signature(
live_config: ProcessingLiveConfig, live_config: ProcessingLiveConfig,
*,
subtract_mean_ascan_enabled: bool,
result_history: list[ResultCollection], result_history: list[ResultCollection],
history_limit: int, history_limit: int,
floor_collection_id: int, floor_collection_id: int,
@@ -57,11 +59,25 @@ def build_bscan_signature(
float(live_config.bscan_gain), float(live_config.bscan_gain),
float(live_config.bscan_start_freq_mhz), float(live_config.bscan_start_freq_mhz),
float(live_config.bscan_stop_freq_mhz), float(live_config.bscan_stop_freq_mhz),
bool(subtract_mean_ascan_enabled),
int(floor_collection_id), int(floor_collection_id),
tuple((int(collection.collection_id), int(collection.monotonic_ns), len(collection.blocks)) for collection in result_tail), tuple((int(collection.collection_id), int(collection.monotonic_ns), len(collection.blocks)) for collection in result_tail),
) )
def apply_mean_ascan_subtraction(
history: deque[np.ndarray],
*,
enabled: bool,
) -> np.ndarray:
"""Subtract mean A-scan across sweeps when enabled."""
sweeps = np.vstack(history).astype(np.float32, copy=False)
if not enabled:
return sweeps
mean_trace = np.mean(sweeps, axis=0, dtype=np.float32)
return sweeps - mean_trace
def rebuild_bscan_history_from_results( def rebuild_bscan_history_from_results(
result_history: list[ResultCollection], result_history: list[ResultCollection],
history_limit: int, history_limit: int,
@@ -183,7 +199,10 @@ class AppWindowBscanPlotMixin:
if not history or depth_axis is None: if not history or depth_axis is None:
return False return False
sweeps = np.vstack(history).astype(np.float32, copy=False) sweeps = apply_mean_ascan_subtraction(
history,
enabled=bool(self._bscan_subtract_mean_ascan.isChecked()),
)
if sweeps.size == 0: if sweeps.size == 0:
return False return False
@@ -229,6 +248,7 @@ class AppWindowBscanPlotMixin:
result_history = list(self._result_history) result_history = list(self._result_history)
return build_bscan_signature( return build_bscan_signature(
live_config=live_config, live_config=live_config,
subtract_mean_ascan_enabled=bool(self._bscan_subtract_mean_ascan.isChecked()),
result_history=result_history, result_history=result_history,
history_limit=self._bscan_history_limit, history_limit=self._bscan_history_limit,
floor_collection_id=self._bscan_history_floor_collection_id, floor_collection_id=self._bscan_history_floor_collection_id,
@@ -18,9 +18,9 @@ class AppWindowSnapshotMixin:
return snapshot_dir / "config_profile.json" return snapshot_dir / "config_profile.json"
@staticmethod @staticmethod
def _vna_json_config_profile_path(output_root: Path, output_stem: str) -> Path: def _vna_json_config_profile_path(output_dir: Path) -> Path:
"""Return companion config-profile path for one VNA-history JSON export batch.""" """Return companion config-profile path for one VNA-history JSON export batch."""
return output_root / f"{output_stem}_config_profile.json" return output_dir / "config_profile.json"
def _save_snapshot(self) -> None: def _save_snapshot(self) -> None:
"""Save runtime snapshot in numpy-directory format.""" """Save runtime snapshot in numpy-directory format."""
@@ -98,8 +98,9 @@ class AppWindowSnapshotMixin:
output_stem = str(summary.get("output_stem", "")).strip() output_stem = str(summary.get("output_stem", "")).strip()
if not output_stem: if not output_stem:
raise RuntimeError("VNA history JSON export did not report output_stem for config companion save") raise RuntimeError("VNA history JSON export did not report output_stem for config companion save")
output_dir = Path(str(summary.get("output_dir", "")).strip() or output_paths[0].parent)
config_profile_path = self._vna_json_config_profile_path(output_root, output_stem) config_profile_path = self._vna_json_config_profile_path(output_dir)
try: try:
self._write_gui_profile_to_path(config_profile_path, allow_overwrite=False) self._write_gui_profile_to_path(config_profile_path, allow_overwrite=False)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
@@ -110,6 +111,7 @@ class AppWindowSnapshotMixin:
"VNA history JSON files were saved, but the adjacent config profile could not be written", "VNA history JSON files were saved, but the adjacent config profile could not be written",
details=( details=(
f"output_root={output_root}\n" f"output_root={output_root}\n"
f"output_dir={output_dir}\n"
f"config_profile_path={config_profile_path}\n" f"config_profile_path={config_profile_path}\n"
f"saved_json_files={len(output_paths)}\n" f"saved_json_files={len(output_paths)}\n"
f"{exported_preview}\n\n" f"{exported_preview}\n\n"
@@ -134,6 +134,9 @@ def build_processing_group(owner) -> QGroupBox:
owner._bscan_stop_freq_mhz.setSingleStep(10.0) owner._bscan_stop_freq_mhz.setSingleStep(10.0)
owner._bscan_stop_freq_mhz.setValue(float(bscan_defaults.stop_freq_mhz)) owner._bscan_stop_freq_mhz.setValue(float(bscan_defaults.stop_freq_mhz))
owner._bscan_subtract_mean_ascan = QCheckBox("Subtract mean A-scan")
owner._bscan_subtract_mean_ascan.setChecked(bool(bscan_defaults.subtract_mean_ascan))
bscan_page = _build_processing_mode_page( bscan_page = _build_processing_mode_page(
owner._processing_mode_pages, owner._processing_mode_pages,
[ [
@@ -143,8 +146,9 @@ def build_processing_group(owner) -> QGroupBox:
("Gain", owner._bscan_gain), ("Gain", owner._bscan_gain),
("Start MHz", owner._bscan_start_freq_mhz), ("Start MHz", owner._bscan_start_freq_mhz),
("Stop MHz", owner._bscan_stop_freq_mhz), ("Stop MHz", owner._bscan_stop_freq_mhz),
owner._bscan_subtract_mean_ascan,
], ],
split_index=3, split_index=4,
) )
owner._processing_mode_pages.addWidget(bscan_page) owner._processing_mode_pages.addWidget(bscan_page)
@@ -314,6 +318,7 @@ def build_processing_group(owner) -> QGroupBox:
owner._bscan_gain.valueChanged.connect(owner._on_processing_live_settings_changed) 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_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._bscan_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._bscan_subtract_mean_ascan.toggled.connect(owner._on_processing_live_settings_changed)
owner._gpr_input_positions_input.editingFinished.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_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_min_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
+3 -3
View File
@@ -14,14 +14,14 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
from python_app.gui.app_window import AppWindow from python_app.gui.app_window import AppWindow
from python_app.gui.theme import apply_dark_theme from python_app.gui.theme import apply_light_theme
def main() -> int: def main() -> int:
"""Run Qt event loop and show main radar control window.""" """Run Qt event loop and show main radar control window."""
app = QApplication(sys.argv) app = QApplication(sys.argv)
apply_dark_theme(app) apply_light_theme(app)
pg.setConfigOptions(antialias=True, foreground="#dbe4f1") pg.setConfigOptions(antialias=True, background="#ffffff", foreground="#334155")
window = AppWindow(PROJECT_ROOT) window = AppWindow(PROJECT_ROOT)
window.showMaximized() window.showMaximized()
return app.exec() return app.exec()
+34 -34
View File
@@ -6,19 +6,19 @@ from PyQt6.QtGui import QColor, QPalette
from PyQt6.QtWidgets import QApplication, QStyleFactory from PyQt6.QtWidgets import QApplication, QStyleFactory
_DARK_STYLESHEET = """ _LIGHT_STYLESHEET = """
QMainWindow, QDialog { QMainWindow, QDialog {
background-color: #0f131a; background-color: #f3f6fb;
} }
QWidget { QWidget {
color: #e7edf7; color: #1f2937;
font-size: 13px; font-size: 13px;
} }
QGroupBox { QGroupBox {
background-color: #151b24; background-color: #ffffff;
border: 1px solid #263142; border: 1px solid #d7dee8;
border-radius: 10px; border-radius: 10px;
margin-top: 14px; margin-top: 14px;
padding: 10px; padding: 10px;
@@ -28,39 +28,39 @@ QGroupBox::title {
subcontrol-origin: margin; subcontrol-origin: margin;
left: 10px; left: 10px;
padding: 0 6px; padding: 0 6px;
color: #9fb4ce; color: #526277;
font-weight: 600; font-weight: 600;
} }
QPushButton { QPushButton {
background-color: #1d2735; background-color: #f8fafc;
border: 1px solid #314055; border: 1px solid #cad4e1;
border-radius: 8px; border-radius: 8px;
padding: 7px 12px; padding: 7px 12px;
} }
QPushButton:hover { QPushButton:hover {
background-color: #243246; background-color: #eef3f9;
} }
QPushButton:pressed { QPushButton:pressed {
background-color: #1a2432; background-color: #e4ebf4;
} }
QPushButton:checked { QPushButton:checked {
background-color: #2f7ee6; background-color: #2f7ee6;
border-color: #5d88bd; border-color: #2f7ee6;
color: #ffffff; color: #ffffff;
} }
QPushButton:checked:hover { QPushButton:checked:hover {
background-color: #3c89ee; background-color: #3b8bf4;
} }
QPushButton:disabled { QPushButton:disabled {
color: #6b7d95; color: #98a4b3;
background-color: #151d27; background-color: #f3f5f8;
border-color: #232e3d; border-color: #dde4ec;
} }
QPushButton#settingsToggleButton { QPushButton#settingsToggleButton {
@@ -83,11 +83,11 @@ QTextEdit,
QComboBox, QComboBox,
QSpinBox, QSpinBox,
QDoubleSpinBox { QDoubleSpinBox {
background-color: #101721; background-color: #ffffff;
border: 1px solid #2e3b4e; border: 1px solid #c9d4e1;
border-radius: 7px; border-radius: 7px;
padding: 5px 8px; padding: 5px 8px;
selection-background-color: #2f7ee6; selection-background-color: #cfe3ff;
} }
QLineEdit:focus, QLineEdit:focus,
@@ -96,7 +96,7 @@ QTextEdit:focus,
QComboBox:focus, QComboBox:focus,
QSpinBox:focus, QSpinBox:focus,
QDoubleSpinBox:focus { QDoubleSpinBox:focus {
border: 1px solid #5d88bd; border: 1px solid #2f7ee6;
} }
QTextEdit#runtimeLogBox { QTextEdit#runtimeLogBox {
@@ -116,34 +116,34 @@ QScrollArea {
} }
QLabel#statusLabel { QLabel#statusLabel {
color: #94b4d9; color: #35507a;
font-weight: 600; font-weight: 600;
padding: 2px 1px; padding: 2px 1px;
} }
QLabel#hintLabel { QLabel#hintLabel {
color: #7f94af; color: #6c7b8d;
} }
""" """
def apply_dark_theme(app: QApplication) -> None: def apply_light_theme(app: QApplication) -> None:
"""Apply a minimal modern dark theme shared by all windows.""" """Apply a minimal light theme shared by all windows."""
app.setStyle(QStyleFactory.create("Fusion")) app.setStyle(QStyleFactory.create("Fusion"))
palette = QPalette() palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor("#0f131a")) palette.setColor(QPalette.ColorRole.Window, QColor("#f3f6fb"))
palette.setColor(QPalette.ColorRole.WindowText, QColor("#e7edf7")) palette.setColor(QPalette.ColorRole.WindowText, QColor("#1f2937"))
palette.setColor(QPalette.ColorRole.Base, QColor("#101721")) palette.setColor(QPalette.ColorRole.Base, QColor("#ffffff"))
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#151b24")) palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#eef3f8"))
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#151b24")) palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#ffffff"))
palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#e7edf7")) palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#1f2937"))
palette.setColor(QPalette.ColorRole.Text, QColor("#e7edf7")) palette.setColor(QPalette.ColorRole.Text, QColor("#1f2937"))
palette.setColor(QPalette.ColorRole.Button, QColor("#1d2735")) palette.setColor(QPalette.ColorRole.Button, QColor("#f8fafc"))
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#e7edf7")) palette.setColor(QPalette.ColorRole.ButtonText, QColor("#1f2937"))
palette.setColor(QPalette.ColorRole.BrightText, QColor("#ffffff")) palette.setColor(QPalette.ColorRole.BrightText, QColor("#ffffff"))
palette.setColor(QPalette.ColorRole.Link, QColor("#5d88bd")) palette.setColor(QPalette.ColorRole.Link, QColor("#2f7ee6"))
palette.setColor(QPalette.ColorRole.Highlight, QColor("#2f7ee6")) palette.setColor(QPalette.ColorRole.Highlight, QColor("#2f7ee6"))
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff")) palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
app.setPalette(palette) app.setPalette(palette)
app.setStyleSheet(_DARK_STYLESHEET) app.setStyleSheet(_LIGHT_STYLESHEET)
+7
View File
@@ -152,6 +152,12 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
gui.processing.bscan.stop_freq_mhz, gui.processing.bscan.stop_freq_mhz,
"gui.processing.bscan", "gui.processing.bscan",
), ),
subtract_mean_ascan=_optional_bool(
bscan_object,
"subtract_mean_ascan",
gui.processing.bscan.subtract_mean_ascan,
"gui.processing.bscan",
),
), ),
gpr=GuiGprStateModel( gpr=GuiGprStateModel(
input_positions=_optional_string( input_positions=_optional_string(
@@ -359,6 +365,7 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]:
"gain": gui.processing.bscan.gain, "gain": gui.processing.bscan.gain,
"start_freq_mhz": gui.processing.bscan.start_freq_mhz, "start_freq_mhz": gui.processing.bscan.start_freq_mhz,
"stop_freq_mhz": gui.processing.bscan.stop_freq_mhz, "stop_freq_mhz": gui.processing.bscan.stop_freq_mhz,
"subtract_mean_ascan": gui.processing.bscan.subtract_mean_ascan,
}, },
"gpr": { "gpr": {
"input_positions": gui.processing.gpr.input_positions, "input_positions": gui.processing.gpr.input_positions,
+1
View File
@@ -42,6 +42,7 @@ class GuiBscanStateModel:
gain: float = 1.0 gain: float = 1.0
start_freq_mhz: float = 100.0 start_freq_mhz: float = 100.0
stop_freq_mhz: float = 8800.0 stop_freq_mhz: float = 8800.0
subtract_mean_ascan: bool = False
@dataclass(slots=True) @dataclass(slots=True)
+19
View File
@@ -9,6 +9,7 @@ from python_app.models.run_config_schema import (
GprRxGeometryModel, GprRxGeometryModel,
GprTxGeometryModel, GprTxGeometryModel,
PreprocessAssetModel, PreprocessAssetModel,
PreprocessNotchModel,
RunConfigModel, RunConfigModel,
) )
from python_app.models.run_config_validation import load_ring_payload, load_switch_payload, validate_gpr_model from python_app.models.run_config_validation import load_ring_payload, load_switch_payload, validate_gpr_model
@@ -137,6 +138,18 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
_as_dict(s11_preprocess_payload.get("reference"), "preprocess.s11.reference"), _as_dict(s11_preprocess_payload.get("reference"), "preprocess.s11.reference"),
model.preprocess.s11.reference, model.preprocess.s11.reference,
) )
notch_payload = _as_dict(preprocess_payload.get("notch"), "preprocess.notch")
model.preprocess.notch = PreprocessNotchModel(
enabled=bool(notch_payload.get("enabled", model.preprocess.notch.enabled)),
taper_width_hz=float(notch_payload.get("taper_width_hz", model.preprocess.notch.taper_width_hz)),
taper_type=str(notch_payload.get("taper_type", model.preprocess.notch.taper_type)),
bands_hz=[],
)
bands_payload = notch_payload.get("bands_hz", [])
if isinstance(bands_payload, list):
for band in bands_payload:
if isinstance(band, (list, tuple)) and len(band) == 2:
model.preprocess.notch.bands_hz.append((float(band[0]), float(band[1])))
model.gpr.mode = str(gpr_payload.get("mode", model.gpr.mode)) model.gpr.mode = str(gpr_payload.get("mode", model.gpr.mode))
model.gpr.relative_permittivity = float( model.gpr.relative_permittivity = float(
@@ -282,6 +295,12 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
"bundle_path": model.preprocess.s11.reference.bundle_path, "bundle_path": model.preprocess.s11.reference.bundle_path,
}, },
}, },
"notch": {
"enabled": model.preprocess.notch.enabled,
"bands_hz": [[low_hz, high_hz] for low_hz, high_hz in model.preprocess.notch.bands_hz],
"taper_width_hz": model.preprocess.notch.taper_width_hz,
"taper_type": model.preprocess.notch.taper_type,
},
}, },
"gpr": { "gpr": {
"mode": model.gpr.mode, "mode": model.gpr.mode,
+2
View File
@@ -8,6 +8,7 @@ from python_app.models.run_config_schema import (
GprTxGeometryModel, GprTxGeometryModel,
LocatorServerRuntimeModel, LocatorServerRuntimeModel,
PreprocessAssetModel, PreprocessAssetModel,
PreprocessNotchModel,
PreprocessModel, PreprocessModel,
RadarModel, RadarModel,
RadarSweepModel, RadarSweepModel,
@@ -33,6 +34,7 @@ __all__ = [
"GprTxGeometryModel", "GprTxGeometryModel",
"LocatorServerRuntimeModel", "LocatorServerRuntimeModel",
"PreprocessAssetModel", "PreprocessAssetModel",
"PreprocessNotchModel",
"PreprocessModel", "PreprocessModel",
"RadarModel", "RadarModel",
"RadarSweepModel", "RadarSweepModel",
+11
View File
@@ -132,12 +132,23 @@ class S11PreprocessModel:
reference: PreprocessAssetModel = field(default_factory=PreprocessAssetModel) reference: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
@dataclass(slots=True)
class PreprocessNotchModel:
"""Optional frequency-domain notch filter applied after calibration and reference subtraction."""
enabled: bool = False
bands_hz: list[tuple[float, float]] = field(default_factory=list)
taper_width_hz: float = 40_000_000.0
taper_type: str = "cosine"
@dataclass(slots=True) @dataclass(slots=True)
class PreprocessModel: class PreprocessModel:
"""Selected preprocessing artifacts for live acquisition.""" """Selected preprocessing artifacts for live acquisition."""
s21: S21PreprocessModel = field(default_factory=S21PreprocessModel) s21: S21PreprocessModel = field(default_factory=S21PreprocessModel)
s11: S11PreprocessModel = field(default_factory=S11PreprocessModel) s11: S11PreprocessModel = field(default_factory=S11PreprocessModel)
notch: PreprocessNotchModel = field(default_factory=PreprocessNotchModel)
@dataclass(slots=True) @dataclass(slots=True)
+12
View File
@@ -108,6 +108,18 @@
"set_name": "", "set_name": "",
"bundle_path": "" "bundle_path": ""
} }
},
"notch": {
"enabled": true,
"bands_hz": [
[790000000.0, 850000000.0],
[1790000000.0, 1890000000.0],
[1970000000.0, 2050000000.0],
[2080000000.0, 2560000000.0],
[2650000000.0, 2700000000.0]
],
"taper_width_hz": 40000000.0,
"taper_type": "cosine"
} }
}, },
"gpr": { "gpr": {
+13 -2
View File
@@ -31,6 +31,11 @@ class NpzStore(StoreApi):
self._root_dir = root_dir self._root_dir = root_dir
self._root_dir.mkdir(parents=True, exist_ok=True) self._root_dir.mkdir(parents=True, exist_ok=True)
@staticmethod
def _vna_json_output_dir(output_root_dir: Path, output_stem: str) -> Path:
"""Return per-export directory for VNA history JSON files."""
return output_root_dir / output_stem
def save_set(self, kind: str, radar_key: str, set_name: str, collection: SweepCollection) -> None: def save_set(self, kind: str, radar_key: str, set_name: str, collection: SweepCollection) -> None:
"""Persist named preprocess set as NPZ and metadata JSON.""" """Persist named preprocess set as NPZ and metadata JSON."""
set_dir = self._set_dir(kind, radar_key) set_dir = self._set_dir(kind, radar_key)
@@ -217,7 +222,9 @@ class NpzStore(StoreApi):
output_stem = sanitize_path_component(output_stem) output_stem = sanitize_path_component(output_stem)
output_root_dir.mkdir(parents=True, exist_ok=True) output_root_dir.mkdir(parents=True, exist_ok=True)
output_path = output_root_dir / f"{output_stem}_vna_bscan_history.json" output_dir = self._vna_json_output_dir(output_root_dir, output_stem)
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"{output_stem}_vna_bscan_history.json"
if output_path.exists(): if output_path.exists():
raise FileExistsError(f"Output JSON file already exists: {output_path}") raise FileExistsError(f"Output JSON file already exists: {output_path}")
@@ -253,6 +260,7 @@ class NpzStore(StoreApi):
summary["channel"] = str(channel) summary["channel"] = str(channel)
summary["primary_stage"] = str(primary_stage) summary["primary_stage"] = str(primary_stage)
summary["output_stem"] = output_stem summary["output_stem"] = output_stem
summary["output_dir"] = str(output_dir)
summary["output_path"] = str(output_path) summary["output_path"] = str(output_path)
return output_path, summary return output_path, summary
@@ -275,6 +283,8 @@ class NpzStore(StoreApi):
output_stem = output_name.strip() or datetime.utcnow().strftime("snapshot_%Y%m%d_%H%M%S") output_stem = output_name.strip() or datetime.utcnow().strftime("snapshot_%Y%m%d_%H%M%S")
output_stem = sanitize_path_component(output_stem) output_stem = sanitize_path_component(output_stem)
output_root_dir.mkdir(parents=True, exist_ok=True) output_root_dir.mkdir(parents=True, exist_ok=True)
output_dir = self._vna_json_output_dir(output_root_dir, output_stem)
output_dir.mkdir(parents=True, exist_ok=True)
selected_raw, selected_preprocessed, selected_results, selection_summary = select_aligned_histories( selected_raw, selected_preprocessed, selected_results, selection_summary = select_aligned_histories(
raw_history, raw_history,
@@ -296,7 +306,7 @@ class NpzStore(StoreApi):
output_paths: list[Path] = [] output_paths: list[Path] = []
payloads: list[dict[str, Any]] = [] payloads: list[dict[str, Any]] = []
for input_index, output_index in combos: for input_index, output_index in combos:
output_path = output_root_dir / ( output_path = output_dir / (
f"{output_stem}_i{input_index}_o{output_index}_{channel}_vna_bscan_history.json" f"{output_stem}_i{input_index}_o{output_index}_{channel}_vna_bscan_history.json"
) )
if output_path.exists(): if output_path.exists():
@@ -330,6 +340,7 @@ class NpzStore(StoreApi):
summary["channel"] = str(channel) summary["channel"] = str(channel)
summary["primary_stage"] = str(primary_stage) summary["primary_stage"] = str(primary_stage)
summary["output_stem"] = output_stem summary["output_stem"] = output_stem
summary["output_dir"] = str(output_dir)
summary["output_paths"] = [str(path) for path in output_paths] summary["output_paths"] = [str(path) for path in output_paths]
return output_paths, summary return output_paths, summary
+12
View File
@@ -93,6 +93,18 @@
"set_name": "", "set_name": "",
"bundle_path": "" "bundle_path": ""
} }
},
"notch": {
"enabled": true,
"bands_hz": [
[790000000.0, 850000000.0],
[1790000000.0, 1890000000.0],
[1970000000.0, 2050000000.0],
[2080000000.0, 2560000000.0],
[2650000000.0, 2700000000.0]
],
"taper_width_hz": 40000000.0,
"taper_type": "cosine"
} }
}, },
"gpr": { "gpr": {