added s11!
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
_load_preprocess_asset(
|
||||||
|
_as_dict(s11_calibration_payload.get("short"), "preprocess.s11.calibration.short"),
|
||||||
|
model.preprocess.s11.calibration.short,
|
||||||
)
|
)
|
||||||
model.preprocess.s21_reference_bundle_path = str(
|
_load_preprocess_asset(
|
||||||
preprocess_payload.get(
|
_as_dict(s11_calibration_payload.get("load"), "preprocess.s11.calibration.load"),
|
||||||
"s21_reference_bundle_path",
|
model.preprocess.s11.calibration.load,
|
||||||
model.preprocess.s21_reference_bundle_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
model.preprocess.s11_open_calibration_bundle_path = str(
|
|
||||||
preprocess_payload.get(
|
|
||||||
"s11_open_calibration_bundle_path",
|
|
||||||
model.preprocess.s11_open_calibration_bundle_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
model.preprocess.s11_short_calibration_bundle_path = str(
|
|
||||||
preprocess_payload.get(
|
|
||||||
"s11_short_calibration_bundle_path",
|
|
||||||
model.preprocess.s11_short_calibration_bundle_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
model.preprocess.s11_load_calibration_bundle_path = str(
|
|
||||||
preprocess_payload.get(
|
|
||||||
"s11_load_calibration_bundle_path",
|
|
||||||
model.preprocess.s11_load_calibration_bundle_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
model.preprocess.s11_reference_bundle_path = str(
|
|
||||||
preprocess_payload.get(
|
|
||||||
"s11_reference_bundle_path",
|
|
||||||
model.preprocess.s11_reference_bundle_path,
|
|
||||||
)
|
)
|
||||||
|
_load_preprocess_asset(
|
||||||
|
_as_dict(s11_preprocess_payload.get("reference"), "preprocess.s11.reference"),
|
||||||
|
model.preprocess.s11.reference,
|
||||||
)
|
)
|
||||||
|
|
||||||
model.gpr.mode = str(gpr_payload.get("mode", model.gpr.mode))
|
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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.
@@ -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))}"
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
"""Abstract storage API for calibration/reference sets."""
|
"""Abstract storage API for preprocess sets."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user