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