added s11!

This commit is contained in:
Ayzen
2026-03-26 18:29:42 +03:00
parent 9ddbde22bd
commit 077542cbd0
43 changed files with 713 additions and 347 deletions
@@ -73,16 +73,31 @@ struct RuntimeConfig {
std::string processing_live_config_path = "python_app/runtime/processing_live.json";
};
struct PreprocessAssetConfig {
std::string set_name{};
std::string bundle_path{};
};
struct S21PreprocessConfig {
PreprocessAssetConfig calibration{};
PreprocessAssetConfig reference{};
};
struct S11CalibrationPreprocessConfig {
PreprocessAssetConfig open{};
PreprocessAssetConfig short_standard{};
PreprocessAssetConfig load{};
};
struct S11PreprocessConfig {
S11CalibrationPreprocessConfig calibration{};
PreprocessAssetConfig reference{};
};
struct PreprocessConfig {
// Names and bundle paths selected by Python GUI layer.
std::string s21_calibration_set{};
std::string s21_reference_set{};
std::string s21_calibration_bundle_path{};
std::string s21_reference_bundle_path{};
std::string s11_open_calibration_bundle_path{};
std::string s11_short_calibration_bundle_path{};
std::string s11_load_calibration_bundle_path{};
std::string s11_reference_bundle_path{};
// Channel-specific preprocessing assets selected by Python GUI layer.
S21PreprocessConfig s21{};
S11PreprocessConfig s11{};
};
struct GprTxGeometry {
@@ -140,6 +140,20 @@ using Json = nlohmann::json;
return fallback;
}
[[nodiscard]] auto parse_preprocess_asset(
const Json& object,
const std::string& key,
const std::string& context
) -> PreprocessAssetConfig {
const auto asset_context = context + "." + key;
const auto* asset_obj = as_object(required_field(object, key), asset_context);
PreprocessAssetConfig asset{};
asset.set_name = optional_string(*asset_obj, "set_name", "");
asset.bundle_path = optional_string(*asset_obj, "bundle_path", "");
return asset;
}
[[nodiscard]] auto parse_driver_mode(const std::string& value) -> DriverMode {
if (value == "mock") {
return DriverMode::Mock;
@@ -438,18 +452,20 @@ auto load_run_config(const std::string& path) -> RunConfig {
{
const auto* preprocess_obj = as_object(required_field(*root_obj, "preprocess"), "preprocess");
config.preprocess.s21_calibration_set = optional_string(*preprocess_obj, "s21_calibration_set", "");
config.preprocess.s21_reference_set = optional_string(*preprocess_obj, "s21_reference_set", "");
config.preprocess.s21_calibration_bundle_path =
optional_string(*preprocess_obj, "s21_calibration_bundle_path", "");
config.preprocess.s21_reference_bundle_path = optional_string(*preprocess_obj, "s21_reference_bundle_path", "");
config.preprocess.s11_open_calibration_bundle_path =
optional_string(*preprocess_obj, "s11_open_calibration_bundle_path", "");
config.preprocess.s11_short_calibration_bundle_path =
optional_string(*preprocess_obj, "s11_short_calibration_bundle_path", "");
config.preprocess.s11_load_calibration_bundle_path =
optional_string(*preprocess_obj, "s11_load_calibration_bundle_path", "");
config.preprocess.s11_reference_bundle_path = optional_string(*preprocess_obj, "s11_reference_bundle_path", "");
const auto* s21_obj = as_object(required_field(*preprocess_obj, "s21"), "preprocess.s21");
config.preprocess.s21.calibration = parse_preprocess_asset(*s21_obj, "calibration", "preprocess.s21");
config.preprocess.s21.reference = parse_preprocess_asset(*s21_obj, "reference", "preprocess.s21");
const auto* s11_obj = as_object(required_field(*preprocess_obj, "s11"), "preprocess.s11");
const auto* s11_calibration_obj =
as_object(required_field(*s11_obj, "calibration"), "preprocess.s11.calibration");
config.preprocess.s11.calibration.open =
parse_preprocess_asset(*s11_calibration_obj, "open", "preprocess.s11.calibration");
config.preprocess.s11.calibration.short_standard =
parse_preprocess_asset(*s11_calibration_obj, "short", "preprocess.s11.calibration");
config.preprocess.s11.calibration.load =
parse_preprocess_asset(*s11_calibration_obj, "load", "preprocess.s11.calibration");
config.preprocess.s11.reference = parse_preprocess_asset(*s11_obj, "reference", "preprocess.s11");
}
if (const auto* gpr_value = optional_field(*root_obj, "gpr"); gpr_value != nullptr) {
@@ -65,16 +65,16 @@ int main(int argc, char** argv) {
radar::preprocessing::CalibrationMaster calibration_master(
radar::preprocessing::make_s21_through_calibrator()
);
calibration_master.load_s21_calibration_bundle(config.preprocess.s21_calibration_bundle_path);
calibration_master.load_s21_calibration_bundle(config.preprocess.s21.calibration.bundle_path);
calibration_master.load_s11_calibration_bundle(
config.preprocess.s11_open_calibration_bundle_path,
config.preprocess.s11_short_calibration_bundle_path,
config.preprocess.s11_load_calibration_bundle_path
config.preprocess.s11.calibration.open.bundle_path,
config.preprocess.s11.calibration.short_standard.bundle_path,
config.preprocess.s11.calibration.load.bundle_path
);
radar::preprocessing::ReferenceMaster reference_master;
reference_master.load_s21_reference_bundle(config.preprocess.s21_reference_bundle_path);
reference_master.load_s11_reference_bundle(config.preprocess.s11_reference_bundle_path);
reference_master.load_s21_reference_bundle(config.preprocess.s21.reference.bundle_path);
reference_master.load_s11_reference_bundle(config.preprocess.s11.reference.bundle_path);
reference_master.prepare_calibrated(calibration_master, config.run_combos);
radar::preprocessing::DataPreprocessor preprocessor(
@@ -23,6 +23,7 @@ struct ProcessingLiveConfig {
float pass_through_y_min_db = -100.0F;
float pass_through_y_max_db = 0.0F;
std::string bscan_axis = "abs";
std::string bscan_channel = "s21";
float bscan_cut_m = 0.824F;
float bscan_max_depth_m = 1.0F;
float bscan_gain = 1.0F;
@@ -30,11 +30,11 @@ using Json = nlohmann::json;
throw std::runtime_error("processing.history_command must be one of: none, remove_last, clear_all");
}
[[nodiscard]] auto parse_pass_through_channel(const std::string& value) -> std::string {
[[nodiscard]] auto parse_s_parameter_channel(const std::string& value, const std::string& field_name) -> std::string {
if (value == "s21" || value == "s11") {
return value;
}
throw std::runtime_error("processing.pass_through_channel must be one of: s21, s11");
throw std::runtime_error(field_name + " must be one of: s21, s11");
}
[[nodiscard]] auto parse_u64_number(const Json& value, const std::string& field_name) -> std::uint64_t {
@@ -112,7 +112,8 @@ using Json = nlohmann::json;
if (!found->is_string()) {
throw std::runtime_error("processing.pass_through_channel must be string");
}
config.pass_through_channel = parse_pass_through_channel(found->get<std::string>());
config.pass_through_channel =
parse_s_parameter_channel(found->get<std::string>(), "processing.pass_through_channel");
}
if (const auto found = root.find("pass_through_fixed_y_enabled"); found != root.end()) {
if (!found->is_boolean()) {
@@ -138,6 +139,12 @@ using Json = nlohmann::json;
}
config.bscan_axis = found->get<std::string>();
}
if (const auto found = root.find("bscan_channel"); found != root.end()) {
if (!found->is_string()) {
throw std::runtime_error("processing.bscan_channel must be string");
}
config.bscan_channel = parse_s_parameter_channel(found->get<std::string>(), "processing.bscan_channel");
}
if (const auto found = root.find("bscan_cut_m"); found != root.end()) {
if (!found->is_number()) {
throw std::runtime_error("processing.bscan_cut_m must be number");
@@ -6,6 +6,7 @@
#include <cstddef>
#include <cstdint>
#include <limits>
#include <span>
#include <string_view>
#include <utility>
#include <vector>
@@ -92,15 +93,28 @@ void fft_inplace(std::vector<std::complex<double>>& values, bool inverse) {
return std::abs(sample);
}
[[nodiscard]] auto fallback_profile(const ipc::SweepTraceBlock& trace) -> BScanProfile {
const std::size_t point_count = std::min(trace.frequency_hz.size(), trace.s21.size());
[[nodiscard]] auto selected_trace_samples(
const ipc::SweepTraceBlock& trace,
const ProcessingLiveConfig& live_config
) -> std::span<const ipc::Complex32> {
if (live_config.bscan_channel == "s11") {
return trace.s11;
}
return trace.s21;
}
[[nodiscard]] auto fallback_profile(
const ipc::SweepTraceBlock& trace,
std::span<const ipc::Complex32> selected_samples
) -> BScanProfile {
const std::size_t point_count = std::min(trace.frequency_hz.size(), selected_samples.size());
BScanProfile fallback{};
fallback.depth_m.reserve(point_count);
fallback.response.reserve(point_count);
const float denominator = point_count > 1U ? static_cast<float>(point_count - 1U) : 1.0F;
for (std::size_t index = 0U; index < point_count; ++index) {
const auto& sample = trace.s21[index];
const auto& sample = selected_samples[index];
fallback.depth_m.push_back(static_cast<float>(static_cast<float>(index) / denominator));
fallback.response.push_back(std::sqrt(sample.re * sample.re + sample.im * sample.im));
}
@@ -111,9 +125,10 @@ void fft_inplace(std::vector<std::complex<double>>& values, bool inverse) {
const ipc::SweepTraceBlock& trace,
const ProcessingLiveConfig& live_config
) -> BScanProfile {
const std::size_t point_count = std::min(trace.frequency_hz.size(), trace.s21.size());
const auto selected_samples = selected_trace_samples(trace, live_config);
const std::size_t point_count = std::min(trace.frequency_hz.size(), selected_samples.size());
if (point_count < 2U) {
return fallback_profile(trace);
return fallback_profile(trace, selected_samples);
}
const double configured_start_hz = static_cast<double>(live_config.bscan_start_freq_mhz) * 1'000'000.0;
@@ -122,55 +137,55 @@ void fft_inplace(std::vector<std::complex<double>>& values, bool inverse) {
const double stop_hz = std::max(configured_start_hz, configured_stop_hz);
std::vector<double> filtered_freq_hz{};
std::vector<std::complex<double>> filtered_s21{};
std::vector<std::complex<double>> filtered_samples{};
filtered_freq_hz.reserve(point_count);
filtered_s21.reserve(point_count);
filtered_samples.reserve(point_count);
for (std::size_t index = 0U; index < point_count; ++index) {
const double frequency_hz = static_cast<double>(trace.frequency_hz[index]);
if (frequency_hz < start_hz || frequency_hz > stop_hz) {
continue;
}
const auto& sample = trace.s21[index];
const auto& sample = selected_samples[index];
filtered_freq_hz.push_back(frequency_hz);
filtered_s21.emplace_back(static_cast<double>(sample.re), static_cast<double>(sample.im));
filtered_samples.emplace_back(static_cast<double>(sample.re), static_cast<double>(sample.im));
}
if (filtered_freq_hz.size() < 2U) {
return fallback_profile(trace);
return fallback_profile(trace, selected_samples);
}
const std::size_t filtered_count = filtered_freq_hz.size();
const double df = (filtered_freq_hz.back() - filtered_freq_hz.front()) / static_cast<double>(filtered_count - 1U);
if (df <= 0.0) {
return fallback_profile(trace);
return fallback_profile(trace, selected_samples);
}
const auto start_bin = static_cast<std::int64_t>(std::llround(filtered_freq_hz.front() / df));
if (start_bin < 0) {
return fallback_profile(trace);
return fallback_profile(trace, selected_samples);
}
const auto start_index = static_cast<std::size_t>(start_bin);
if (start_index > (std::numeric_limits<std::size_t>::max() / 2U)) {
return fallback_profile(trace);
return fallback_profile(trace, selected_samples);
}
if (start_index > (std::numeric_limits<std::size_t>::max() - filtered_count + 1U)) {
return fallback_profile(trace);
return fallback_profile(trace, selected_samples);
}
const std::size_t min_fft_len = 2U * (start_index + filtered_count - 1U);
const std::size_t fft_len = next_power_of_two(min_fft_len);
if (fft_len < min_fft_len || (fft_len & (fft_len - 1U)) != 0U) {
return fallback_profile(trace);
return fallback_profile(trace, selected_samples);
}
if (start_index > fft_len || filtered_count > (fft_len - start_index)) {
return fallback_profile(trace);
return fallback_profile(trace, selected_samples);
}
std::vector<std::complex<double>> spectrum(fft_len, std::complex<double>(0.0, 0.0));
for (std::size_t index = 0U; index < filtered_count; ++index) {
spectrum[start_index + index] = filtered_s21[index];
spectrum[start_index + index] = filtered_samples[index];
}
fft_inplace(spectrum, true);
+5 -2
View File
@@ -24,6 +24,7 @@ from python_app.models.dataset_model import ResultCollection, SweepCollection
from python_app.models.run_config_model import RunConfigModel
from python_app.orchestration.config_writer import ConfigWriter
from python_app.orchestration.live_processing_config import ProcessingLiveConfigWriter
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model
from python_app.orchestration.process_supervisor import ProcessSupervisor
from python_app.orchestration.shm_reader import ShmRingReader
from python_app.storage.npz_store import NpzStore
@@ -78,8 +79,10 @@ class AppWindow(
def _init_preprocess_state(self) -> None:
"""Initialize preprocessing dialog and selected set names."""
self._preprocess_dialog: PreprocessDialog | None = None
self._selected_s21_calibration_set = str(self._defaults_config.preprocess.s21_calibration_set)
self._selected_s21_reference_set = str(self._defaults_config.preprocess.s21_reference_set)
self._selected_preprocess_sets = {
key: str(preprocess_asset_model(self._defaults_config, key).set_name)
for key in PREPROCESS_ASSET_KEYS
}
def _init_capture_state(self) -> None:
"""Initialize one-shot capture and sequence-control flags."""
@@ -7,6 +7,7 @@ from python_app.models.run_config_model import ComboModel, GprRxGeometryModel, G
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.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model
from python_app.storage.npz_store import radar_key_from_config
@@ -118,8 +119,10 @@ class AppWindowConfigMixin:
if self._switches_are_effectively_static(config):
config.combos = [ComboModel(input=0, output=0)]
config.preprocess.s21_calibration_set = self._selected_s21_calibration_set
config.preprocess.s21_reference_set = self._selected_s21_reference_set
for key in PREPROCESS_ASSET_KEYS:
asset = preprocess_asset_model(config, key)
asset.set_name = self._selected_preprocess_sets[key]
asset.bundle_path = ""
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())
@@ -132,7 +135,7 @@ class AppWindowConfigMixin:
return config
def _radar_key(self, config: RunConfigModel) -> str:
"""Build radar key used by calibration/reference storage lookup."""
"""Build radar key used by preprocess-set storage lookup."""
return radar_key_from_config(
model_name=config.radar.model,
serial=config.radar.serial,
@@ -158,6 +161,7 @@ class AppWindowConfigMixin:
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_channel=self._bscan_channel.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()),
@@ -9,6 +9,7 @@ from python_app.gui.runtime.history import build_run_history_signature, record_r
from python_app.hardware_full.librevna_service import LibreVnaService
from python_app.models.dataset_model import ComboKey, ResultCollection, SweepCollection
from python_app.models.run_config_model import RunConfigModel
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, PREPROCESS_ASSET_SPECS, preprocess_asset_model
from python_app.orchestration.shm_reader import ShmRingReader
@@ -38,28 +39,26 @@ class AppWindowPipelineMixin:
run_signature = self._build_run_history_signature(config)
radar_key = self._radar_key(config)
if not config.preprocess.s21_calibration_set or not config.preprocess.s21_reference_set:
raise RuntimeError("Select calibration and reference sets in Preprocessing Panel before Start")
missing_assets = [
PREPROCESS_ASSET_SPECS[key].display_name
for key in PREPROCESS_ASSET_KEYS
if not preprocess_asset_model(config, key).set_name
]
if missing_assets:
raise RuntimeError(
"Select all required preprocess sets in Preprocessing Panel before Start: "
+ ", ".join(missing_assets)
)
combo_keys = [ComboKey(input_pos=combo.input, output_pos=combo.output) for combo in config.combos]
if not self._store.has_combo_coverage(
"calibration", radar_key, config.preprocess.s21_calibration_set, combo_keys
):
raise RuntimeError("Selected calibration set does not cover requested run combos")
if not self._store.has_combo_coverage(
"reference", radar_key, config.preprocess.s21_reference_set, combo_keys
):
raise RuntimeError("Selected reference set does not cover requested run combos")
for key in PREPROCESS_ASSET_KEYS:
spec = PREPROCESS_ASSET_SPECS[key]
asset = preprocess_asset_model(config, key)
if not self._store.has_combo_coverage(spec.set_kind, radar_key, asset.set_name, combo_keys):
raise RuntimeError(f"Selected {spec.display_name} set does not cover requested run combos")
calibration_bundle, reference_bundle = self._config_writer.prepare_s21_bundles(
self._store,
radar_key,
config.preprocess.s21_calibration_set,
config.preprocess.s21_reference_set,
)
config.preprocess.s21_calibration_bundle_path = str(calibration_bundle)
config.preprocess.s21_reference_bundle_path = str(reference_bundle)
self._config_writer.prepare_preprocess_bundles(self._store, radar_key, config)
config.runtime.continuous = not single_capture
if not single_capture:
@@ -389,7 +389,10 @@ class AppWindowPlotMixin:
self._bscan_plot.addItem(image_item)
self._bscan_plot.setXRange(x_min, x_max, padding=0.02)
self._bscan_plot.setYRange(depth_min, depth_max, padding=0.02)
self._bscan_plot.setTitle(f"B-scan in{display_key[0]}/out{display_key[1]} | sweeps={sweep_count}")
bscan_channel = self._bscan_channel.currentText().upper()
self._bscan_plot.setTitle(
f"B-scan {bscan_channel} in{display_key[0]}/out{display_key[1]} | sweeps={sweep_count}"
)
return True
def _sync_bscan_history_from_results(self) -> None:
@@ -793,12 +796,13 @@ class AppWindowPlotMixin:
return True
return False
def _draw_single_trace(self, trace: TraceData, title: str) -> None:
def _draw_single_trace(self, trace: TraceData, title: str, *, channel: str = "s21") -> None:
"""Draw one trace on stacked magnitude/phase plots."""
show_magnitude = self._show_magnitude_curves()
show_phase = self._show_phase_curves()
magnitude_plot = self._trace_magnitude_plot
phase_plot = self._trace_phase_plot
samples = trace.s11 if channel == "s11" else trace.s21
magnitude_plot.setVisible(show_magnitude)
phase_plot.setVisible(show_phase)
@@ -822,7 +826,7 @@ class AppWindowPlotMixin:
phase_plot.setTitle(title)
if show_magnitude:
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(trace.s21), 1e-12))
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
magnitude_curve = pg.PlotCurveItem(
trace.frequency_hz,
magnitude_db,
@@ -834,7 +838,7 @@ class AppWindowPlotMixin:
] = magnitude_curve
if show_phase:
phase_deg = np.degrees(np.angle(trace.s21))
phase_deg = np.degrees(np.angle(samples))
phase_curve = pg.PlotCurveItem(
trace.frequency_hz,
phase_deg,
@@ -3,11 +3,17 @@
from __future__ import annotations
from python_app.gui.preprocess_dialog import PreprocessDialog
from python_app.orchestration.preprocess_assets import (
PREPROCESS_ASSET_KEYS,
PREPROCESS_ASSET_SPECS,
preprocess_asset_channel,
preprocess_asset_display_name,
)
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
class AppWindowPreprocessMixin:
"""Handles calibration/reference set management and capture workflow."""
"""Handles preprocess set management and sequential capture workflows."""
def _open_preprocess_panel(self) -> None:
"""Open preprocessing dialog and refresh available sets."""
@@ -38,39 +44,39 @@ class AppWindowPreprocessMixin:
self._update_capture_dialog_state()
return dialog
def _on_preprocess_selection_changed(self, calibration_set: str, reference_set: str) -> None:
def _on_preprocess_selection_changed(self) -> None:
"""Persist selected preprocessing set names from dialog."""
self._selected_s21_calibration_set = calibration_set.strip()
self._selected_s21_reference_set = reference_set.strip()
dialog = self._ensure_preprocess_dialog()
self._selected_preprocess_sets = dialog.selection_snapshot()
self._refresh_preprocess_summary_labels()
def _refresh_preprocess_summary_labels(self) -> None:
"""Update compact summary labels in the main window."""
self._selected_calibration_label.setText(self._selected_s21_calibration_set or "<not selected>")
self._selected_reference_label.setText(self._selected_s21_reference_set or "<not selected>")
for key in PREPROCESS_ASSET_KEYS:
self._selected_preprocess_labels[key].setText(self._selected_preprocess_sets.get(key, "") or "<not selected>")
def _refresh_sets(self) -> None:
"""Refresh calibration/reference set lists for current radar key."""
"""Refresh preprocess set lists for current radar key."""
config = self._build_config()
radar_key = self._radar_key(config)
calibration_sets = self._store.list_sets("calibration", radar_key)
reference_sets = self._store.list_sets("reference", radar_key)
dialog = self._ensure_preprocess_dialog()
dialog.set_calibration_sets(calibration_sets)
dialog.set_reference_sets(reference_sets)
if self._selected_s21_calibration_set not in calibration_sets:
self._selected_s21_calibration_set = calibration_sets[0] if calibration_sets else ""
if self._selected_s21_reference_set not in reference_sets:
self._selected_s21_reference_set = reference_sets[0] if reference_sets else ""
available_sets = {
key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
for key in PREPROCESS_ASSET_KEYS
}
dialog.set_available_sets(available_sets)
dialog.set_selected_sets(self._selected_s21_calibration_set, self._selected_s21_reference_set)
for key, names in available_sets.items():
if self._selected_preprocess_sets.get(key, "") not in names:
self._selected_preprocess_sets[key] = names[0] if names else ""
dialog.set_selected_sets(self._selected_preprocess_sets)
self._refresh_preprocess_summary_labels()
self._log(f"Set lists refreshed for key={radar_key}")
self._log(f"Preprocess set lists refreshed for key={radar_key}")
def _start_capture_sequence(self, kind: str) -> None:
"""Start sequential capture session for requested preprocessing kind."""
"""Start sequential capture session for requested preprocess asset."""
if self._capture_session is not None:
self._show_error("Another capture sequence is already active")
return
@@ -91,18 +97,19 @@ class AppWindowPreprocessMixin:
try:
config = self._build_config()
radar_key = self._radar_key(config)
existing_sets = self._store.list_sets(kind, radar_key)
existing_sets = self._store.list_sets(PREPROCESS_ASSET_SPECS[kind].set_kind, radar_key)
display_name = preprocess_asset_display_name(kind)
if set_name in existing_sets:
raise RuntimeError(f"Set '{set_name}' already exists for {kind} and cannot be overwritten")
raise RuntimeError(f"Set '{set_name}' already exists for {display_name} and cannot be overwritten")
session = SequentialCaptureSession(config=config, kind=kind, set_name=set_name)
session.open()
self._capture_session = session
dialog.clear_capture_log()
dialog.set_status(f"{kind.title()} sequence started")
dialog.set_status(f"{display_name} sequence started")
self._update_capture_dialog_state()
self._log(f"{kind.title()} sequence started for set={set_name}; fill all N*M combos")
self._log(f"{display_name} sequence started for set={set_name}; fill all N*M combos")
except Exception as exc: # noqa: BLE001
self._cleanup_capture_session()
self._show_error(f"Failed to start {kind} sequence: {exc}")
@@ -121,9 +128,11 @@ class AppWindowPreprocessMixin:
trace = session.capture_current_combo()
state = session.state()
tx_label, rx_label = dialog.antenna_labels()
display_name = preprocess_asset_display_name(session.kind)
channel = preprocess_asset_channel(session.kind)
dialog.append_capture_log_entry(
kind=session.kind,
kind=display_name,
captured_count=state.captured_count,
total_count=state.total_count,
input_pos=trace.combo.input_pos,
@@ -132,11 +141,11 @@ class AppWindowPreprocessMixin:
rx_label=rx_label,
)
dialog.draw_last_trace(trace, title=f"{session.kind.title()} captured")
self._draw_single_trace(trace, title=f"{session.kind.title()} last trace")
dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
self._log(
f"{session.kind.title()} capture: {state.captured_count}/{state.total_count} | "
f"{display_name} capture: {state.captured_count}/{state.total_count} | "
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
)
@@ -144,16 +153,13 @@ class AppWindowPreprocessMixin:
radar_key, collection = session.finalize(self._store)
set_name = session.set_name
kind = session.kind
display_name = preprocess_asset_display_name(kind)
self._cleanup_capture_session()
if kind == "calibration":
self._selected_s21_calibration_set = set_name
else:
self._selected_s21_reference_set = set_name
self._selected_preprocess_sets[kind] = set_name
self._refresh_sets()
dialog.set_status(f"{kind.title()} set saved: {set_name} ({len(collection.traces)} traces)")
self._log(f"{kind.title()} sequence completed and saved: set={set_name}, key={radar_key}")
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
self._resume_pipeline_if_needed()
else:
self._update_capture_dialog_state()
@@ -166,11 +172,11 @@ class AppWindowPreprocessMixin:
if self._capture_session is None:
return
kind = self._capture_session.kind
display_name = preprocess_asset_display_name(self._capture_session.kind)
self._cleanup_capture_session()
dialog = self._ensure_preprocess_dialog()
dialog.set_status(f"{kind.title()} sequence aborted")
self._log(f"{kind.title()} sequence aborted")
dialog.set_status(f"{display_name} sequence aborted")
self._log(f"{display_name} sequence aborted")
if resume_pipeline:
self._resume_pipeline_if_needed()
@@ -4,16 +4,18 @@ from __future__ import annotations
from PyQt6.QtWidgets import QFormLayout, QGroupBox, QLabel
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_display_name
def build_preprocess_summary_group(owner) -> QGroupBox:
"""Create selected calibration/reference summary section."""
"""Create selected preprocess-set summary section."""
group = QGroupBox("Selected Preprocess Sets")
form = QFormLayout(group)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
owner._selected_calibration_label = QLabel("<not selected>")
owner._selected_reference_label = QLabel("<not selected>")
form.addRow("Calibration", owner._selected_calibration_label)
form.addRow("Reference", owner._selected_reference_label)
owner._selected_preprocess_labels = {}
for key in PREPROCESS_ASSET_KEYS:
label = QLabel("<not selected>")
owner._selected_preprocess_labels[key] = label
form.addRow(preprocess_asset_display_name(key), label)
return group
@@ -110,6 +110,9 @@ def build_processing_group(owner) -> QGroupBox:
owner._bscan_axis = QComboBox()
owner._bscan_axis.addItems(["abs", "real", "phase"])
owner._bscan_channel = QComboBox()
owner._bscan_channel.addItems(["s21", "s11"])
owner._bscan_cut_m = QDoubleSpinBox()
owner._bscan_cut_m.setDecimals(3)
owner._bscan_cut_m.setRange(0.0, 2.0)
@@ -141,6 +144,7 @@ def build_processing_group(owner) -> QGroupBox:
owner._bscan_stop_freq_mhz.setValue(8800.0)
bscan_form.addRow("Axis", owner._bscan_axis)
bscan_form.addRow("Channel", owner._bscan_channel)
bscan_form.addRow("Cut m", owner._bscan_cut_m)
bscan_form.addRow("Max depth m", owner._bscan_max_depth_m)
bscan_form.addRow("Gain", owner._bscan_gain)
@@ -221,6 +225,7 @@ def build_processing_group(owner) -> QGroupBox:
form.addRow(owner._processing_mode_pages)
owner._bscan_axis.currentTextChanged.connect(owner._on_processing_live_settings_changed)
owner._bscan_channel.currentTextChanged.connect(owner._on_processing_live_settings_changed)
owner._bscan_cut_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._bscan_max_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._bscan_gain.valueChanged.connect(owner._on_processing_live_settings_changed)
+7 -4
View File
@@ -22,15 +22,17 @@ def _result_tail(
for collection in result_history[-history_limit:]
if int(collection.collection_id) > int(floor_collection_id)
]
unique_tail: list[ResultCollection] = []
unique_reversed_tail: list[ResultCollection] = []
seen_keys: set[tuple[int, int]] = set()
for collection in filtered:
for collection in reversed(filtered):
key = (int(collection.collection_id), int(collection.monotonic_ns))
if key in seen_keys:
continue
seen_keys.add(key)
unique_tail.append(collection)
return unique_tail
unique_reversed_tail.append(collection)
unique_reversed_tail.reverse()
return unique_reversed_tail
def build_bscan_signature(
@@ -47,6 +49,7 @@ def build_bscan_signature(
)
return (
str(live_config.bscan_axis),
str(live_config.bscan_channel),
float(live_config.bscan_cut_m),
float(live_config.bscan_max_depth_m),
float(live_config.bscan_gain),
+82 -74
View File
@@ -1,4 +1,4 @@
"""Dialog for calibration/reference set selection and sequential capture."""
"""Dialog for preprocess set selection and sequential capture."""
from __future__ import annotations
@@ -6,6 +6,7 @@ from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import (
QComboBox,
QDialog,
QFormLayout,
QGridLayout,
QGroupBox,
QHBoxLayout,
@@ -15,23 +16,24 @@ from PyQt6.QtWidgets import (
QPushButton,
QVBoxLayout,
)
import pyqtgraph as pg
import numpy as np
import pyqtgraph as pg
from python_app.models.dataset_model import TraceData
from python_app.orchestration.preprocess_assets import (
PREPROCESS_ASSET_KEYS,
PREPROCESS_ASSET_SPECS,
S11_PREPROCESS_ASSET_KEYS,
S21_PREPROCESS_ASSET_KEYS,
preprocess_asset_display_name,
)
class PreprocessDialog(QDialog):
"""Standalone dialog for preprocessing capture workflows.
The dialog combines three concerns:
1. Set selection (calibration/reference).
2. Sequential capture controls for filling all N*M combinations.
3. Quick preview of the last captured trace.
"""
"""Standalone dialog for preprocess set selection and capture workflows."""
refresh_requested = pyqtSignal()
selection_changed = pyqtSignal(str, str)
selection_changed = pyqtSignal()
start_sequence_requested = pyqtSignal(str)
capture_next_requested = pyqtSignal()
abort_sequence_requested = pyqtSignal()
@@ -39,13 +41,14 @@ class PreprocessDialog(QDialog):
def __init__(self, parent=None) -> None:
"""Initialize window metadata and compose dialog UI."""
super().__init__(parent)
self._set_combos: dict[str, QComboBox] = {}
self._init_window()
self._build_ui()
def _init_window(self) -> None:
"""Set static window properties."""
self.setWindowTitle("Preprocessing Setup")
self.resize(1040, 760)
self.resize(1120, 820)
def _build_ui(self) -> None:
"""Build root dialog layout and all sections."""
@@ -57,28 +60,33 @@ class PreprocessDialog(QDialog):
def _build_sets_group(self) -> QGroupBox:
"""Build set-management controls used for preprocessing snapshots."""
group = QGroupBox("Calibration / Reference Sets", self)
layout = QGridLayout(group)
group = QGroupBox("Preprocess Sets", self)
layout = QVBoxLayout(group)
header_row = QHBoxLayout()
self._set_name_input = QLineEdit("set_001", group)
self._calibration_combo = QComboBox(group)
self._reference_combo = QComboBox(group)
refresh_button = QPushButton("Refresh Sets", group)
refresh_button.clicked.connect(self.refresh_requested.emit)
header_row.addWidget(QLabel("Set name"))
header_row.addWidget(self._set_name_input, stretch=1)
header_row.addWidget(refresh_button)
layout.addLayout(header_row)
self._calibration_combo.currentTextChanged.connect(self._emit_selection_changed)
self._reference_combo.currentTextChanged.connect(self._emit_selection_changed)
layout.addWidget(self._build_selector_group("S21", S21_PREPROCESS_ASSET_KEYS, group))
layout.addWidget(self._build_selector_group("S11", S11_PREPROCESS_ASSET_KEYS, group))
return group
layout.addWidget(QLabel("Set name"), 0, 0)
layout.addWidget(self._set_name_input, 0, 1)
layout.addWidget(refresh_button, 0, 2)
def _build_selector_group(self, title: str, keys: tuple[str, ...], parent: QGroupBox) -> QGroupBox:
"""Build one selector subgroup for a channel family."""
group = QGroupBox(title, parent)
form = QFormLayout(group)
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
layout.addWidget(QLabel("Calibration set"), 1, 0)
layout.addWidget(self._calibration_combo, 1, 1, 1, 2)
layout.addWidget(QLabel("Reference set"), 2, 0)
layout.addWidget(self._reference_combo, 2, 1, 1, 2)
for key in keys:
combo = QComboBox(group)
combo.currentTextChanged.connect(self._emit_selection_changed)
self._set_combos[key] = combo
form.addRow(self._asset_row_label(key), combo)
return group
def _build_sequence_group(self) -> QGroupBox:
@@ -95,8 +103,6 @@ class PreprocessDialog(QDialog):
self._tx_antenna_label_input.setPlaceholderText("e.g. TX_A")
self._rx_antenna_label_input.setPlaceholderText("e.g. RX_B")
button_row = self._build_sequence_button_row(group)
layout.addWidget(QLabel("Active type"), 0, 0)
layout.addWidget(self._active_kind_label, 0, 1)
layout.addWidget(QLabel("Progress"), 1, 0)
@@ -107,22 +113,26 @@ class PreprocessDialog(QDialog):
layout.addWidget(self._tx_antenna_label_input, 3, 1)
layout.addWidget(QLabel("RX antenna label"), 4, 0)
layout.addWidget(self._rx_antenna_label_input, 4, 1)
layout.addLayout(button_row, 5, 0, 1, 2)
layout.addLayout(self._build_sequence_button_grid(group), 5, 0, 1, 2)
layout.addLayout(self._build_sequence_action_row(group), 6, 0, 1, 2)
self._capture_log = QPlainTextEdit(group)
self._capture_log.setReadOnly(True)
self._capture_log.setPlaceholderText("Capture history per combo")
layout.addWidget(self._capture_log, 6, 0, 1, 2)
layout.addWidget(self._capture_log, 7, 0, 1, 2)
return group
def _build_sequence_button_row(self, parent: QGroupBox) -> QHBoxLayout:
"""Build action buttons for sequence flow control."""
start_calibration_button = QPushButton("Start Calibration Sequence", parent)
start_calibration_button.clicked.connect(lambda: self.start_sequence_requested.emit("calibration"))
start_reference_button = QPushButton("Start Reference Sequence", parent)
start_reference_button.clicked.connect(lambda: self.start_sequence_requested.emit("reference"))
def _build_sequence_button_grid(self, parent: QGroupBox) -> QGridLayout:
"""Build per-asset capture start buttons."""
layout = QGridLayout()
for index, key in enumerate(PREPROCESS_ASSET_KEYS):
button = QPushButton(f"Start {preprocess_asset_display_name(key)}", parent)
button.clicked.connect(lambda _checked=False, asset_key=key: self.start_sequence_requested.emit(asset_key))
layout.addWidget(button, index // 2, index % 2)
return layout
def _build_sequence_action_row(self, parent: QGroupBox) -> QHBoxLayout:
"""Build capture/abort row for active sequence control."""
self._capture_next_button = QPushButton("Capture Current Combo", parent)
self._capture_next_button.clicked.connect(self.capture_next_requested.emit)
self._capture_next_button.setEnabled(False)
@@ -131,12 +141,11 @@ class PreprocessDialog(QDialog):
self._abort_button.clicked.connect(self.abort_sequence_requested.emit)
self._abort_button.setEnabled(False)
button_row = QHBoxLayout()
button_row.addWidget(start_calibration_button)
button_row.addWidget(start_reference_button)
button_row.addWidget(self._capture_next_button)
button_row.addWidget(self._abort_button)
return button_row
layout = QHBoxLayout()
layout.addWidget(self._capture_next_button)
layout.addWidget(self._abort_button)
layout.addStretch(1)
return layout
def _build_status_line(self, root_layout: QVBoxLayout) -> None:
"""Build one-line status output for dialog operations."""
@@ -155,13 +164,9 @@ class PreprocessDialog(QDialog):
"""Return requested target set name."""
return self._set_name_input.text().strip()
def calibration_set(self) -> str:
"""Return currently selected calibration set."""
return self._calibration_combo.currentText().strip()
def reference_set(self) -> str:
"""Return currently selected reference set."""
return self._reference_combo.currentText().strip()
def selection_snapshot(self) -> dict[str, str]:
"""Return currently selected set names keyed by preprocess asset key."""
return {key: self._set_combos[key].currentText().strip() for key in PREPROCESS_ASSET_KEYS}
def antenna_labels(self) -> tuple[str, str]:
"""Return optional TX/RX user labels used in capture logs."""
@@ -209,7 +214,8 @@ class PreprocessDialog(QDialog):
self._abort_button.setEnabled(False)
return
self._active_kind_label.setText(kind)
active_label = preprocess_asset_display_name(kind) if kind in PREPROCESS_ASSET_SPECS else kind
self._active_kind_label.setText(active_label)
self._progress_label.setText(f"{captured_count} / {total_count}")
if next_input is None or next_output is None:
self._combo_label.setText("<complete>")
@@ -220,35 +226,32 @@ class PreprocessDialog(QDialog):
self._capture_next_button.setEnabled(True)
self._abort_button.setEnabled(True)
def set_calibration_sets(self, names: list[str]) -> None:
"""Replace calibration set choices while preserving current selection when possible."""
self._set_combo_items(self._calibration_combo, names, self.calibration_set())
def set_available_sets(self, available_sets: dict[str, list[str]]) -> None:
"""Replace combo-box choices for all preprocess assets."""
for key in PREPROCESS_ASSET_KEYS:
combo = self._set_combos[key]
self._set_combo_items(combo, available_sets.get(key, []), combo.currentText().strip())
def set_reference_sets(self, names: list[str]) -> None:
"""Replace reference set choices while preserving current selection when possible."""
self._set_combo_items(self._reference_combo, names, self.reference_set())
def set_selected_sets(self, calibration_set: str, reference_set: str) -> None:
"""Apply selected set names to both comboboxes and emit selection update."""
if calibration_set:
index = self._calibration_combo.findText(calibration_set)
def set_selected_sets(self, selected_sets: dict[str, str]) -> None:
"""Apply selected set names to all comboboxes and emit selection update."""
for key in PREPROCESS_ASSET_KEYS:
selected_value = selected_sets.get(key, "")
if not selected_value:
continue
combo = self._set_combos[key]
index = combo.findText(selected_value)
if index >= 0:
self._calibration_combo.setCurrentIndex(index)
if reference_set:
index = self._reference_combo.findText(reference_set)
if index >= 0:
self._reference_combo.setCurrentIndex(index)
combo.setCurrentIndex(index)
self._emit_selection_changed()
def set_status(self, message: str) -> None:
"""Set short human-readable status line."""
self._status_label.setText(message)
def draw_last_trace(self, trace: TraceData, title: str) -> None:
"""Draw the latest captured sweep trace in dB scale."""
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(trace.s21), 1e-12))
def draw_last_trace(self, trace: TraceData, title: str, *, channel: str) -> None:
"""Draw the latest captured sweep trace for the requested channel in dB scale."""
samples = trace.s11 if channel == "s11" else trace.s21
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
self._preview_plot.clear()
self._preview_plot.plot(
trace.frequency_hz,
@@ -261,8 +264,13 @@ class PreprocessDialog(QDialog):
)
def _emit_selection_changed(self) -> None:
"""Emit current calibration/reference selection."""
self.selection_changed.emit(self.calibration_set(), self.reference_set())
"""Emit current selection snapshot change."""
self.selection_changed.emit()
@staticmethod
def _asset_row_label(key: str) -> str:
"""Return short selector label for one preprocess asset."""
return preprocess_asset_display_name(key)
@staticmethod
def _set_combo_items(combo: QComboBox, names: list[str], current_text: str) -> None:
+3 -2
View File
@@ -7,6 +7,7 @@ from typing import TypeVar
from python_app.models.dataset_model import ResultCollection, SweepCollection
from python_app.models.run_config_model import RunConfigModel
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model
THistoryCollection = TypeVar("THistoryCollection", SweepCollection, ResultCollection)
@@ -63,6 +64,7 @@ def build_run_history_signature(
) -> tuple[object, ...]:
"""Build deterministic signature to detect run-settings changes (excluding live processing params)."""
combos_signature = tuple((int(combo.input), int(combo.output)) for combo in config.combos)
preprocess_signature = tuple(preprocess_asset_model(config, key).set_name for key in PREPROCESS_ASSET_KEYS)
return (
str(config.radar.driver_mode),
str(config.radar.serial),
@@ -79,8 +81,7 @@ def build_run_history_signature(
str(config.output_switch.driver),
int(config.output_switch.positions),
bool(config.output_switch.invert_logic),
str(config.preprocess.s21_calibration_set),
str(config.preprocess.s21_reference_set),
preprocess_signature,
combos_signature,
)
+28 -10
View File
@@ -8,6 +8,7 @@ from typing import Any, Protocol
import numpy as np
from python_app.hardware_full.librevna_driver.models import SweepResult
from python_app.models.run_config_model import RadarSweepModel
@@ -30,8 +31,8 @@ class LibreVnaBackend(Protocol):
def read_device_limits(self) -> dict[str, float | int]:
"""Query runtime device limits."""
def acquire_s21(self) -> tuple[np.ndarray, np.ndarray]:
"""Acquire one S21 trace."""
def acquire(self) -> SweepResult:
"""Acquire one sweep with all available traces."""
@dataclass(slots=True)
@@ -108,15 +109,21 @@ class NativeLibreVnaBackend:
"max_power_dbm": float(limits.max_power_dbm),
}
def acquire_s21(self) -> tuple[np.ndarray, np.ndarray]:
"""Acquire one S21 sweep from hardware."""
def acquire(self) -> SweepResult:
"""Acquire one sweep from hardware."""
if self._settings is None:
raise RuntimeError("Radar service is not configured")
if self._device is None:
raise RuntimeError("Device not found")
result = self._device.vna.acquire(expected_points=self._settings.points, timeout_s=20.0)
return np.asarray(result.x, dtype=np.float32), np.asarray(result.trace("s21"), dtype=np.complex64)
return SweepResult(
x=np.asarray(result.x, dtype=np.float32),
traces={
"s11": np.asarray(result.trace("s11"), dtype=np.complex64),
"s21": np.asarray(result.trace("s21"), dtype=np.complex64),
},
)
@dataclass(slots=True)
@@ -149,15 +156,26 @@ class MockLibreVnaBackend:
"""Mock backend does not support native device limits queries."""
raise RuntimeError("LibreVNA Python driver is not available")
def acquire_s21(self) -> tuple[np.ndarray, np.ndarray]:
"""Generate synthetic S21 values using deterministic phase envelope."""
def acquire(self) -> SweepResult:
"""Generate synthetic S11 and S21 values using deterministic envelopes."""
if self._settings is None:
raise RuntimeError("Radar service is not configured")
points = self._settings.points
freq = np.linspace(self._settings.start_hz, self._settings.stop_hz, points, dtype=np.float32)
phase = (2.0 * math.pi * np.linspace(0.0, 1.0, points, dtype=np.float32)) + self._mock_phase
envelope = 0.6 + 0.4 * np.sin(phase * 0.5)
s21 = (envelope * np.cos(phase) + 1j * envelope * np.sin(phase)).astype(np.complex64)
s21_envelope = 0.6 + 0.4 * np.sin(phase * 0.5)
s11_envelope = 0.25 + 0.15 * np.cos(phase * 0.75)
s21 = (s21_envelope * np.cos(phase) + 1j * s21_envelope * np.sin(phase)).astype(np.complex64)
reflected_phase = phase * 0.6 + 0.8
s11 = (
s11_envelope * np.cos(reflected_phase) + 1j * s11_envelope * np.sin(reflected_phase)
).astype(np.complex64)
self._mock_phase += 0.05
return freq, s21
return SweepResult(
x=freq,
traces={
"s11": s11,
"s21": s21,
},
)
+4 -5
View File
@@ -4,9 +4,8 @@ from __future__ import annotations
from dataclasses import dataclass, field
import numpy as np
from python_app.hardware_full.librevna_backends import LibreVnaBackend, MockLibreVnaBackend, NativeLibreVnaBackend
from python_app.hardware_full.librevna_driver.models import SweepResult
from python_app.models.run_config_model import RadarSweepModel
@@ -89,10 +88,10 @@ class LibreVnaService:
if opened_here:
self.close()
def acquire_s21(self) -> tuple[np.ndarray, np.ndarray]:
"""Acquire one S21 trace from currently selected backend."""
def acquire(self) -> SweepResult:
"""Acquire one sweep with all available traces from active backend."""
if self._backend is None:
raise RuntimeError("LibreVNA backend is not initialized")
if self._using_mock_backend and not self._driver_available and self.backend_mode != "mock":
raise RuntimeError("Device not found")
return self._backend.acquire_s21()
return self._backend.acquire()
+2 -1
View File
@@ -32,10 +32,11 @@ class ComboKey:
@dataclass(slots=True)
class TraceData:
"""One frequency-domain S21 trace for a specific switch combination."""
"""One frequency-domain trace set for a specific switch combination."""
combo: ComboKey
frequency_hz: np.ndarray
s11: np.ndarray
s21: np.ndarray
+59 -44
View File
@@ -8,6 +8,7 @@ from python_app.models.run_config_schema import (
ComboModel,
GprRxGeometryModel,
GprTxGeometryModel,
PreprocessAssetModel,
RunConfigModel,
)
from python_app.models.run_config_validation import load_ring_payload, load_switch_payload, validate_gpr_model
@@ -22,6 +23,12 @@ def _as_dict(value: Any, context: str) -> dict[str, Any]:
return value
def _load_preprocess_asset(payload: dict[str, Any], target: PreprocessAssetModel) -> None:
"""Load preprocess asset fields into target model."""
target.set_name = str(payload.get("set_name", target.set_name))
target.bundle_path = str(payload.get("bundle_path", target.bundle_path))
def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
"""Decode JSON-like payload into :class:`RunConfigModel`."""
# Schema carries only minimal-safe fallbacks; operational defaults live in run_config.json.
@@ -65,47 +72,33 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
run_payload.get("processing_live_config_path", model.runtime.processing_live_config_path)
)
model.preprocess.s21_calibration_set = str(
preprocess_payload.get("s21_calibration_set", model.preprocess.s21_calibration_set)
s21_preprocess_payload = _as_dict(preprocess_payload.get("s21"), "preprocess.s21")
_load_preprocess_asset(
_as_dict(s21_preprocess_payload.get("calibration"), "preprocess.s21.calibration"),
model.preprocess.s21.calibration,
)
model.preprocess.s21_reference_set = str(
preprocess_payload.get("s21_reference_set", model.preprocess.s21_reference_set)
_load_preprocess_asset(
_as_dict(s21_preprocess_payload.get("reference"), "preprocess.s21.reference"),
model.preprocess.s21.reference,
)
model.preprocess.s21_calibration_bundle_path = str(
preprocess_payload.get(
"s21_calibration_bundle_path",
model.preprocess.s21_calibration_bundle_path,
)
s11_preprocess_payload = _as_dict(preprocess_payload.get("s11"), "preprocess.s11")
s11_calibration_payload = _as_dict(s11_preprocess_payload.get("calibration"), "preprocess.s11.calibration")
_load_preprocess_asset(
_as_dict(s11_calibration_payload.get("open"), "preprocess.s11.calibration.open"),
model.preprocess.s11.calibration.open,
)
model.preprocess.s21_reference_bundle_path = str(
preprocess_payload.get(
"s21_reference_bundle_path",
model.preprocess.s21_reference_bundle_path,
)
_load_preprocess_asset(
_as_dict(s11_calibration_payload.get("short"), "preprocess.s11.calibration.short"),
model.preprocess.s11.calibration.short,
)
model.preprocess.s11_open_calibration_bundle_path = str(
preprocess_payload.get(
"s11_open_calibration_bundle_path",
model.preprocess.s11_open_calibration_bundle_path,
)
_load_preprocess_asset(
_as_dict(s11_calibration_payload.get("load"), "preprocess.s11.calibration.load"),
model.preprocess.s11.calibration.load,
)
model.preprocess.s11_short_calibration_bundle_path = str(
preprocess_payload.get(
"s11_short_calibration_bundle_path",
model.preprocess.s11_short_calibration_bundle_path,
)
)
model.preprocess.s11_load_calibration_bundle_path = str(
preprocess_payload.get(
"s11_load_calibration_bundle_path",
model.preprocess.s11_load_calibration_bundle_path,
)
)
model.preprocess.s11_reference_bundle_path = str(
preprocess_payload.get(
"s11_reference_bundle_path",
model.preprocess.s11_reference_bundle_path,
)
_load_preprocess_asset(
_as_dict(s11_preprocess_payload.get("reference"), "preprocess.s11.reference"),
model.preprocess.s11.reference,
)
model.gpr.mode = str(gpr_payload.get("mode", model.gpr.mode))
@@ -213,14 +206,36 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
"combos": [{"input": combo.input, "output": combo.output} for combo in model.combos],
},
"preprocess": {
"s21_calibration_set": model.preprocess.s21_calibration_set,
"s21_reference_set": model.preprocess.s21_reference_set,
"s21_calibration_bundle_path": model.preprocess.s21_calibration_bundle_path,
"s21_reference_bundle_path": model.preprocess.s21_reference_bundle_path,
"s11_open_calibration_bundle_path": model.preprocess.s11_open_calibration_bundle_path,
"s11_short_calibration_bundle_path": model.preprocess.s11_short_calibration_bundle_path,
"s11_load_calibration_bundle_path": model.preprocess.s11_load_calibration_bundle_path,
"s11_reference_bundle_path": model.preprocess.s11_reference_bundle_path,
"s21": {
"calibration": {
"set_name": model.preprocess.s21.calibration.set_name,
"bundle_path": model.preprocess.s21.calibration.bundle_path,
},
"reference": {
"set_name": model.preprocess.s21.reference.set_name,
"bundle_path": model.preprocess.s21.reference.bundle_path,
},
},
"s11": {
"calibration": {
"open": {
"set_name": model.preprocess.s11.calibration.open.set_name,
"bundle_path": model.preprocess.s11.calibration.open.bundle_path,
},
"short": {
"set_name": model.preprocess.s11.calibration.short.set_name,
"bundle_path": model.preprocess.s11.calibration.short.bundle_path,
},
"load": {
"set_name": model.preprocess.s11.calibration.load.set_name,
"bundle_path": model.preprocess.s11.calibration.load.bundle_path,
},
},
"reference": {
"set_name": model.preprocess.s11.reference.set_name,
"bundle_path": model.preprocess.s11.reference.bundle_path,
},
},
},
"gpr": {
"mode": model.gpr.mode,
+8
View File
@@ -6,6 +6,7 @@ from python_app.models.run_config_schema import (
GprModel,
GprRxGeometryModel,
GprTxGeometryModel,
PreprocessAssetModel,
PreprocessModel,
RadarModel,
RadarSweepModel,
@@ -13,6 +14,9 @@ from python_app.models.run_config_schema import (
RingsModel,
RunConfigModel,
RuntimeModel,
S11CalibrationModel,
S11PreprocessModel,
S21PreprocessModel,
SwitchModel,
)
from python_app.models.run_config_validation import (
@@ -26,6 +30,7 @@ __all__ = [
"GprModel",
"GprRxGeometryModel",
"GprTxGeometryModel",
"PreprocessAssetModel",
"PreprocessModel",
"RadarModel",
"RadarSweepModel",
@@ -33,6 +38,9 @@ __all__ = [
"RingsModel",
"RunConfigModel",
"RuntimeModel",
"S11CalibrationModel",
"S11PreprocessModel",
"S21PreprocessModel",
"SwitchModel",
"load_ring_payload",
"load_switch_payload",
+35 -8
View File
@@ -85,18 +85,45 @@ class RuntimeModel:
processing_live_config_path: str = ""
@dataclass(slots=True)
class PreprocessAssetModel:
"""One preprocessing asset selected for live acquisition."""
set_name: str = ""
bundle_path: str = ""
@dataclass(slots=True)
class S21PreprocessModel:
"""Two-port S21 preprocessing assets."""
calibration: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
reference: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
@dataclass(slots=True)
class S11CalibrationModel:
"""One-port S11 OSL calibration assets."""
open: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
short: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
load: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
@dataclass(slots=True)
class S11PreprocessModel:
"""One-port S11 preprocessing assets."""
calibration: S11CalibrationModel = field(default_factory=S11CalibrationModel)
reference: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
@dataclass(slots=True)
class PreprocessModel:
"""Selected preprocessing artifacts for live acquisition."""
s21_calibration_set: str = ""
s21_reference_set: str = ""
s21_calibration_bundle_path: str = ""
s21_reference_bundle_path: str = ""
s11_open_calibration_bundle_path: str = ""
s11_short_calibration_bundle_path: str = ""
s11_load_calibration_bundle_path: str = ""
s11_reference_bundle_path: str = ""
s21: S21PreprocessModel = field(default_factory=S21PreprocessModel)
s11: S11PreprocessModel = field(default_factory=S11PreprocessModel)
@dataclass(slots=True)
+11 -11
View File
@@ -6,6 +6,7 @@ import json
from pathlib import Path
from python_app.models.run_config_model import RunConfigModel, parse_combos_from_text
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, PREPROCESS_ASSET_SPECS, preprocess_asset_model
from python_app.storage.npz_store import NpzStore
@@ -17,20 +18,19 @@ class ConfigWriter:
self._runtime_dir = runtime_dir
self._runtime_dir.mkdir(parents=True, exist_ok=True)
def prepare_s21_bundles(
def prepare_preprocess_bundles(
self,
store: NpzStore,
radar_key: str,
s21_calibration_set: str,
s21_reference_set: str,
) -> tuple[Path, Path]:
"""Export calibration/reference sets into binary bundles for preprocessor."""
calibration_bundle = self._runtime_dir / "s21_calibration_bundle.bin"
reference_bundle = self._runtime_dir / "s21_reference_bundle.bin"
store.export_set_bundle("calibration", radar_key, s21_calibration_set, calibration_bundle)
store.export_set_bundle("reference", radar_key, s21_reference_set, reference_bundle)
return calibration_bundle, reference_bundle
config: RunConfigModel,
) -> None:
"""Export selected preprocess sets into runtime bundles and update config paths."""
for key in PREPROCESS_ASSET_KEYS:
spec = PREPROCESS_ASSET_SPECS[key]
asset = preprocess_asset_model(config, key)
bundle_path = self._runtime_dir / spec.runtime_filename
store.export_set_bundle(spec.set_kind, radar_key, asset.set_name, bundle_path)
asset.bundle_path = str(bundle_path)
def write(self, config: RunConfigModel, output_path: Path) -> Path:
"""Write run configuration JSON file."""
@@ -19,6 +19,7 @@ class ProcessingLiveConfig:
pass_through_y_min_db: float = -100.0
pass_through_y_max_db: float = 0.0
bscan_axis: str = "abs"
bscan_channel: str = "s21"
bscan_cut_m: float = 0.824
bscan_max_depth_m: float = 1.0
bscan_gain: float = 1.0
@@ -58,6 +59,7 @@ class ProcessingLiveConfig:
"pass_through_y_min_db": float(self.pass_through_y_min_db),
"pass_through_y_max_db": float(self.pass_through_y_max_db),
"bscan_axis": str(self.bscan_axis),
"bscan_channel": str(self.bscan_channel),
"bscan_cut_m": float(self.bscan_cut_m),
"bscan_max_depth_m": float(self.bscan_max_depth_m),
"bscan_gain": float(self.bscan_gain),
@@ -0,0 +1,95 @@
"""Canonical preprocess asset definitions shared by GUI and runtime orchestration."""
from __future__ import annotations
from dataclasses import dataclass
from python_app.models.run_config_model import PreprocessAssetModel, RunConfigModel
@dataclass(frozen=True, slots=True)
class PreprocessAssetSpec:
"""Stable description of one preprocess asset."""
key: str
display_name: str
set_kind: str
runtime_filename: str
channel: str
PREPROCESS_ASSET_SPECS = {
"s21_calibration": PreprocessAssetSpec(
key="s21_calibration",
display_name="S21 Calibration",
set_kind="s21_calibration",
runtime_filename="s21_calibration_bundle.bin",
channel="s21",
),
"s21_reference": PreprocessAssetSpec(
key="s21_reference",
display_name="S21 Reference",
set_kind="s21_reference",
runtime_filename="s21_reference_bundle.bin",
channel="s21",
),
"s11_open": PreprocessAssetSpec(
key="s11_open",
display_name="S11 Open",
set_kind="s11_open",
runtime_filename="s11_open_calibration_bundle.bin",
channel="s11",
),
"s11_short": PreprocessAssetSpec(
key="s11_short",
display_name="S11 Short",
set_kind="s11_short",
runtime_filename="s11_short_calibration_bundle.bin",
channel="s11",
),
"s11_load": PreprocessAssetSpec(
key="s11_load",
display_name="S11 Load",
set_kind="s11_load",
runtime_filename="s11_load_calibration_bundle.bin",
channel="s11",
),
"s11_reference": PreprocessAssetSpec(
key="s11_reference",
display_name="S11 Reference",
set_kind="s11_reference",
runtime_filename="s11_reference_bundle.bin",
channel="s11",
),
}
PREPROCESS_ASSET_KEYS = tuple(PREPROCESS_ASSET_SPECS.keys())
S21_PREPROCESS_ASSET_KEYS = ("s21_calibration", "s21_reference")
S11_PREPROCESS_ASSET_KEYS = ("s11_open", "s11_short", "s11_load", "s11_reference")
def preprocess_asset_model(config: RunConfigModel, key: str) -> PreprocessAssetModel:
"""Return nested preprocess asset model by canonical asset key."""
if key == "s21_calibration":
return config.preprocess.s21.calibration
if key == "s21_reference":
return config.preprocess.s21.reference
if key == "s11_open":
return config.preprocess.s11.calibration.open
if key == "s11_short":
return config.preprocess.s11.calibration.short
if key == "s11_load":
return config.preprocess.s11.calibration.load
if key == "s11_reference":
return config.preprocess.s11.reference
raise KeyError(f"Unknown preprocess asset key: {key}")
def preprocess_asset_display_name(key: str) -> str:
"""Return human-readable label for preprocess asset."""
return PREPROCESS_ASSET_SPECS[key].display_name
def preprocess_asset_channel(key: str) -> str:
"""Return associated trace channel for preprocess asset."""
return PREPROCESS_ASSET_SPECS[key].channel
+5 -5
View File
@@ -40,16 +40,16 @@ def decode_trace_collection(payload: bytes, expected_magic: int) -> SweepCollect
freq = np.frombuffer(cursor.read_bytes(freq_bytes), dtype="<f4").astype(np.float32, copy=False)
interleaved_bytes = point_count * 8
# Runtime trace payloads now carry S11 before S21. The Python layer
# still works with S21 only for now, so consume and discard S11 here.
cursor.read_bytes(interleaved_bytes)
interleaved = np.frombuffer(cursor.read_bytes(interleaved_bytes), dtype="<f4")
s21 = (interleaved[0::2] + 1j * interleaved[1::2]).astype(np.complex64, copy=False)
s11_interleaved = np.frombuffer(cursor.read_bytes(interleaved_bytes), dtype="<f4")
s21_interleaved = np.frombuffer(cursor.read_bytes(interleaved_bytes), dtype="<f4")
s11 = (s11_interleaved[0::2] + 1j * s11_interleaved[1::2]).astype(np.complex64, copy=False)
s21 = (s21_interleaved[0::2] + 1j * s21_interleaved[1::2]).astype(np.complex64, copy=False)
traces.append(
TraceData(
combo=ComboKey(input_pos=input_pos, output_pos=output_pos),
frequency_hz=freq,
s11=s11,
s21=s21,
)
)
+72 -18
View File
@@ -60,6 +60,38 @@
"input": 3,
"output": 0
},
{
"input": 0,
"output": 1
},
{
"input": 1,
"output": 1
},
{
"input": 2,
"output": 1
},
{
"input": 3,
"output": 1
},
{
"input": 0,
"output": 2
},
{
"input": 1,
"output": 2
},
{
"input": 2,
"output": 2
},
{
"input": 3,
"output": 2
},
{
"input": 0,
"output": 3
@@ -79,14 +111,36 @@
]
},
"preprocess": {
"s21_calibration_set": "smoke_cal",
"s21_reference_set": "smoke_ref",
"s21_calibration_bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_calibration_bundle.bin",
"s21_reference_bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_reference_bundle.bin",
"s11_open_calibration_bundle_path": "/home/europa/Documents/radar_system/data_acq_and_processing/preprocessing/testdata/s11_open_calibration_bundle.bin",
"s11_short_calibration_bundle_path": "/home/europa/Documents/radar_system/data_acq_and_processing/preprocessing/testdata/s11_short_calibration_bundle.bin",
"s11_load_calibration_bundle_path": "/home/europa/Documents/radar_system/data_acq_and_processing/preprocessing/testdata/s11_load_calibration_bundle.bin",
"s11_reference_bundle_path": "/home/europa/Documents/radar_system/data_acq_and_processing/preprocessing/testdata/s11_reference_bundle.bin"
"s21": {
"calibration": {
"set_name": "smoke_cal",
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_calibration_bundle.bin"
},
"reference": {
"set_name": "smoke_ref",
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_reference_bundle.bin"
}
},
"s11": {
"calibration": {
"open": {
"set_name": "smoke_open",
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s11_open_calibration_bundle.bin"
},
"short": {
"set_name": "smoke_short",
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s11_short_calibration_bundle.bin"
},
"load": {
"set_name": "smoke_load",
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s11_load_calibration_bundle.bin"
}
},
"reference": {
"set_name": "smoke_s11_ref",
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s11_reference_bundle.bin"
}
}
},
"gpr": {
"mode": "point",
@@ -122,28 +176,28 @@
},
"rings": {
"raw": {
"name": "/radar_raw_smoke_1703912_791574940686872",
"capacity": 32,
"name": "/radar_raw_smoke_2_20242576200473",
"capacity": 50,
"slot_size_bytes": 2097152
},
"raw_tap": {
"name": "/radar_raw_tap_smoke_1703912_791574940693657",
"capacity": 32,
"name": "/radar_raw_tap_smoke_2_20242576209903",
"capacity": 50,
"slot_size_bytes": 2097152
},
"preprocessed": {
"name": "/radar_preprocessed_smoke_1703912_791574940694423",
"capacity": 32,
"name": "/radar_preprocessed_smoke_2_20242576212225",
"capacity": 50,
"slot_size_bytes": 2097152
},
"preprocessed_tap": {
"name": "/radar_preprocessed_tap_smoke_1703912_791574940694970",
"capacity": 32,
"name": "/radar_preprocessed_tap_smoke_2_20242576213549",
"capacity": 50,
"slot_size_bytes": 2097152
},
"results": {
"name": "/radar_results_smoke_1703912_791574940696951",
"capacity": 32,
"name": "/radar_results_smoke_2_20242576214682",
"capacity": 50,
"slot_size_bytes": 2097152
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11 -1
View File
@@ -46,12 +46,22 @@ def _all_traces_from_raw_or_pre(collection_dir: Path) -> list[tuple[np.ndarray,
raise ValueError(f"Invalid trace record in {collection_dir / 'meta.json'}")
freq_file = str(trace_meta.get("freq_file", ""))
s11_file = str(trace_meta.get("s11_file", ""))
s21_file = str(trace_meta.get("s21_file", ""))
freq = np.load(collection_dir / freq_file)
s11 = np.load(collection_dir / s11_file)
s21 = np.load(collection_dir / s21_file)
if freq.shape != s11.shape:
raise ValueError(f"Shape mismatch freq/s11 in {collection_dir}")
if freq.shape != s21.shape:
raise ValueError(f"Shape mismatch freq/s21 in {collection_dir}")
if not (np.isfinite(freq).all() and np.isfinite(np.real(s21)).all() and np.isfinite(np.imag(s21)).all()):
if not (
np.isfinite(freq).all()
and np.isfinite(np.real(s11)).all()
and np.isfinite(np.imag(s11)).all()
and np.isfinite(np.real(s21)).all()
and np.isfinite(np.imag(s21)).all()
):
raise ValueError(f"Non-finite values in {collection_dir}")
label = f"i{int(trace_meta.get('input', 0))}_o{int(trace_meta.get('output', 0))}"
+29 -15
View File
@@ -49,7 +49,13 @@ def _shm_unlink(name: str) -> None:
raise OSError(err, f"shm_unlink failed for {name}")
def build_synthetic_collection(config: RunConfigModel, value_scale: float) -> SweepCollection:
def build_synthetic_collection(
config: RunConfigModel,
value_scale: float,
*,
s11_scale: float,
s11_phase_offset: float,
) -> SweepCollection:
"""Build synthetic sweep collection for all configured switch combos."""
traces: list[TraceData] = []
combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
@@ -62,12 +68,15 @@ def build_synthetic_collection(config: RunConfigModel, value_scale: float) -> Sw
dtype=np.float32,
)
phase = np.linspace(0.0, np.pi * 2.0, config.radar.sweep.points, dtype=np.float32)
reflected_phase = (phase * 0.6) + s11_phase_offset
s11 = (s11_scale * (np.cos(reflected_phase) + 1j * np.sin(reflected_phase))).astype(np.complex64)
s21 = value_scale * (np.cos(phase) + 1j * np.sin(phase)).astype(np.complex64)
traces.append(
TraceData(
combo=ComboKey(input_pos=combo.input, output_pos=combo.output),
frequency_hz=frequency_hz,
s11=s11,
s21=s21,
)
)
@@ -107,22 +116,27 @@ def main() -> int:
power_dbm=config.radar.sweep.power_dbm,
)
calibration_set = build_synthetic_collection(config, value_scale=1.0)
reference_set = build_synthetic_collection(config, value_scale=0.3)
s21_calibration_set = build_synthetic_collection(config, value_scale=1.0, s11_scale=0.15, s11_phase_offset=0.4)
s21_reference_set = build_synthetic_collection(config, value_scale=0.3, s11_scale=0.08, s11_phase_offset=0.9)
s11_open_set = build_synthetic_collection(config, value_scale=0.85, s11_scale=0.95, s11_phase_offset=0.1)
s11_short_set = build_synthetic_collection(config, value_scale=0.85, s11_scale=0.95, s11_phase_offset=3.2)
s11_load_set = build_synthetic_collection(config, value_scale=0.85, s11_scale=0.05, s11_phase_offset=1.4)
s11_reference_set = build_synthetic_collection(config, value_scale=0.3, s11_scale=0.18, s11_phase_offset=1.1)
store.save_set("calibration", radar_key, "smoke_cal", calibration_set)
store.save_set("reference", radar_key, "smoke_ref", reference_set)
store.save_set("s21_calibration", radar_key, "smoke_cal", s21_calibration_set)
store.save_set("s21_reference", radar_key, "smoke_ref", s21_reference_set)
store.save_set("s11_open", radar_key, "smoke_open", s11_open_set)
store.save_set("s11_short", radar_key, "smoke_short", s11_short_set)
store.save_set("s11_load", radar_key, "smoke_load", s11_load_set)
store.save_set("s11_reference", radar_key, "smoke_s11_ref", s11_reference_set)
calibration_bundle, reference_bundle = config_writer.prepare_s21_bundles(
store,
radar_key,
"smoke_cal",
"smoke_ref",
)
config.preprocess.s21_calibration_set = "smoke_cal"
config.preprocess.s21_reference_set = "smoke_ref"
config.preprocess.s21_calibration_bundle_path = str(calibration_bundle)
config.preprocess.s21_reference_bundle_path = str(reference_bundle)
config.preprocess.s21.calibration.set_name = "smoke_cal"
config.preprocess.s21.reference.set_name = "smoke_ref"
config.preprocess.s11.calibration.open.set_name = "smoke_open"
config.preprocess.s11.calibration.short.set_name = "smoke_short"
config.preprocess.s11.calibration.load.set_name = "smoke_load"
config.preprocess.s11.reference.set_name = "smoke_s11_ref"
config_writer.prepare_preprocess_bundles(store, radar_key, config)
config_path = config_writer.write(config, project_root / "python_app/runtime/run_config_smoke.json")
+3 -4
View File
@@ -28,13 +28,12 @@ def serialize_trace_collection(collection: SweepCollection, magic: int) -> bytes
for trace in collection.traces:
freq = np.asarray(trace.frequency_hz, dtype=np.float32)
s11 = np.asarray(trace.s11, dtype=np.complex64)
s21 = np.asarray(trace.s21, dtype=np.complex64)
if freq.size != s11.size:
raise ValueError("Trace frequency and S11 sizes must match")
if freq.size != s21.size:
raise ValueError("Trace frequency and S21 sizes must match")
# Python workflows still operate on S21 only. Emit a zero-filled S11
# channel so C++ trace bundles keep the same wire format as runtime
# rings while the Python layer remains unchanged.
s11 = np.zeros(freq.size, dtype=np.complex64)
buffer.extend(struct.pack("<III", trace.combo.input_pos, trace.combo.output_pos, int(freq.size)))
buffer.extend(freq.astype("<f4", copy=False).tobytes())
+3
View File
@@ -157,8 +157,10 @@ def save_trace_history_numpy(stage_dir: Path, history: list[SweepCollection]) ->
for trace in collection.traces:
tag = f"i{trace.combo.input_pos}_o{trace.combo.output_pos}"
freq = np.asarray(trace.frequency_hz, dtype=np.float32)
s11 = np.asarray(trace.s11, dtype=np.complex64)
s21 = np.asarray(trace.s21, dtype=np.complex64)
np.save(collection_dir / f"{tag}_freq.npy", freq)
np.save(collection_dir / f"{tag}_s11.npy", s11)
np.save(collection_dir / f"{tag}_s21.npy", s21)
traces_meta.append(
{
@@ -166,6 +168,7 @@ def save_trace_history_numpy(stage_dir: Path, history: list[SweepCollection]) ->
"output": int(trace.combo.output_pos),
"points": int(freq.size),
"freq_file": f"{tag}_freq.npy",
"s11_file": f"{tag}_s11.npy",
"s21_file": f"{tag}_s21.npy",
}
)
+8 -3
View File
@@ -24,7 +24,7 @@ from python_app.storage.store_api import StoreApi
class NpzStore(StoreApi):
"""Persist calibration/reference sets and runtime snapshots using NumPy files."""
"""Persist preprocess sets and runtime snapshots using NumPy files."""
def __init__(self, root_dir: Path) -> None:
"""Create store rooted at `root_dir`."""
@@ -32,7 +32,7 @@ class NpzStore(StoreApi):
self._root_dir.mkdir(parents=True, exist_ok=True)
def save_set(self, kind: str, radar_key: str, set_name: str, collection: SweepCollection) -> None:
"""Persist named calibration/reference 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.mkdir(parents=True, exist_ok=True)
@@ -45,14 +45,17 @@ class NpzStore(StoreApi):
for trace in collection.traces:
suffix = f"i{trace.combo.input_pos}_o{trace.combo.output_pos}"
freq_key = f"freq_{suffix}"
s11_key = f"s11_{suffix}"
s21_key = f"s21_{suffix}"
payload[freq_key] = np.asarray(trace.frequency_hz, dtype=np.float32)
payload[s11_key] = np.asarray(trace.s11, dtype=np.complex64)
payload[s21_key] = np.asarray(trace.s21, dtype=np.complex64)
combo_records.append(
{
"input": trace.combo.input_pos,
"output": trace.combo.output_pos,
"freq_key": freq_key,
"s11_key": s11_key,
"s21_key": s21_key,
}
)
@@ -66,7 +69,7 @@ class NpzStore(StoreApi):
meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")
def load_set(self, kind: str, radar_key: str, set_name: str) -> SweepCollection:
"""Load named calibration/reference set from NPZ representation."""
"""Load named preprocess set from NPZ representation."""
set_dir = self._set_dir(kind, radar_key)
npz_path = set_dir / f"{set_name}.npz"
meta_path = set_dir / f"{set_name}.json"
@@ -80,11 +83,13 @@ class NpzStore(StoreApi):
traces: list[TraceData] = []
for combo in meta["combos"]:
freq = np.asarray(arrays[combo["freq_key"]], dtype=np.float32)
s11 = np.asarray(arrays[combo["s11_key"]], dtype=np.complex64)
s21 = np.asarray(arrays[combo["s21_key"]], dtype=np.complex64)
traces.append(
TraceData(
combo=ComboKey(input_pos=int(combo["input"]), output_pos=int(combo["output"])),
frequency_hz=freq,
s11=s11,
s21=s21,
)
)
+1 -1
View File
@@ -1,4 +1,4 @@
"""Abstract storage API for calibration/reference sets."""
"""Abstract storage API for preprocess sets."""
from __future__ import annotations
+5 -4
View File
@@ -54,12 +54,13 @@ def capture_calibration_set(
if config.runtime.settling_ms > 0:
time.sleep(config.runtime.settling_ms / 1000.0)
frequency_hz, s21 = radar.acquire_s21()
sweep = radar.acquire()
traces.append(
TraceData(
combo=ComboKey(input_pos=combo.input, output_pos=combo.output),
frequency_hz=frequency_hz,
s21=s21,
frequency_hz=sweep.x,
s11=sweep.trace("s11"),
s21=sweep.trace("s21"),
)
)
finally:
@@ -77,5 +78,5 @@ def capture_calibration_set(
ifbw_hz=config.radar.sweep.if_bandwidth_hz,
power_dbm=config.radar.sweep.power_dbm,
)
store.save_set("calibration", radar_key, set_name, collection)
store.save_set("s21_calibration", radar_key, set_name, collection)
return radar_key, collection
+5 -4
View File
@@ -54,12 +54,13 @@ def capture_reference_set(
if config.runtime.settling_ms > 0:
time.sleep(config.runtime.settling_ms / 1000.0)
frequency_hz, s21 = radar.acquire_s21()
sweep = radar.acquire()
traces.append(
TraceData(
combo=ComboKey(input_pos=combo.input, output_pos=combo.output),
frequency_hz=frequency_hz,
s21=s21,
frequency_hz=sweep.x,
s11=sweep.trace("s11"),
s21=sweep.trace("s21"),
)
)
finally:
@@ -77,5 +78,5 @@ def capture_reference_set(
ifbw_hz=config.radar.sweep.if_bandwidth_hz,
power_dbm=config.radar.sweep.power_dbm,
)
store.save_set("reference", radar_key, set_name, collection)
store.save_set("s21_reference", radar_key, set_name, collection)
return radar_key, collection
@@ -1,4 +1,4 @@
"""Sequential capture workflow for calibration/reference dataset creation."""
"""Sequential capture workflow for preprocess asset dataset creation."""
from __future__ import annotations
@@ -6,6 +6,8 @@ from contextlib import suppress
from dataclasses import dataclass
import time
import numpy as np
from python_app.hardware_full.librevna_service import LibreVnaService
from python_app.hardware_full.switch_service import SwitchService
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
@@ -28,8 +30,8 @@ class SequentialCaptureSession:
"""Manage hardware and switch stepping for full combo capture sequence."""
def __init__(self, config: RunConfigModel, kind: str, set_name: str) -> None:
"""Create capture session for a calibration or reference set."""
if kind not in {"calibration", "reference"}:
"""Create capture session for one preprocess asset set."""
if kind not in {"s21_calibration", "s21_reference", "s11_open", "s11_short", "s11_load", "s11_reference"}:
raise RuntimeError(f"Unsupported capture kind: {kind}")
if not set_name:
raise RuntimeError("Set name is required")
@@ -69,7 +71,7 @@ class SequentialCaptureSession:
@property
def kind(self) -> str:
"""Return capture kind (`calibration` or `reference`)."""
"""Return canonical preprocess asset key for this capture session."""
return self._kind
@property
@@ -125,11 +127,12 @@ class SequentialCaptureSession:
if self._config.runtime.settling_ms > 0:
time.sleep(self._config.runtime.settling_ms / 1000.0)
frequency_hz, s21 = self._radar.acquire_s21()
sweep = self._radar.acquire()
trace = TraceData(
combo=ComboKey(input_pos=combo.input, output_pos=combo.output),
frequency_hz=frequency_hz,
s21=s21,
frequency_hz=np.asarray(sweep.x, dtype=np.float32),
s11=np.asarray(sweep.trace("s11"), dtype=np.complex64),
s21=np.asarray(sweep.trace("s21"), dtype=np.complex64),
)
self._traces.append(trace)
self._next_index += 1
+30 -8
View File
@@ -55,14 +55,36 @@
]
},
"preprocess": {
"s21_calibration_set": "",
"s21_reference_set": "",
"s21_calibration_bundle_path": "python_app/runtime/s21_calibration_bundle.bin",
"s21_reference_bundle_path": "python_app/runtime/s21_reference_bundle.bin",
"s11_open_calibration_bundle_path": "",
"s11_short_calibration_bundle_path": "",
"s11_load_calibration_bundle_path": "",
"s11_reference_bundle_path": ""
"s21": {
"calibration": {
"set_name": "",
"bundle_path": ""
},
"reference": {
"set_name": "",
"bundle_path": ""
}
},
"s11": {
"calibration": {
"open": {
"set_name": "",
"bundle_path": ""
},
"short": {
"set_name": "",
"bundle_path": ""
},
"load": {
"set_name": "",
"bundle_path": ""
}
},
"reference": {
"set_name": "",
"bundle_path": ""
}
}
},
"gpr": {
"mode": "point",