move to new version
This commit is contained in:
@@ -119,7 +119,7 @@ TARGETS := \
|
|||||||
$(BIN_DIR)/data_preprocessor \
|
$(BIN_DIR)/data_preprocessor \
|
||||||
$(BIN_DIR)/data_processor
|
$(BIN_DIR)/data_processor
|
||||||
|
|
||||||
.PHONY: all clean kamil_adc_collector sar3d_parity
|
.PHONY: all clean kamil_adc_collector sar3d_parity gpr_timing_check
|
||||||
|
|
||||||
all: $(TARGETS)
|
all: $(TARGETS)
|
||||||
|
|
||||||
@@ -137,6 +137,20 @@ $(BIN_DIR)/sar3d_parity: $(SAR3D_PARITY_OBJS) $(GPR_CPP_LIB)
|
|||||||
@mkdir -p $(BIN_DIR)
|
@mkdir -p $(BIN_DIR)
|
||||||
$(CXX) $(SAR3D_PARITY_OBJS) -o $@ $(DATA_PROCESSOR_LDFLAGS)
|
$(CXX) $(SAR3D_PARITY_OBJS) -o $@ $(DATA_PROCESSOR_LDFLAGS)
|
||||||
|
|
||||||
|
# On-demand numeric check of the backprojection frame-timing model against the
|
||||||
|
# Multi_antenna_3D notebook (scripts/gpr_timing_check_reference.py prints the
|
||||||
|
# same numbers from the notebook formulas). Not part of `all`.
|
||||||
|
GPR_TIMING_CHECK_OBJS := $(addprefix $(BUILD_DIR)/, \
|
||||||
|
data_acq_and_processing/processing/tools/gpr_timing_check_main.o \
|
||||||
|
data_acq_and_processing/common_cpp/ipc/src/shared_types.o \
|
||||||
|
data_acq_and_processing/common_cpp/config/src/run_config.o)
|
||||||
|
|
||||||
|
gpr_timing_check: $(BIN_DIR)/gpr_timing_check
|
||||||
|
|
||||||
|
$(BIN_DIR)/gpr_timing_check: $(GPR_TIMING_CHECK_OBJS)
|
||||||
|
@mkdir -p $(BIN_DIR)
|
||||||
|
$(CXX) $(GPR_TIMING_CHECK_OBJS) -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
$(BIN_DIR)/sweep_orchestrator: $(SWEEP_ORCH_OBJS)
|
$(BIN_DIR)/sweep_orchestrator: $(SWEEP_ORCH_OBJS)
|
||||||
@mkdir -p $(BIN_DIR)
|
@mkdir -p $(BIN_DIR)
|
||||||
$(CXX) $(SWEEP_ORCH_OBJS) -o $@ $(ORCH_LDFLAGS)
|
$(CXX) $(SWEEP_ORCH_OBJS) -o $@ $(ORCH_LDFLAGS)
|
||||||
|
|||||||
@@ -74,7 +74,28 @@ struct ProcessingLiveConfig {
|
|||||||
bool gpr_remove_sidelobe_objects_enabled = true;
|
bool gpr_remove_sidelobe_objects_enabled = true;
|
||||||
// BP image is computed in the y=imaging_plane_y_m slice of the 3D grid.
|
// BP image is computed in the y=imaging_plane_y_m slice of the 3D grid.
|
||||||
// Default 0 keeps legacy 1D antenna layouts imaging in the antenna plane.
|
// Default 0 keeps legacy 1D antenna layouts imaging in the antenna plane.
|
||||||
|
// Used only when gpr_plane_mode == "const".
|
||||||
float gpr_imaging_plane_y_m = 0.0F;
|
float gpr_imaging_plane_y_m = 0.0F;
|
||||||
|
// Acquisition timing model for motion correction (notebook
|
||||||
|
// FREQUENCY_TIMING_MODE): "segmented_interleaved" — Tx outputs run in
|
||||||
|
// sequential segments, each segment sweeps all frequencies over its window
|
||||||
|
// while its Tx interleave per frequency; "interleaved" — legacy model where
|
||||||
|
// all Tx interleave over the whole frame.
|
||||||
|
std::string gpr_timing_mode = "segmented_interleaved";
|
||||||
|
// Fallback segment size when per-trace capture timestamps are absent;
|
||||||
|
// timestamp grouping wins when available.
|
||||||
|
std::uint32_t gpr_tx_per_segment = 2U;
|
||||||
|
// Imaging plane of the 2D BP map (notebook BP_PLANE_MODE): "ground" — the
|
||||||
|
// plane follows the ground given the frame pitch, needed once the antennas
|
||||||
|
// are spread vertically; "const" — legacy fixed y = gpr_imaging_plane_y_m.
|
||||||
|
std::string gpr_plane_mode = "ground";
|
||||||
|
// Height of the LOWER antenna row above the ground, metres (notebook
|
||||||
|
// BP_GROUND_DEPTH_M; structural constant of the frame).
|
||||||
|
float gpr_ground_depth_m = 1.25F;
|
||||||
|
// Frame pitch used by the ground plane. Override disabled -> the pitch
|
||||||
|
// falls back to gpr_look_angle_deg (notebook BP_GROUND_PITCH_DEG = None).
|
||||||
|
bool gpr_ground_pitch_override_enabled = false;
|
||||||
|
float gpr_ground_pitch_deg = 6.5F;
|
||||||
// Coherent BP object visibility (window + the draw limits below) is applied in
|
// Coherent BP object visibility (window + the draw limits below) is applied in
|
||||||
// the processor itself, matching Horns_motion_3libre.py — there is NO score
|
// the processor itself, matching Horns_motion_3libre.py — there is NO score
|
||||||
// threshold for it. Only legacy GPR still thresholds, on a pair count.
|
// threshold for it. Only legacy GPR still thresholds, on a pair count.
|
||||||
|
|||||||
@@ -67,6 +67,20 @@ using Json = nlohmann::json;
|
|||||||
throw std::runtime_error(field_name + " must be one of: int_minus, int_focus");
|
throw std::runtime_error(field_name + " must be one of: int_minus, int_focus");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto parse_gpr_timing_mode(const std::string& value, const std::string& field_name) -> std::string {
|
||||||
|
if (value == "segmented_interleaved" || value == "interleaved") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
throw std::runtime_error(field_name + " must be one of: segmented_interleaved, interleaved");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto parse_gpr_plane_mode(const std::string& value, const std::string& field_name) -> std::string {
|
||||||
|
if (value == "ground" || value == "const") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
throw std::runtime_error(field_name + " must be one of: ground, const");
|
||||||
|
}
|
||||||
|
|
||||||
void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::string& value) {
|
void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::string& value) {
|
||||||
if (value == "backprojection") {
|
if (value == "backprojection") {
|
||||||
return;
|
return;
|
||||||
@@ -355,6 +369,45 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
|
|||||||
}
|
}
|
||||||
config.gpr_imaging_plane_y_m = static_cast<float>(found->get<double>());
|
config.gpr_imaging_plane_y_m = static_cast<float>(found->get<double>());
|
||||||
}
|
}
|
||||||
|
if (const auto found = root.find("gpr_timing_mode"); found != root.end()) {
|
||||||
|
if (!found->is_string()) {
|
||||||
|
throw std::runtime_error("processing.gpr_timing_mode must be string");
|
||||||
|
}
|
||||||
|
config.gpr_timing_mode = parse_gpr_timing_mode(found->get<std::string>(), "processing.gpr_timing_mode");
|
||||||
|
}
|
||||||
|
if (const auto found = root.find("gpr_tx_per_segment"); found != root.end()) {
|
||||||
|
config.gpr_tx_per_segment = parse_u32_number(*found, "processing.gpr_tx_per_segment");
|
||||||
|
if (config.gpr_tx_per_segment == 0U) {
|
||||||
|
throw std::runtime_error("processing.gpr_tx_per_segment must be >= 1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (const auto found = root.find("gpr_plane_mode"); found != root.end()) {
|
||||||
|
if (!found->is_string()) {
|
||||||
|
throw std::runtime_error("processing.gpr_plane_mode must be string");
|
||||||
|
}
|
||||||
|
config.gpr_plane_mode = parse_gpr_plane_mode(found->get<std::string>(), "processing.gpr_plane_mode");
|
||||||
|
}
|
||||||
|
if (const auto found = root.find("gpr_ground_depth_m"); found != root.end()) {
|
||||||
|
if (!found->is_number()) {
|
||||||
|
throw std::runtime_error("processing.gpr_ground_depth_m must be number");
|
||||||
|
}
|
||||||
|
config.gpr_ground_depth_m = static_cast<float>(found->get<double>());
|
||||||
|
if (config.gpr_ground_depth_m < 0.0F) {
|
||||||
|
throw std::runtime_error("processing.gpr_ground_depth_m must be >= 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (const auto found = root.find("gpr_ground_pitch_override_enabled"); found != root.end()) {
|
||||||
|
if (!found->is_boolean()) {
|
||||||
|
throw std::runtime_error("processing.gpr_ground_pitch_override_enabled must be bool");
|
||||||
|
}
|
||||||
|
config.gpr_ground_pitch_override_enabled = found->get<bool>();
|
||||||
|
}
|
||||||
|
if (const auto found = root.find("gpr_ground_pitch_deg"); found != root.end()) {
|
||||||
|
if (!found->is_number()) {
|
||||||
|
throw std::runtime_error("processing.gpr_ground_pitch_deg must be number");
|
||||||
|
}
|
||||||
|
config.gpr_ground_pitch_deg = static_cast<float>(found->get<double>());
|
||||||
|
}
|
||||||
for (const auto& [key, target] : {
|
for (const auto& [key, target] : {
|
||||||
std::pair{"gpr_visible_x_min_m", &config.gpr_visible_x_min_m},
|
std::pair{"gpr_visible_x_min_m", &config.gpr_visible_x_min_m},
|
||||||
std::pair{"gpr_visible_x_max_m", &config.gpr_visible_x_max_m},
|
std::pair{"gpr_visible_x_max_m", &config.gpr_visible_x_max_m},
|
||||||
|
|||||||
+305
-64
@@ -653,52 +653,242 @@ void validate_collection_trace_order(
|
|||||||
return traces;
|
return traces;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frequency-domain speed correction applied to S21 *before* the IFFT, ported
|
// ── Frame acquisition timing ────────────────────────────────────────────────
|
||||||
// from Horns_motion_3libre.py (apply_intra_sweep_phase_correction +
|
|
||||||
// compute_frequency_sample_times). The measurement is interleaved Tx-by-frequency
|
|
||||||
// — Tx0(f0), Tx1(f0), Tx0(f1), ... — so each Tx occupies one interleave slot and
|
|
||||||
// its frequency k is sampled at global step (slot_count*k + slot). Each in-band
|
|
||||||
// frequency's phase is rotated back to the centre of the (full) interleaved sweep.
|
|
||||||
//
|
//
|
||||||
// Unlike the offline Python script, every physical input is taken from the live
|
// The real acquisition is SEGMENTED (notebook FREQUENCY_TIMING_MODE =
|
||||||
// system: `speed_mps` is the socket-fed velocity, `look_angle_deg`/`direction_sign`
|
// 'segmented_interleaved'): the Tx outputs are grouped into segments that run
|
||||||
// are config, and `tx_sweep_time_s` is derived per-collection from the acquisition
|
// one after another; each segment sweeps ALL frequencies over its own window,
|
||||||
// timestamps (capture_span / Tx count). A zero speed or non-positive sweep time
|
// and within a segment its Tx interleave per frequency while all Rx receive in
|
||||||
// makes this a no-op.
|
// parallel. The dominant motion shift is therefore BETWEEN segments (a pair-
|
||||||
void apply_intra_sweep_motion_correction(
|
// level dtau relative to the frame centre), with a much smaller intra-segment
|
||||||
|
// sweep shift on top — the legacy whole-frame interleaved model captured only
|
||||||
|
// the latter.
|
||||||
|
//
|
||||||
|
// Segment windows come from the per-trace capture timestamps: traces measured
|
||||||
|
// in one segment share one (start, end) window on both producers, so grouping
|
||||||
|
// outputs by identical windows reconstructs the segments exactly (notebook
|
||||||
|
// snapshot_acquisition_timing). When timestamps are absent (zero sentinel) the
|
||||||
|
// frame is split uniformly into ceil(n_tx / tx_per_segment) segments.
|
||||||
|
//
|
||||||
|
// Segment/slot assignment is a hardware property: it is derived from the FULL
|
||||||
|
// run_combos order, never from the live Tx selection — otherwise disabling
|
||||||
|
// channels would silently shrink the frame model (this also fixes the old
|
||||||
|
// capture_span / N_selected_tx bug).
|
||||||
|
|
||||||
|
struct SegmentWindow {
|
||||||
|
double start_s = 0.0;
|
||||||
|
double end_s = 0.0;
|
||||||
|
double center_s = 0.0;
|
||||||
|
bool stretched = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TxSlot {
|
||||||
|
std::size_t segment_index = 0U;
|
||||||
|
std::size_t slot = 0U;
|
||||||
|
std::size_t slot_count = 1U;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FrameTiming {
|
||||||
|
std::vector<SegmentWindow> segments{};
|
||||||
|
std::unordered_map<std::uint32_t, TxSlot> by_output_pos{};
|
||||||
|
double frame_center_s = 0.0;
|
||||||
|
bool from_trace_timestamps = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] auto build_frame_timing(
|
||||||
|
const config::RunConfig& run_config,
|
||||||
|
const ipc::PreprocessedCollection& collection,
|
||||||
|
std::size_t tx_per_segment_fallback,
|
||||||
|
bool segmented
|
||||||
|
) -> FrameTiming {
|
||||||
|
FrameTiming timing{};
|
||||||
|
|
||||||
|
// Full hardware Tx order: first occurrence of each output in run_combos
|
||||||
|
// (notebook tx_order_full). Deliberately ignores the live Tx selection.
|
||||||
|
std::vector<std::uint32_t> tx_order{};
|
||||||
|
for (const auto& combo : run_config.run_combos) {
|
||||||
|
if (std::find(tx_order.begin(), tx_order.end(), combo.output_pos) == tx_order.end()) {
|
||||||
|
tx_order.push_back(combo.output_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tx_order.empty()) {
|
||||||
|
return timing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double frame_span_s =
|
||||||
|
collection.capture_end_ns > collection.capture_start_ns
|
||||||
|
? static_cast<double>(collection.capture_end_ns - collection.capture_start_ns) * 1e-9
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
// Timestamp path: per output take the window of its FIRST trace (notebook
|
||||||
|
// setdefault); require every window measured. Traces of one segment share
|
||||||
|
// one window by construction, so identical windows identify segments.
|
||||||
|
std::unordered_map<std::uint32_t, std::pair<std::uint64_t, std::uint64_t>> window_by_tx{};
|
||||||
|
bool timestamps_ok = segmented;
|
||||||
|
if (segmented) {
|
||||||
|
for (const auto& trace : collection.traces) {
|
||||||
|
window_by_tx.try_emplace(
|
||||||
|
trace.combo.output_pos,
|
||||||
|
std::pair{trace.capture_start_ns, trace.capture_end_ns}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const auto tx : tx_order) {
|
||||||
|
const auto found = window_by_tx.find(tx);
|
||||||
|
if (found == window_by_tx.end() || found->second.second <= found->second.first) {
|
||||||
|
timestamps_ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestamps_ok) {
|
||||||
|
std::uint64_t first_start_ns = std::numeric_limits<std::uint64_t>::max();
|
||||||
|
std::uint64_t last_end_ns = 0U;
|
||||||
|
for (const auto tx : tx_order) {
|
||||||
|
const auto& window = window_by_tx.at(tx);
|
||||||
|
first_start_ns = std::min(first_start_ns, window.first);
|
||||||
|
last_end_ns = std::max(last_end_ns, window.second);
|
||||||
|
}
|
||||||
|
const double acquisition_s = static_cast<double>(last_end_ns - first_start_ns) * 1e-9;
|
||||||
|
|
||||||
|
// Unique windows ordered by start; Tx sharing a window form one segment,
|
||||||
|
// slotted in hardware order.
|
||||||
|
std::vector<std::pair<std::uint64_t, std::uint64_t>> windows{};
|
||||||
|
for (const auto tx : tx_order) {
|
||||||
|
const auto& window = window_by_tx.at(tx);
|
||||||
|
if (std::find(windows.begin(), windows.end(), window) == windows.end()) {
|
||||||
|
windows.push_back(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(windows.begin(), windows.end());
|
||||||
|
|
||||||
|
const double nominal_segment_s =
|
||||||
|
acquisition_s / static_cast<double>(std::max<std::size_t>(windows.size(), 1U));
|
||||||
|
timing.segments.reserve(windows.size());
|
||||||
|
for (const auto& window : windows) {
|
||||||
|
SegmentWindow segment{};
|
||||||
|
segment.start_s = static_cast<double>(window.first - first_start_ns) * 1e-9;
|
||||||
|
segment.end_s = static_cast<double>(window.second - first_start_ns) * 1e-9;
|
||||||
|
segment.center_s = 0.5 * (segment.start_s + segment.end_s);
|
||||||
|
// A segment noticeably longer than the frame/n_segments norm had an
|
||||||
|
// instrument stall inside; where exactly is unknowable from the
|
||||||
|
// timestamps, so the uniform frequency layout is approximate there.
|
||||||
|
segment.stretched =
|
||||||
|
nominal_segment_s > 0.0 && (segment.end_s - segment.start_s) > 1.03 * nominal_segment_s;
|
||||||
|
timing.segments.push_back(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto tx : tx_order) {
|
||||||
|
const auto& window = window_by_tx.at(tx);
|
||||||
|
const auto segment_index = static_cast<std::size_t>(
|
||||||
|
std::find(windows.begin(), windows.end(), window) - windows.begin()
|
||||||
|
);
|
||||||
|
TxSlot slot{};
|
||||||
|
slot.segment_index = segment_index;
|
||||||
|
slot.slot = 0U;
|
||||||
|
for (const auto other : tx_order) {
|
||||||
|
if (other == tx) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (window_by_tx.at(other) == window) {
|
||||||
|
++slot.slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slot.slot_count = static_cast<std::size_t>(std::count_if(
|
||||||
|
tx_order.begin(), tx_order.end(),
|
||||||
|
[&](std::uint32_t other) { return window_by_tx.at(other) == window; }
|
||||||
|
));
|
||||||
|
timing.by_output_pos.emplace(tx, slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
timing.frame_center_s = 0.5 * acquisition_s;
|
||||||
|
timing.from_trace_timestamps = true;
|
||||||
|
return timing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: uniform split of the collection-level span. `segmented` divides
|
||||||
|
// the outputs into chunks of tx_per_segment; the legacy 'interleaved' mode is
|
||||||
|
// the same code with a single segment holding every Tx.
|
||||||
|
const std::size_t per_segment = segmented ? std::max<std::size_t>(tx_per_segment_fallback, 1U)
|
||||||
|
: tx_order.size();
|
||||||
|
const std::size_t segment_count = (tx_order.size() + per_segment - 1U) / per_segment;
|
||||||
|
const double segment_span_s = frame_span_s / static_cast<double>(segment_count);
|
||||||
|
timing.segments.reserve(segment_count);
|
||||||
|
for (std::size_t index = 0U; index < segment_count; ++index) {
|
||||||
|
SegmentWindow segment{};
|
||||||
|
segment.start_s = static_cast<double>(index) * segment_span_s;
|
||||||
|
segment.end_s = segment.start_s + segment_span_s;
|
||||||
|
segment.center_s = 0.5 * (segment.start_s + segment.end_s);
|
||||||
|
timing.segments.push_back(segment);
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0U; index < tx_order.size(); ++index) {
|
||||||
|
TxSlot slot{};
|
||||||
|
slot.segment_index = index / per_segment;
|
||||||
|
slot.slot = index % per_segment;
|
||||||
|
const std::size_t segment_begin = slot.segment_index * per_segment;
|
||||||
|
slot.slot_count = std::min(per_segment, tx_order.size() - segment_begin);
|
||||||
|
timing.by_output_pos.emplace(tx_order[index], slot);
|
||||||
|
}
|
||||||
|
timing.frame_center_s = 0.5 * frame_span_s;
|
||||||
|
return timing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frequency-domain speed correction applied to S21 *before* the IFFT, ported
|
||||||
|
// from the Multi_antenna_3D notebook (apply_intra_sweep_phase_correction +
|
||||||
|
// compute_frequency_sample_times, segmented branch). One phase model covers
|
||||||
|
// both motion terms:
|
||||||
|
// dtau_pair — the pair-level shift of this Tx's segment centre against the
|
||||||
|
// frame centre (dominant between sequential segments);
|
||||||
|
// dtau_intra — the within-segment sweep shift: frequency k of interleave
|
||||||
|
// slot `slot` is event (slot_count*k + slot) of the segment.
|
||||||
|
// The combined phi = 2*pi*f*(dtau_intra + dtau_pair) is applied as
|
||||||
|
// s21 *= exp(-i*phi), matching the notebook's motion_phase_sign = -1.
|
||||||
|
//
|
||||||
|
// Every physical input is taken from the live system: `speed_mps` is the
|
||||||
|
// socket-fed velocity, `look_angle_deg`/`direction_sign` are config, and the
|
||||||
|
// segment windows come from the collection's own trace timestamps. A zero
|
||||||
|
// speed or an empty segment window makes this a no-op.
|
||||||
|
void apply_motion_correction(
|
||||||
SelectedTrace& trace,
|
SelectedTrace& trace,
|
||||||
|
const SegmentWindow& segment,
|
||||||
std::size_t slot,
|
std::size_t slot,
|
||||||
std::size_t slot_count,
|
std::size_t slot_count,
|
||||||
|
double frame_center_s,
|
||||||
double start_hz,
|
double start_hz,
|
||||||
double stop_hz,
|
double stop_hz,
|
||||||
double velocity_mps,
|
double velocity_mps,
|
||||||
double speed_mps,
|
double speed_mps,
|
||||||
double look_angle_deg,
|
double look_angle_deg,
|
||||||
double direction_sign,
|
double direction_sign,
|
||||||
double tx_sweep_time_s,
|
|
||||||
MotionMode motion_mode
|
MotionMode motion_mode
|
||||||
) {
|
) {
|
||||||
const std::size_t n_full = trace.frequency_hz.size();
|
const std::size_t n_full = trace.frequency_hz.size();
|
||||||
|
const double segment_span_s = segment.end_s - segment.start_s;
|
||||||
if (n_full < 2U || trace.s21.size() != n_full || slot_count == 0U
|
if (n_full < 2U || trace.s21.size() != n_full || slot_count == 0U
|
||||||
|| !(velocity_mps > 0.0) || !(tx_sweep_time_s > 0.0)) {
|
|| !(velocity_mps > 0.0) || !(segment_span_s > 0.0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const double low_hz = std::min(start_hz, stop_hz);
|
const double low_hz = std::min(start_hz, stop_hz);
|
||||||
const double high_hz = std::max(start_hz, stop_hz);
|
const double high_hz = std::max(start_hz, stop_hz);
|
||||||
|
|
||||||
// Absolute sample time of full-sweep frequency index k for this Tx slot.
|
// The segment sweeps all frequencies over its window; on each frequency its
|
||||||
const double dt_base = tx_sweep_time_s / static_cast<double>(n_full - 1U);
|
// Tx take turns, so frequency k of slot `slot` is event (slot_count*k + slot)
|
||||||
|
// out of slot_count*n_full.
|
||||||
|
const double dt_event_s = segment_span_s / (static_cast<double>(slot_count) * static_cast<double>(n_full));
|
||||||
const auto sample_time_s = [&](std::size_t index) {
|
const auto sample_time_s = [&](std::size_t index) {
|
||||||
return ((static_cast<double>(slot_count) * static_cast<double>(index)) + static_cast<double>(slot)) * dt_base;
|
return segment.start_s
|
||||||
|
+ ((static_cast<double>(slot_count) * static_cast<double>(index)) + static_cast<double>(slot))
|
||||||
|
* dt_event_s;
|
||||||
};
|
};
|
||||||
// t_center is the midpoint of the *full* interleaved sweep for this slot,
|
|
||||||
// i.e. between the first and last full-array frequencies (not the cut band).
|
|
||||||
const double t_center_s = 0.5 * (sample_time_s(0U) + sample_time_s(n_full - 1U));
|
|
||||||
|
|
||||||
const double theta_rad = (look_angle_deg * kPi) / 180.0;
|
const double theta_rad = (look_angle_deg * kPi) / 180.0;
|
||||||
const double motion_factor = direction_sign * speed_mps * std::cos(theta_rad);
|
const double motion_factor = direction_sign * speed_mps * std::cos(theta_rad);
|
||||||
|
|
||||||
|
// Pair-level coarse shift: segment centre vs frame centre.
|
||||||
|
const double dz_pair_m = motion_factor * (segment.center_s - frame_center_s);
|
||||||
|
const double dtau_pair_s = 2.0 * dz_pair_m / velocity_mps;
|
||||||
|
|
||||||
std::vector<std::size_t> band_indices{};
|
std::vector<std::size_t> band_indices{};
|
||||||
std::vector<double> band_frequency_hz{};
|
std::vector<double> band_frequency_hz{};
|
||||||
std::vector<double> phi{};
|
std::vector<double> phi{};
|
||||||
@@ -712,10 +902,9 @@ void apply_intra_sweep_motion_correction(
|
|||||||
if (frequency < low_hz || frequency > high_hz) {
|
if (frequency < low_hz || frequency > high_hz) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const double dt_intra_s = sample_time_s(index) - t_center_s;
|
const double dt_intra_s = sample_time_s(index) - segment.center_s;
|
||||||
const double delta_range_m = motion_factor * dt_intra_s;
|
const double delta_path_m = 2.0 * motion_factor * dt_intra_s;
|
||||||
const double delta_path_m = 2.0 * delta_range_m;
|
const double dtau_s = (delta_path_m / velocity_mps) + dtau_pair_s;
|
||||||
const double dtau_s = delta_path_m / velocity_mps;
|
|
||||||
if (dtau_s != 0.0) {
|
if (dtau_s != 0.0) {
|
||||||
any_nonzero = true;
|
any_nonzero = true;
|
||||||
}
|
}
|
||||||
@@ -892,11 +1081,32 @@ void normalize_pair_ascans(
|
|||||||
return std::sqrt((dx * dx) + (dy * dy) + (dz * dz));
|
return std::sqrt((dx * dx) + (dy * dy) + (dz * dz));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Y of the BP imaging plane at range z. In 'ground' mode the plane follows the
|
||||||
|
// ground in the radar's tilted axes (notebook bp_plane_y): the frame is pitched
|
||||||
|
// down by pitch_deg and the lower antenna row sits ground_depth_m above ground,
|
||||||
|
// so y(z) = (-ground_depth_m + z*sin(pitch)) / cos(pitch). With antennas spread
|
||||||
|
// vertically an off-plane point smears into an equal-range arc, so the plane
|
||||||
|
// choice matters; 'const' keeps the legacy fixed-y slice.
|
||||||
|
struct ImagingPlane {
|
||||||
|
bool ground_mode = false;
|
||||||
|
double const_y_m = 0.0;
|
||||||
|
double ground_depth_m = 0.0;
|
||||||
|
double pitch_deg = 0.0;
|
||||||
|
|
||||||
|
[[nodiscard]] auto y_at(double z_m) const -> double {
|
||||||
|
if (!ground_mode) {
|
||||||
|
return const_y_m;
|
||||||
|
}
|
||||||
|
const double pitch_rad = (pitch_deg * kPi) / 180.0;
|
||||||
|
return (-ground_depth_m + (z_m * std::sin(pitch_rad))) / std::cos(pitch_rad);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
[[nodiscard]] auto build_grid(
|
[[nodiscard]] auto build_grid(
|
||||||
const GeometrySelection& selection,
|
const GeometrySelection& selection,
|
||||||
double max_depth_m,
|
double max_depth_m,
|
||||||
double min_z_m,
|
double min_z_m,
|
||||||
double imaging_plane_y_m
|
const ImagingPlane& plane
|
||||||
) -> GridDefinition {
|
) -> GridDefinition {
|
||||||
GridDefinition grid{};
|
GridDefinition grid{};
|
||||||
if (selection.x_tx.empty() || selection.x_rx.empty() || !(max_depth_m > min_z_m)) {
|
if (selection.x_tx.empty() || selection.x_rx.empty() || !(max_depth_m > min_z_m)) {
|
||||||
@@ -917,6 +1127,8 @@ void normalize_pair_ascans(
|
|||||||
|
|
||||||
for (std::size_t row = 0U; row < grid.z_grid.size(); ++row) {
|
for (std::size_t row = 0U; row < grid.z_grid.size(); ++row) {
|
||||||
const double z_value = grid.z_grid[row];
|
const double z_value = grid.z_grid[row];
|
||||||
|
// Plane height depends only on range, so it is constant along a row.
|
||||||
|
const double plane_y_m = plane.y_at(z_value);
|
||||||
for (std::size_t col = 0U; col < grid.x_grid.size(); ++col) {
|
for (std::size_t col = 0U; col < grid.x_grid.size(); ++col) {
|
||||||
const double x_value = grid.x_grid[col];
|
const double x_value = grid.x_grid[col];
|
||||||
const auto cell_index = (row * grid.x_grid.size()) + col;
|
const auto cell_index = (row * grid.x_grid.size()) + col;
|
||||||
@@ -924,14 +1136,14 @@ void normalize_pair_ascans(
|
|||||||
for (std::size_t tx_index = 0U; tx_index < selection.x_tx.size(); ++tx_index) {
|
for (std::size_t tx_index = 0U; tx_index < selection.x_tx.size(); ++tx_index) {
|
||||||
grid.tx_distance_grids[tx_index][cell_index] = distance_3d(
|
grid.tx_distance_grids[tx_index][cell_index] = distance_3d(
|
||||||
x_value - selection.x_tx[tx_index],
|
x_value - selection.x_tx[tx_index],
|
||||||
imaging_plane_y_m - selection.y_tx[tx_index],
|
plane_y_m - selection.y_tx[tx_index],
|
||||||
z_value - selection.z_tx[tx_index]
|
z_value - selection.z_tx[tx_index]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (std::size_t rx_index = 0U; rx_index < selection.x_rx.size(); ++rx_index) {
|
for (std::size_t rx_index = 0U; rx_index < selection.x_rx.size(); ++rx_index) {
|
||||||
grid.rx_distance_grids[rx_index][cell_index] = distance_3d(
|
grid.rx_distance_grids[rx_index][cell_index] = distance_3d(
|
||||||
x_value - selection.x_rx[rx_index],
|
x_value - selection.x_rx[rx_index],
|
||||||
imaging_plane_y_m - selection.y_rx[rx_index],
|
plane_y_m - selection.y_rx[rx_index],
|
||||||
z_value - selection.z_rx[rx_index]
|
z_value - selection.z_rx[rx_index]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -948,10 +1160,10 @@ void normalize_pair_ascans(
|
|||||||
const GeometrySelection& selection,
|
const GeometrySelection& selection,
|
||||||
double max_depth_m,
|
double max_depth_m,
|
||||||
double min_z_m,
|
double min_z_m,
|
||||||
double imaging_plane_y_m
|
const ImagingPlane& plane
|
||||||
) -> std::vector<double> {
|
) -> std::vector<double> {
|
||||||
std::vector<double> signature;
|
std::vector<double> signature;
|
||||||
signature.reserve((selection.x_tx.size() + selection.x_rx.size()) * 3U + 3U);
|
signature.reserve((selection.x_tx.size() + selection.x_rx.size()) * 3U + 6U);
|
||||||
const auto append = [&](const std::vector<double>& axis) {
|
const auto append = [&](const std::vector<double>& axis) {
|
||||||
signature.insert(signature.end(), axis.begin(), axis.end());
|
signature.insert(signature.end(), axis.begin(), axis.end());
|
||||||
};
|
};
|
||||||
@@ -963,7 +1175,10 @@ void normalize_pair_ascans(
|
|||||||
append(selection.z_rx);
|
append(selection.z_rx);
|
||||||
signature.push_back(max_depth_m);
|
signature.push_back(max_depth_m);
|
||||||
signature.push_back(min_z_m);
|
signature.push_back(min_z_m);
|
||||||
signature.push_back(imaging_plane_y_m);
|
signature.push_back(plane.ground_mode ? 1.0 : 0.0);
|
||||||
|
signature.push_back(plane.const_y_m);
|
||||||
|
signature.push_back(plane.ground_depth_m);
|
||||||
|
signature.push_back(plane.pitch_deg);
|
||||||
return signature;
|
return signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1003,19 +1218,20 @@ void normalize_pair_ascans(
|
|||||||
std::uint32_t tx_index,
|
std::uint32_t tx_index,
|
||||||
std::uint32_t rx_index,
|
std::uint32_t rx_index,
|
||||||
const GeometrySelection& selection,
|
const GeometrySelection& selection,
|
||||||
double imaging_plane_y_m
|
const ImagingPlane& plane
|
||||||
) -> std::pair<double, double> {
|
) -> std::pair<double, double> {
|
||||||
const double x_center = 0.5 * (selection.x_tx[tx_index] + selection.x_rx[rx_index]);
|
const double x_center = 0.5 * (selection.x_tx[tx_index] + selection.x_rx[rx_index]);
|
||||||
|
const double plane_y_m = plane.y_at(kCompensationReferenceDepthM);
|
||||||
const double dz_tx = kCompensationReferenceDepthM - selection.z_tx[tx_index];
|
const double dz_tx = kCompensationReferenceDepthM - selection.z_tx[tx_index];
|
||||||
const double dz_rx = kCompensationReferenceDepthM - selection.z_rx[rx_index];
|
const double dz_rx = kCompensationReferenceDepthM - selection.z_rx[rx_index];
|
||||||
const double r_tx = distance_3d(
|
const double r_tx = distance_3d(
|
||||||
x_center - selection.x_tx[tx_index],
|
x_center - selection.x_tx[tx_index],
|
||||||
imaging_plane_y_m - selection.y_tx[tx_index],
|
plane_y_m - selection.y_tx[tx_index],
|
||||||
dz_tx
|
dz_tx
|
||||||
);
|
);
|
||||||
const double r_rx = distance_3d(
|
const double r_rx = distance_3d(
|
||||||
x_center - selection.x_rx[rx_index],
|
x_center - selection.x_rx[rx_index],
|
||||||
imaging_plane_y_m - selection.y_rx[rx_index],
|
plane_y_m - selection.y_rx[rx_index],
|
||||||
dz_rx
|
dz_rx
|
||||||
);
|
);
|
||||||
return attenuation_components(r_tx, r_rx, dz_tx, dz_rx);
|
return attenuation_components(r_tx, r_rx, dz_tx, dz_rx);
|
||||||
@@ -1053,7 +1269,7 @@ void normalize_pair_ascans(
|
|||||||
const std::unordered_map<PairKey, AscanResult>& ascans_by_pair,
|
const std::unordered_map<PairKey, AscanResult>& ascans_by_pair,
|
||||||
const GridDefinition& grid,
|
const GridDefinition& grid,
|
||||||
const GeometrySelection& selection,
|
const GeometrySelection& selection,
|
||||||
double imaging_plane_y_m,
|
const ImagingPlane& plane,
|
||||||
double velocity_mps,
|
double velocity_mps,
|
||||||
double min_depth_m,
|
double min_depth_m,
|
||||||
double max_depth_m,
|
double max_depth_m,
|
||||||
@@ -1095,7 +1311,7 @@ void normalize_pair_ascans(
|
|||||||
trace.tx_local_index,
|
trace.tx_local_index,
|
||||||
trace.rx_local_index,
|
trace.rx_local_index,
|
||||||
selection,
|
selection,
|
||||||
imaging_plane_y_m
|
plane
|
||||||
);
|
);
|
||||||
contributions.push_back(PairContribution{
|
contributions.push_back(PairContribution{
|
||||||
&grid.tx_distance_grids[trace.tx_local_index],
|
&grid.tx_distance_grids[trace.tx_local_index],
|
||||||
@@ -1505,21 +1721,22 @@ void apply_depth_gate(
|
|||||||
const ObjectRecord& object,
|
const ObjectRecord& object,
|
||||||
const std::vector<SelectedTrace>& selected_traces,
|
const std::vector<SelectedTrace>& selected_traces,
|
||||||
const GeometrySelection& selection,
|
const GeometrySelection& selection,
|
||||||
double imaging_plane_y_m
|
const ImagingPlane& plane
|
||||||
) -> std::vector<double> {
|
) -> std::vector<double> {
|
||||||
std::vector<double> signature{};
|
std::vector<double> signature{};
|
||||||
signature.reserve(selected_traces.size());
|
signature.reserve(selected_traces.size());
|
||||||
|
const double plane_y_m = plane.y_at(object.z_m);
|
||||||
for (const auto& trace : selected_traces) {
|
for (const auto& trace : selected_traces) {
|
||||||
const auto tx = trace.tx_local_index;
|
const auto tx = trace.tx_local_index;
|
||||||
const auto rx = trace.rx_local_index;
|
const auto rx = trace.rx_local_index;
|
||||||
const double r_tx = distance_3d(
|
const double r_tx = distance_3d(
|
||||||
object.x_m - selection.x_tx[tx],
|
object.x_m - selection.x_tx[tx],
|
||||||
imaging_plane_y_m - selection.y_tx[tx],
|
plane_y_m - selection.y_tx[tx],
|
||||||
object.z_m - selection.z_tx[tx]
|
object.z_m - selection.z_tx[tx]
|
||||||
);
|
);
|
||||||
const double r_rx = distance_3d(
|
const double r_rx = distance_3d(
|
||||||
object.x_m - selection.x_rx[rx],
|
object.x_m - selection.x_rx[rx],
|
||||||
imaging_plane_y_m - selection.y_rx[rx],
|
plane_y_m - selection.y_rx[rx],
|
||||||
object.z_m - selection.z_rx[rx]
|
object.z_m - selection.z_rx[rx]
|
||||||
);
|
);
|
||||||
signature.push_back(0.5 * (r_tx + r_rx));
|
signature.push_back(0.5 * (r_tx + r_rx));
|
||||||
@@ -1531,12 +1748,12 @@ void mark_sidelobe_candidates(
|
|||||||
std::vector<ObjectRecord>& objects,
|
std::vector<ObjectRecord>& objects,
|
||||||
const std::vector<SelectedTrace>& selected_traces,
|
const std::vector<SelectedTrace>& selected_traces,
|
||||||
const GeometrySelection& selection,
|
const GeometrySelection& selection,
|
||||||
double imaging_plane_y_m
|
const ImagingPlane& plane
|
||||||
) {
|
) {
|
||||||
std::vector<std::vector<double>> signatures{};
|
std::vector<std::vector<double>> signatures{};
|
||||||
signatures.reserve(objects.size());
|
signatures.reserve(objects.size());
|
||||||
for (const auto& object : objects) {
|
for (const auto& object : objects) {
|
||||||
signatures.push_back(bistatic_depth_signature(object, selected_traces, selection, imaging_plane_y_m));
|
signatures.push_back(bistatic_depth_signature(object, selected_traces, selection, plane));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (std::size_t object_index = 0U; object_index < objects.size(); ++object_index) {
|
for (std::size_t object_index = 0U; object_index < objects.size(); ++object_index) {
|
||||||
@@ -1871,34 +2088,48 @@ void add_bp_score_metrics(
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intra-sweep speed correction before the IFFT. The Tx interleave slot is the
|
// Motion correction before the IFFT: pair-level (between segments) plus
|
||||||
// Tx local index (outputs are sorted), with slot_count = number of selected Tx.
|
// intra-segment sweep shift, one phase model (see apply_motion_correction).
|
||||||
// tx_sweep_time is derived per-collection from the acquisition timestamps:
|
// The frame timing is built from the FULL collection — including traces of
|
||||||
// one Tx's interleaved sweep spans the whole frame, so capture_span / N_tx.
|
// deselected Tx — so disabling channels never shrinks the frame model.
|
||||||
// Speed comes from the socket-fed live config; look-angle/direction from config.
|
// Speed comes from the socket-fed live config; look-angle/direction from config.
|
||||||
const auto motion_mode = parse_motion_mode(live_config.gpr_motion_mode);
|
const auto motion_mode = parse_motion_mode(live_config.gpr_motion_mode);
|
||||||
const std::size_t motion_slot_count = selection.output_positions.size();
|
const auto frame_timing = build_frame_timing(
|
||||||
const double capture_span_s =
|
run_config,
|
||||||
collection.capture_end_ns > collection.capture_start_ns
|
collection,
|
||||||
? static_cast<double>(collection.capture_end_ns - collection.capture_start_ns) * 1e-9
|
static_cast<std::size_t>(std::max<std::uint32_t>(live_config.gpr_tx_per_segment, 1U)),
|
||||||
: 0.0;
|
live_config.gpr_timing_mode == "segmented_interleaved"
|
||||||
const double tx_sweep_time_s =
|
);
|
||||||
motion_slot_count > 0U ? capture_span_s / static_cast<double>(motion_slot_count) : 0.0;
|
for (const auto& segment : frame_timing.segments) {
|
||||||
|
if (segment.stretched) {
|
||||||
|
std::cerr << "[gpr] collection " << collection.collection_id
|
||||||
|
<< ": segment at " << segment.start_s << "s runs "
|
||||||
|
<< (segment.end_s - segment.start_s)
|
||||||
|
<< "s (above the frame/n_segments norm) — the instrument stalled "
|
||||||
|
<< "inside it, motion correction for this frame is approximate\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
const double motion_speed_mps = static_cast<double>(live_config.gpr_speed_m_s);
|
const double motion_speed_mps = static_cast<double>(live_config.gpr_speed_m_s);
|
||||||
const double motion_look_angle_deg = static_cast<double>(live_config.gpr_look_angle_deg);
|
const double motion_look_angle_deg = static_cast<double>(live_config.gpr_look_angle_deg);
|
||||||
const double motion_direction_sign = static_cast<double>(live_config.gpr_direction_sign);
|
const double motion_direction_sign = static_cast<double>(live_config.gpr_direction_sign);
|
||||||
for (auto& trace : selected_traces) {
|
for (auto& trace : selected_traces) {
|
||||||
apply_intra_sweep_motion_correction(
|
const auto slot_it = frame_timing.by_output_pos.find(trace.combo.output_pos);
|
||||||
|
if (slot_it == frame_timing.by_output_pos.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto& tx_slot = slot_it->second;
|
||||||
|
apply_motion_correction(
|
||||||
trace,
|
trace,
|
||||||
trace.tx_local_index,
|
frame_timing.segments[tx_slot.segment_index],
|
||||||
motion_slot_count,
|
tx_slot.slot,
|
||||||
|
tx_slot.slot_count,
|
||||||
|
frame_timing.frame_center_s,
|
||||||
start_hz,
|
start_hz,
|
||||||
stop_hz,
|
stop_hz,
|
||||||
velocity_mps,
|
velocity_mps,
|
||||||
motion_speed_mps,
|
motion_speed_mps,
|
||||||
motion_look_angle_deg,
|
motion_look_angle_deg,
|
||||||
motion_direction_sign,
|
motion_direction_sign,
|
||||||
tx_sweep_time_s,
|
|
||||||
motion_mode
|
motion_mode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1919,18 +2150,28 @@ void add_bp_score_metrics(
|
|||||||
}
|
}
|
||||||
normalize_pair_ascans(ascans_by_pair, velocity_mps, min_depth_m, max_depth_m);
|
normalize_pair_ascans(ascans_by_pair, velocity_mps, min_depth_m, max_depth_m);
|
||||||
|
|
||||||
const double imaging_plane_y_m = static_cast<double>(live_config.gpr_imaging_plane_y_m);
|
// 'ground' plane pitch falls back to the look angle when the override is
|
||||||
|
// off (notebook BP_GROUND_PITCH_DEG = None -> LOOK_ANGLE_DEG).
|
||||||
|
const ImagingPlane plane{
|
||||||
|
live_config.gpr_plane_mode == "ground",
|
||||||
|
static_cast<double>(live_config.gpr_imaging_plane_y_m),
|
||||||
|
static_cast<double>(live_config.gpr_ground_depth_m),
|
||||||
|
live_config.gpr_ground_pitch_override_enabled
|
||||||
|
? static_cast<double>(live_config.gpr_ground_pitch_deg)
|
||||||
|
: static_cast<double>(live_config.gpr_look_angle_deg),
|
||||||
|
};
|
||||||
|
|
||||||
// The imaging grid (90k cells x 6 distance fields) only depends on antenna
|
// The imaging grid (90k cells x 6 distance fields) only depends on antenna
|
||||||
// geometry and the depth window, which are constant across a run, so it is
|
// geometry, the depth window and the imaging plane, which are constant
|
||||||
// memoized between collections and rebuilt only when that signature changes.
|
// across a run, so it is memoized between collections and rebuilt only when
|
||||||
// process_backprojection_gpr is entered by a single processing thread (the
|
// that signature changes. process_backprojection_gpr is entered by a single
|
||||||
// backprojection workers join before it returns), so a local static is safe.
|
// processing thread (the backprojection workers join before it returns), so
|
||||||
|
// a local static is safe.
|
||||||
static std::vector<double> cached_grid_signature;
|
static std::vector<double> cached_grid_signature;
|
||||||
static GridDefinition cached_grid;
|
static GridDefinition cached_grid;
|
||||||
const auto signature = grid_signature(selection, max_depth_m, kGridZMinM, imaging_plane_y_m);
|
const auto signature = grid_signature(selection, max_depth_m, kGridZMinM, plane);
|
||||||
if (cached_grid.x_grid.empty() || signature != cached_grid_signature) {
|
if (cached_grid.x_grid.empty() || signature != cached_grid_signature) {
|
||||||
cached_grid = build_grid(selection, max_depth_m, kGridZMinM, imaging_plane_y_m);
|
cached_grid = build_grid(selection, max_depth_m, kGridZMinM, plane);
|
||||||
cached_grid_signature = signature;
|
cached_grid_signature = signature;
|
||||||
}
|
}
|
||||||
const GridDefinition& grid = cached_grid;
|
const GridDefinition& grid = cached_grid;
|
||||||
@@ -1943,7 +2184,7 @@ void add_bp_score_metrics(
|
|||||||
ascans_by_pair,
|
ascans_by_pair,
|
||||||
grid,
|
grid,
|
||||||
selection,
|
selection,
|
||||||
imaging_plane_y_m,
|
plane,
|
||||||
velocity_mps,
|
velocity_mps,
|
||||||
min_depth_m,
|
min_depth_m,
|
||||||
max_depth_m,
|
max_depth_m,
|
||||||
@@ -1961,7 +2202,7 @@ void add_bp_score_metrics(
|
|||||||
auto objects = find_bp_objects(display_map, grid, static_cast<double>(live_config.gpr_object_min_frac));
|
auto objects = find_bp_objects(display_map, grid, static_cast<double>(live_config.gpr_object_min_frac));
|
||||||
add_local_prominence_metrics(objects, display_map, grid, min_depth_m, max_depth_m);
|
add_local_prominence_metrics(objects, display_map, grid, min_depth_m, max_depth_m);
|
||||||
add_incoherent_support_metrics(objects, incoherent_display_map, bp.coherence_factor);
|
add_incoherent_support_metrics(objects, incoherent_display_map, bp.coherence_factor);
|
||||||
mark_sidelobe_candidates(objects, selected_traces, selection, imaging_plane_y_m);
|
mark_sidelobe_candidates(objects, selected_traces, selection, plane);
|
||||||
add_bp_score_metrics(objects, live_config);
|
add_bp_score_metrics(objects, live_config);
|
||||||
|
|
||||||
if (live_config.gpr_remove_sidelobe_objects_enabled) {
|
if (live_config.gpr_remove_sidelobe_objects_enabled) {
|
||||||
|
|||||||
@@ -918,7 +918,12 @@ void apply_legacy_motion_correction(
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto grid = build_grid(selection, max_depth_m, kLegacyGridZMinM, imaging_plane_y_m);
|
// Legacy GPR always images in the fixed-y slice; the ground plane mode is a
|
||||||
|
// coherent-BP-only feature.
|
||||||
|
const auto grid = build_grid(
|
||||||
|
selection, max_depth_m, kLegacyGridZMinM,
|
||||||
|
ImagingPlane{false, imaging_plane_y_m, 0.0, 0.0}
|
||||||
|
);
|
||||||
if (grid.x_grid.empty() || grid.z_grid.empty()) {
|
if (grid.x_grid.empty() || grid.z_grid.empty()) {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <complex>
|
#include <complex>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
// Numeric check of the GPR backprojection frame-timing model against the
|
||||||
|
// Multi_antenna_3D notebook (compute_pair_timestamps_for_snapshot +
|
||||||
|
// apply_intra_sweep_phase_correction, 'segmented_interleaved').
|
||||||
|
//
|
||||||
|
// Builds a synthetic 4x4 collection with two segment windows taken from
|
||||||
|
// per-trace capture timestamps, runs build_frame_timing and
|
||||||
|
// apply_motion_correction from gpr_backprojection_processor.ipp, and prints
|
||||||
|
// segment/slot assignment, frame centre, per-Tx pair dtau and the motion phase
|
||||||
|
// at probe frequency indices. Compare against the Python replica
|
||||||
|
// (scripts/gpr_timing_check_reference.py) — every printed number must match.
|
||||||
|
//
|
||||||
|
// Also prints the fallback (zero timestamps) and legacy 'interleaved' layouts.
|
||||||
|
//
|
||||||
|
// Usage: gpr_timing_check
|
||||||
|
#include "gpr_processor.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <complex>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
|
#include <numeric>
|
||||||
|
#include <queue>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <tuple>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace radar::processing {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#include "../processors/src/gpr_backprojection_processor.ipp"
|
||||||
|
|
||||||
|
constexpr double kSpeedMps = 1.42;
|
||||||
|
constexpr double kLookAngleDeg = 19.5;
|
||||||
|
constexpr double kDirectionSign = 1.0;
|
||||||
|
constexpr std::size_t kFreqCount = 11U;
|
||||||
|
|
||||||
|
[[nodiscard]] auto make_run_config() -> config::RunConfig {
|
||||||
|
config::RunConfig run_config{};
|
||||||
|
for (std::uint32_t output = 0U; output < 4U; ++output) {
|
||||||
|
for (std::uint32_t input = 0U; input < 4U; ++input) {
|
||||||
|
run_config.run_combos.push_back(ipc::ComboKey{input, output});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return run_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto make_collection(bool with_trace_timestamps) -> ipc::PreprocessedCollection {
|
||||||
|
ipc::PreprocessedCollection collection{};
|
||||||
|
collection.collection_id = 7U;
|
||||||
|
// The collection-level capture starts 10 ms before the first trace — the
|
||||||
|
// trace marks, not the collection window, must define the frame model.
|
||||||
|
collection.capture_start_ns = 990'000'000ULL;
|
||||||
|
collection.capture_end_ns = 1'483'000'000ULL;
|
||||||
|
|
||||||
|
// Segment A (Tx0, Tx1): 1.000..1.223 s; segment B (Tx2, Tx3): 1.223..1.483 s
|
||||||
|
// (260 ms against the 241.5 ms norm -> stretched).
|
||||||
|
const auto window_for_tx = [&](std::uint32_t output) -> std::pair<std::uint64_t, std::uint64_t> {
|
||||||
|
if (!with_trace_timestamps) {
|
||||||
|
return {0U, 0U};
|
||||||
|
}
|
||||||
|
return output < 2U
|
||||||
|
? std::pair<std::uint64_t, std::uint64_t>{1'000'000'000ULL, 1'223'000'000ULL}
|
||||||
|
: std::pair<std::uint64_t, std::uint64_t>{1'223'000'000ULL, 1'483'000'000ULL};
|
||||||
|
};
|
||||||
|
|
||||||
|
for (std::uint32_t output = 0U; output < 4U; ++output) {
|
||||||
|
for (std::uint32_t input = 0U; input < 4U; ++input) {
|
||||||
|
ipc::SweepTraceBlock trace{};
|
||||||
|
trace.combo = ipc::ComboKey{input, output};
|
||||||
|
const auto window = window_for_tx(output);
|
||||||
|
trace.capture_start_ns = window.first;
|
||||||
|
trace.capture_end_ns = window.second;
|
||||||
|
collection.traces.push_back(std::move(trace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto make_trace(std::uint32_t output_pos) -> SelectedTrace {
|
||||||
|
SelectedTrace trace{};
|
||||||
|
trace.combo = ipc::ComboKey{0U, output_pos};
|
||||||
|
trace.frequency_hz.resize(kFreqCount);
|
||||||
|
trace.s21.assign(kFreqCount, std::complex<double>(1.0, 0.0));
|
||||||
|
for (std::size_t index = 0U; index < kFreqCount; ++index) {
|
||||||
|
trace.frequency_hz[index] =
|
||||||
|
1.5e9 + (static_cast<double>(index) * (6.0e9 - 1.5e9) / static_cast<double>(kFreqCount - 1U));
|
||||||
|
}
|
||||||
|
return trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_timing(const char* label, const FrameTiming& timing) {
|
||||||
|
std::printf("%s: frame_center_s=%.9f from_timestamps=%d\n",
|
||||||
|
label, timing.frame_center_s, timing.from_trace_timestamps ? 1 : 0);
|
||||||
|
for (std::size_t index = 0U; index < timing.segments.size(); ++index) {
|
||||||
|
const auto& segment = timing.segments[index];
|
||||||
|
std::printf(" segment %zu: start=%.9f end=%.9f center=%.9f stretched=%d\n",
|
||||||
|
index, segment.start_s, segment.end_s, segment.center_s,
|
||||||
|
segment.stretched ? 1 : 0);
|
||||||
|
}
|
||||||
|
std::vector<std::uint32_t> outputs{};
|
||||||
|
outputs.reserve(timing.by_output_pos.size());
|
||||||
|
for (const auto& [output, _slot] : timing.by_output_pos) {
|
||||||
|
outputs.push_back(output);
|
||||||
|
}
|
||||||
|
std::sort(outputs.begin(), outputs.end());
|
||||||
|
for (const auto output : outputs) {
|
||||||
|
const auto& slot = timing.by_output_pos.at(output);
|
||||||
|
std::printf(" tx o%u: segment=%zu slot=%zu slot_count=%zu\n",
|
||||||
|
output, slot.segment_index, slot.slot, slot.slot_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_motion_phase(const FrameTiming& timing, MotionMode motion_mode, const char* label) {
|
||||||
|
for (std::uint32_t output = 0U; output < 4U; ++output) {
|
||||||
|
auto trace = make_trace(output);
|
||||||
|
const auto& slot = timing.by_output_pos.at(output);
|
||||||
|
apply_motion_correction(
|
||||||
|
trace,
|
||||||
|
timing.segments[slot.segment_index],
|
||||||
|
slot.slot,
|
||||||
|
slot.slot_count,
|
||||||
|
timing.frame_center_s,
|
||||||
|
1.5e9,
|
||||||
|
6.0e9,
|
||||||
|
kSpeedOfLightMetersPerSec,
|
||||||
|
kSpeedMps,
|
||||||
|
kLookAngleDeg,
|
||||||
|
kDirectionSign,
|
||||||
|
motion_mode
|
||||||
|
);
|
||||||
|
// s21 started at 1+0j, so arg(s21[k]) = -phi[k] (notebook sign = -1).
|
||||||
|
std::printf("%s tx o%u: phi(k=0)=%.12e phi(k=5)=%.12e phi(k=10)=%.12e\n",
|
||||||
|
label, output,
|
||||||
|
-std::arg(trace.s21[0]), -std::arg(trace.s21[5]), -std::arg(trace.s21[10]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto run_check() -> int {
|
||||||
|
const auto run_config = make_run_config();
|
||||||
|
|
||||||
|
const auto timed = build_frame_timing(run_config, make_collection(true), 2U, true);
|
||||||
|
print_timing("segmented+timestamps", timed);
|
||||||
|
print_motion_phase(timed, MotionMode::IntMinus, "int_minus");
|
||||||
|
print_motion_phase(timed, MotionMode::IntFocus, "int_focus");
|
||||||
|
|
||||||
|
const auto fallback = build_frame_timing(run_config, make_collection(false), 2U, true);
|
||||||
|
print_timing("segmented+fallback", fallback);
|
||||||
|
print_motion_phase(fallback, MotionMode::IntMinus, "fallback int_minus");
|
||||||
|
|
||||||
|
const auto interleaved = build_frame_timing(run_config, make_collection(true), 2U, false);
|
||||||
|
print_timing("interleaved", interleaved);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace radar::processing
|
||||||
|
|
||||||
|
auto main() -> int {
|
||||||
|
return radar::processing::run_check();
|
||||||
|
}
|
||||||
@@ -64,6 +64,10 @@ _WEB_LIVE_SCHEMA = [
|
|||||||
("gpr_start_freq_mhz", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_start_freq_mhz", {"legacy_gpr": "_legacy_gpr_start_freq_mhz", "sar3d": "_sar3d_start_freq_mhz"})),
|
("gpr_start_freq_mhz", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_start_freq_mhz", {"legacy_gpr": "_legacy_gpr_start_freq_mhz", "sar3d": "_sar3d_start_freq_mhz"})),
|
||||||
("gpr_stop_freq_mhz", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_stop_freq_mhz", {"legacy_gpr": "_legacy_gpr_stop_freq_mhz", "sar3d": "_sar3d_stop_freq_mhz"})),
|
("gpr_stop_freq_mhz", "Geometry & depth", _GPR_LIKE_MODES, _dual("_gpr_stop_freq_mhz", {"legacy_gpr": "_legacy_gpr_stop_freq_mhz", "sar3d": "_sar3d_stop_freq_mhz"})),
|
||||||
("gpr_imaging_plane_y_m", "Geometry & depth", ("gpr",), _attr("_gpr_imaging_plane_y_m")),
|
("gpr_imaging_plane_y_m", "Geometry & depth", ("gpr",), _attr("_gpr_imaging_plane_y_m")),
|
||||||
|
("gpr_plane_mode", "Geometry & depth", ("gpr",), _attr("_gpr_plane_mode")),
|
||||||
|
("gpr_ground_depth_m", "Geometry & depth", ("gpr",), _attr("_gpr_ground_depth_m")),
|
||||||
|
("gpr_ground_pitch_override_enabled", "Geometry & depth", ("gpr",), _attr("_gpr_ground_pitch_override_enabled")),
|
||||||
|
("gpr_ground_pitch_deg", "Geometry & depth", ("gpr",), _attr("_gpr_ground_pitch_deg")),
|
||||||
("gpr_range_comp_power", "Imaging", ("gpr",), _attr("_gpr_range_comp_power")),
|
("gpr_range_comp_power", "Imaging", ("gpr",), _attr("_gpr_range_comp_power")),
|
||||||
("gpr_score_mode", "Imaging", ("gpr",), _attr("_gpr_score_mode")),
|
("gpr_score_mode", "Imaging", ("gpr",), _attr("_gpr_score_mode")),
|
||||||
("gpr_background_subtract_enabled", "Imaging", _GPR_MODES, _dual("_gpr_background_subtract_enabled", {"legacy_gpr": "_legacy_gpr_background_subtract_enabled"})),
|
("gpr_background_subtract_enabled", "Imaging", _GPR_MODES, _dual("_gpr_background_subtract_enabled", {"legacy_gpr": "_legacy_gpr_background_subtract_enabled"})),
|
||||||
@@ -82,6 +86,8 @@ _WEB_LIVE_SCHEMA = [
|
|||||||
("gpr_visible_z_min_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_min_m", {"legacy_gpr": "_legacy_gpr_visible_z_min_m"})),
|
("gpr_visible_z_min_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_min_m", {"legacy_gpr": "_legacy_gpr_visible_z_min_m"})),
|
||||||
("gpr_visible_z_max_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_max_m", {"legacy_gpr": "_legacy_gpr_visible_z_max_m"})),
|
("gpr_visible_z_max_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_max_m", {"legacy_gpr": "_legacy_gpr_visible_z_max_m"})),
|
||||||
("gpr_motion_mode", "Motion", ("gpr",), _attr("_gpr_motion_mode")),
|
("gpr_motion_mode", "Motion", ("gpr",), _attr("_gpr_motion_mode")),
|
||||||
|
("gpr_timing_mode", "Motion", ("gpr",), _attr("_gpr_timing_mode")),
|
||||||
|
("gpr_tx_per_segment", "Motion", ("gpr",), _attr("_gpr_tx_per_segment")),
|
||||||
("gpr_look_angle_deg", "Motion", _GPR_LIKE_MODES, _dual("_gpr_look_angle_deg", {"legacy_gpr": "_legacy_gpr_look_angle_deg", "sar3d": "_sar3d_look_angle_deg"})),
|
("gpr_look_angle_deg", "Motion", _GPR_LIKE_MODES, _dual("_gpr_look_angle_deg", {"legacy_gpr": "_legacy_gpr_look_angle_deg", "sar3d": "_sar3d_look_angle_deg"})),
|
||||||
("gpr_direction_sign", "Motion", ("gpr", "sar3d"), _dual("_gpr_direction_sign", {"sar3d": "_sar3d_direction_sign"})),
|
("gpr_direction_sign", "Motion", ("gpr", "sar3d"), _dual("_gpr_direction_sign", {"sar3d": "_sar3d_direction_sign"})),
|
||||||
("gpr_apply_freq_phase_correction", "Motion", ("legacy_gpr", "sar3d"), _dual("_legacy_gpr_apply_freq_phase_correction", {"sar3d": "_sar3d_apply_freq_phase_correction"})),
|
("gpr_apply_freq_phase_correction", "Motion", ("legacy_gpr", "sar3d"), _dual("_legacy_gpr_apply_freq_phase_correction", {"sar3d": "_sar3d_apply_freq_phase_correction"})),
|
||||||
|
|||||||
@@ -309,6 +309,12 @@ class AppWindowConfigProfileIOMixin:
|
|||||||
self._gpr_background_mean_count,
|
self._gpr_background_mean_count,
|
||||||
self._gpr_remove_sidelobe_objects_enabled,
|
self._gpr_remove_sidelobe_objects_enabled,
|
||||||
self._gpr_imaging_plane_y_m,
|
self._gpr_imaging_plane_y_m,
|
||||||
|
self._gpr_timing_mode,
|
||||||
|
self._gpr_tx_per_segment,
|
||||||
|
self._gpr_plane_mode,
|
||||||
|
self._gpr_ground_depth_m,
|
||||||
|
self._gpr_ground_pitch_override_enabled,
|
||||||
|
self._gpr_ground_pitch_deg,
|
||||||
self._gpr_render_mode,
|
self._gpr_render_mode,
|
||||||
self._gpr_visible_x_min_m,
|
self._gpr_visible_x_min_m,
|
||||||
self._gpr_visible_x_max_m,
|
self._gpr_visible_x_max_m,
|
||||||
@@ -507,6 +513,16 @@ class AppWindowConfigProfileIOMixin:
|
|||||||
bool(gui_state.processing.gpr.remove_sidelobe_objects_enabled)
|
bool(gui_state.processing.gpr.remove_sidelobe_objects_enabled)
|
||||||
)
|
)
|
||||||
self._gpr_imaging_plane_y_m.setValue(float(gui_state.processing.gpr.imaging_plane_y_m))
|
self._gpr_imaging_plane_y_m.setValue(float(gui_state.processing.gpr.imaging_plane_y_m))
|
||||||
|
self._set_combo_current_text(self._gpr_timing_mode, gui_state.processing.gpr.timing_mode)
|
||||||
|
self._gpr_tx_per_segment.setValue(int(gui_state.processing.gpr.tx_per_segment))
|
||||||
|
self._set_combo_current_text(self._gpr_plane_mode, gui_state.processing.gpr.plane_mode)
|
||||||
|
self._gpr_ground_depth_m.setValue(float(gui_state.processing.gpr.ground_depth_m))
|
||||||
|
ground_pitch_override_enabled = bool(
|
||||||
|
gui_state.processing.gpr.ground_pitch_override_enabled
|
||||||
|
)
|
||||||
|
self._gpr_ground_pitch_override_enabled.setChecked(ground_pitch_override_enabled)
|
||||||
|
self._gpr_ground_pitch_deg.setValue(float(gui_state.processing.gpr.ground_pitch_deg))
|
||||||
|
self._gpr_ground_pitch_deg.setEnabled(ground_pitch_override_enabled)
|
||||||
self._set_combo_current_text(self._gpr_render_mode, gui_state.processing.gpr.render_mode)
|
self._set_combo_current_text(self._gpr_render_mode, gui_state.processing.gpr.render_mode)
|
||||||
self._gpr_visible_x_min_m.setValue(float(gui_state.processing.gpr.visible_x_min_m))
|
self._gpr_visible_x_min_m.setValue(float(gui_state.processing.gpr.visible_x_min_m))
|
||||||
self._gpr_visible_x_max_m.setValue(float(gui_state.processing.gpr.visible_x_max_m))
|
self._gpr_visible_x_max_m.setValue(float(gui_state.processing.gpr.visible_x_max_m))
|
||||||
|
|||||||
@@ -391,6 +391,12 @@ class AppWindowConfigStateBuildersMixin:
|
|||||||
background_mean_count=int(self._gpr_background_mean_count.value()),
|
background_mean_count=int(self._gpr_background_mean_count.value()),
|
||||||
remove_sidelobe_objects_enabled=bool(self._gpr_remove_sidelobe_objects_enabled.isChecked()),
|
remove_sidelobe_objects_enabled=bool(self._gpr_remove_sidelobe_objects_enabled.isChecked()),
|
||||||
imaging_plane_y_m=float(self._gpr_imaging_plane_y_m.value()),
|
imaging_plane_y_m=float(self._gpr_imaging_plane_y_m.value()),
|
||||||
|
timing_mode=self._gpr_timing_mode.currentText(),
|
||||||
|
tx_per_segment=int(self._gpr_tx_per_segment.value()),
|
||||||
|
plane_mode=self._gpr_plane_mode.currentText(),
|
||||||
|
ground_depth_m=float(self._gpr_ground_depth_m.value()),
|
||||||
|
ground_pitch_override_enabled=bool(self._gpr_ground_pitch_override_enabled.isChecked()),
|
||||||
|
ground_pitch_deg=float(self._gpr_ground_pitch_deg.value()),
|
||||||
render_mode=self._gpr_render_mode.currentText(),
|
render_mode=self._gpr_render_mode.currentText(),
|
||||||
visible_x_min_m=float(self._gpr_visible_x_min_m.value()),
|
visible_x_min_m=float(self._gpr_visible_x_min_m.value()),
|
||||||
visible_x_max_m=float(self._gpr_visible_x_max_m.value()),
|
visible_x_max_m=float(self._gpr_visible_x_max_m.value()),
|
||||||
|
|||||||
@@ -376,9 +376,58 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
owner._gpr_imaging_plane_y_m.setSingleStep(0.05)
|
owner._gpr_imaging_plane_y_m.setSingleStep(0.05)
|
||||||
owner._gpr_imaging_plane_y_m.setValue(float(gpr_live_defaults.imaging_plane_y_m))
|
owner._gpr_imaging_plane_y_m.setValue(float(gpr_live_defaults.imaging_plane_y_m))
|
||||||
owner._gpr_imaging_plane_y_m.setToolTip(
|
owner._gpr_imaging_plane_y_m.setToolTip(
|
||||||
"Y coordinate of the BP imaging slice (m). Use 0 for legacy 1D antenna layouts."
|
"Y coordinate of the BP imaging slice (m) in 'const' plane mode. "
|
||||||
|
"Use 0 for legacy 1D antenna layouts."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
owner._gpr_timing_mode = QComboBox()
|
||||||
|
owner._gpr_timing_mode.addItems(["segmented_interleaved", "interleaved"])
|
||||||
|
owner._set_combo_current_text(owner._gpr_timing_mode, gpr_live_defaults.timing_mode)
|
||||||
|
owner._gpr_timing_mode.setToolTip(
|
||||||
|
"Acquisition timing model for motion correction: 'segmented_interleaved' — Tx run "
|
||||||
|
"in sequential segments (real hardware), 'interleaved' — legacy whole-frame model."
|
||||||
|
)
|
||||||
|
|
||||||
|
owner._gpr_tx_per_segment = QSpinBox()
|
||||||
|
owner._gpr_tx_per_segment.setRange(1, 64)
|
||||||
|
owner._gpr_tx_per_segment.setValue(int(gpr_live_defaults.tx_per_segment))
|
||||||
|
owner._gpr_tx_per_segment.setToolTip(
|
||||||
|
"Fallback Tx count per segment when per-trace capture timestamps are absent; "
|
||||||
|
"timestamp grouping wins when available."
|
||||||
|
)
|
||||||
|
|
||||||
|
owner._gpr_plane_mode = QComboBox()
|
||||||
|
owner._gpr_plane_mode.addItems(["ground", "const"])
|
||||||
|
owner._set_combo_current_text(owner._gpr_plane_mode, gpr_live_defaults.plane_mode)
|
||||||
|
owner._gpr_plane_mode.setToolTip(
|
||||||
|
"BP imaging plane: 'ground' follows the ground given the frame pitch "
|
||||||
|
"(needed with vertically spread antennas), 'const' is the legacy fixed Y."
|
||||||
|
)
|
||||||
|
|
||||||
|
owner._gpr_ground_depth_m = QDoubleSpinBox()
|
||||||
|
owner._gpr_ground_depth_m.setDecimals(3)
|
||||||
|
owner._gpr_ground_depth_m.setRange(0.0, 50.0)
|
||||||
|
owner._gpr_ground_depth_m.setSingleStep(0.05)
|
||||||
|
owner._gpr_ground_depth_m.setValue(float(gpr_live_defaults.ground_depth_m))
|
||||||
|
owner._gpr_ground_depth_m.setToolTip(
|
||||||
|
"Height of the lower antenna row above the ground (m); structural constant."
|
||||||
|
)
|
||||||
|
|
||||||
|
owner._gpr_ground_pitch_override_enabled = QCheckBox("Override ground pitch")
|
||||||
|
owner._gpr_ground_pitch_override_enabled.setChecked(
|
||||||
|
bool(gpr_live_defaults.ground_pitch_override_enabled)
|
||||||
|
)
|
||||||
|
owner._gpr_ground_pitch_override_enabled.setToolTip(
|
||||||
|
"Unchecked: the ground plane uses 'Look angle deg' as the frame pitch."
|
||||||
|
)
|
||||||
|
|
||||||
|
owner._gpr_ground_pitch_deg = QDoubleSpinBox()
|
||||||
|
owner._gpr_ground_pitch_deg.setDecimals(2)
|
||||||
|
owner._gpr_ground_pitch_deg.setRange(-90.0, 90.0)
|
||||||
|
owner._gpr_ground_pitch_deg.setSingleStep(0.1)
|
||||||
|
owner._gpr_ground_pitch_deg.setValue(float(gpr_live_defaults.ground_pitch_deg))
|
||||||
|
owner._gpr_ground_pitch_deg.setEnabled(bool(gpr_live_defaults.ground_pitch_override_enabled))
|
||||||
|
|
||||||
gpr_page = _build_processing_mode_page(
|
gpr_page = _build_processing_mode_page(
|
||||||
owner._processing_mode_pages,
|
owner._processing_mode_pages,
|
||||||
[
|
[
|
||||||
@@ -400,6 +449,12 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
("Approach min frames", owner._gpr_object_approach_min_frames),
|
("Approach min frames", owner._gpr_object_approach_min_frames),
|
||||||
("Start MHz", owner._gpr_start_freq_mhz),
|
("Start MHz", owner._gpr_start_freq_mhz),
|
||||||
("Stop MHz", owner._gpr_stop_freq_mhz),
|
("Stop MHz", owner._gpr_stop_freq_mhz),
|
||||||
|
("Timing mode", owner._gpr_timing_mode),
|
||||||
|
("Tx per segment", owner._gpr_tx_per_segment),
|
||||||
|
("Plane mode", owner._gpr_plane_mode),
|
||||||
|
("Ground depth m", owner._gpr_ground_depth_m),
|
||||||
|
owner._gpr_ground_pitch_override_enabled,
|
||||||
|
("Ground pitch deg", owner._gpr_ground_pitch_deg),
|
||||||
("Imaging plane Y m", owner._gpr_imaging_plane_y_m),
|
("Imaging plane Y m", owner._gpr_imaging_plane_y_m),
|
||||||
("Visible X min m", owner._gpr_visible_x_min_m),
|
("Visible X min m", owner._gpr_visible_x_min_m),
|
||||||
("Visible X max m", owner._gpr_visible_x_max_m),
|
("Visible X max m", owner._gpr_visible_x_max_m),
|
||||||
@@ -793,6 +848,13 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
owner._gpr_background_mean_count.valueChanged.connect(owner._on_processing_live_settings_changed)
|
owner._gpr_background_mean_count.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
owner._gpr_remove_sidelobe_objects_enabled.toggled.connect(owner._on_processing_live_settings_changed)
|
owner._gpr_remove_sidelobe_objects_enabled.toggled.connect(owner._on_processing_live_settings_changed)
|
||||||
owner._gpr_imaging_plane_y_m.valueChanged.connect(owner._on_processing_live_settings_changed)
|
owner._gpr_imaging_plane_y_m.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
|
owner._gpr_timing_mode.currentTextChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
|
owner._gpr_tx_per_segment.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
|
owner._gpr_plane_mode.currentTextChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
|
owner._gpr_ground_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
|
owner._gpr_ground_pitch_override_enabled.toggled.connect(owner._gpr_ground_pitch_deg.setEnabled)
|
||||||
|
owner._gpr_ground_pitch_override_enabled.toggled.connect(owner._on_processing_live_settings_changed)
|
||||||
|
owner._gpr_ground_pitch_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
owner._gpr_render_mode.currentTextChanged.connect(owner._on_gpr_visual_settings_changed)
|
owner._gpr_render_mode.currentTextChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||||
owner._gpr_max_detected_objects_to_draw.valueChanged.connect(owner._on_gpr_locator_threshold_changed)
|
owner._gpr_max_detected_objects_to_draw.valueChanged.connect(owner._on_gpr_locator_threshold_changed)
|
||||||
owner._gpr_draw_top_m_objects.valueChanged.connect(owner._on_gpr_locator_threshold_changed)
|
owner._gpr_draw_top_m_objects.valueChanged.connect(owner._on_gpr_locator_threshold_changed)
|
||||||
|
|||||||
@@ -380,6 +380,42 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
|||||||
gui.processing.gpr.imaging_plane_y_m,
|
gui.processing.gpr.imaging_plane_y_m,
|
||||||
"gui.processing.gpr",
|
"gui.processing.gpr",
|
||||||
),
|
),
|
||||||
|
timing_mode=_optional_string(
|
||||||
|
gpr_object,
|
||||||
|
"timing_mode",
|
||||||
|
gui.processing.gpr.timing_mode,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
tx_per_segment=_optional_int(
|
||||||
|
gpr_object,
|
||||||
|
"tx_per_segment",
|
||||||
|
gui.processing.gpr.tx_per_segment,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
plane_mode=_optional_string(
|
||||||
|
gpr_object,
|
||||||
|
"plane_mode",
|
||||||
|
gui.processing.gpr.plane_mode,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
ground_depth_m=_optional_float(
|
||||||
|
gpr_object,
|
||||||
|
"ground_depth_m",
|
||||||
|
gui.processing.gpr.ground_depth_m,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
ground_pitch_override_enabled=_optional_bool(
|
||||||
|
gpr_object,
|
||||||
|
"ground_pitch_override_enabled",
|
||||||
|
gui.processing.gpr.ground_pitch_override_enabled,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
ground_pitch_deg=_optional_float(
|
||||||
|
gpr_object,
|
||||||
|
"ground_pitch_deg",
|
||||||
|
gui.processing.gpr.ground_pitch_deg,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
render_mode=_optional_string(
|
render_mode=_optional_string(
|
||||||
gpr_object,
|
gpr_object,
|
||||||
"render_mode",
|
"render_mode",
|
||||||
@@ -598,6 +634,16 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
|||||||
raise ValueError("gui.processing.gpr.score_mode must be one of: peak, combined")
|
raise ValueError("gui.processing.gpr.score_mode must be one of: peak, combined")
|
||||||
if gui.processing.gpr.motion_mode not in {"int_minus", "int_focus"}:
|
if gui.processing.gpr.motion_mode not in {"int_minus", "int_focus"}:
|
||||||
raise ValueError("gui.processing.gpr.motion_mode must be one of: int_minus, int_focus")
|
raise ValueError("gui.processing.gpr.motion_mode must be one of: int_minus, int_focus")
|
||||||
|
if gui.processing.gpr.timing_mode not in {"segmented_interleaved", "interleaved"}:
|
||||||
|
raise ValueError(
|
||||||
|
"gui.processing.gpr.timing_mode must be one of: segmented_interleaved, interleaved"
|
||||||
|
)
|
||||||
|
if gui.processing.gpr.tx_per_segment < 1:
|
||||||
|
raise ValueError("gui.processing.gpr.tx_per_segment must be >= 1")
|
||||||
|
if gui.processing.gpr.plane_mode not in {"ground", "const"}:
|
||||||
|
raise ValueError("gui.processing.gpr.plane_mode must be one of: ground, const")
|
||||||
|
if gui.processing.gpr.ground_depth_m < 0.0:
|
||||||
|
raise ValueError("gui.processing.gpr.ground_depth_m must be >= 0")
|
||||||
if gui.processing.legacy_gpr.mode not in {"point", "extended"}:
|
if gui.processing.legacy_gpr.mode not in {"point", "extended"}:
|
||||||
raise ValueError("gui.processing.legacy_gpr.mode must be one of: point, extended")
|
raise ValueError("gui.processing.legacy_gpr.mode must be one of: point, extended")
|
||||||
if gui.processing.legacy_gpr.render_mode not in {"heatmap", "objects_only"}:
|
if gui.processing.legacy_gpr.render_mode not in {"heatmap", "objects_only"}:
|
||||||
@@ -750,6 +796,12 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]:
|
|||||||
"background_mean_count": gui.processing.gpr.background_mean_count,
|
"background_mean_count": gui.processing.gpr.background_mean_count,
|
||||||
"remove_sidelobe_objects_enabled": gui.processing.gpr.remove_sidelobe_objects_enabled,
|
"remove_sidelobe_objects_enabled": gui.processing.gpr.remove_sidelobe_objects_enabled,
|
||||||
"imaging_plane_y_m": gui.processing.gpr.imaging_plane_y_m,
|
"imaging_plane_y_m": gui.processing.gpr.imaging_plane_y_m,
|
||||||
|
"timing_mode": gui.processing.gpr.timing_mode,
|
||||||
|
"tx_per_segment": gui.processing.gpr.tx_per_segment,
|
||||||
|
"plane_mode": gui.processing.gpr.plane_mode,
|
||||||
|
"ground_depth_m": gui.processing.gpr.ground_depth_m,
|
||||||
|
"ground_pitch_override_enabled": gui.processing.gpr.ground_pitch_override_enabled,
|
||||||
|
"ground_pitch_deg": gui.processing.gpr.ground_pitch_deg,
|
||||||
"render_mode": gui.processing.gpr.render_mode,
|
"render_mode": gui.processing.gpr.render_mode,
|
||||||
"visible_x_min_m": gui.processing.gpr.visible_x_min_m,
|
"visible_x_min_m": gui.processing.gpr.visible_x_min_m,
|
||||||
"visible_x_max_m": gui.processing.gpr.visible_x_max_m,
|
"visible_x_max_m": gui.processing.gpr.visible_x_max_m,
|
||||||
|
|||||||
@@ -87,6 +87,18 @@ class GuiGprStateModel:
|
|||||||
background_mean_count: int = 10
|
background_mean_count: int = 10
|
||||||
remove_sidelobe_objects_enabled: bool = True
|
remove_sidelobe_objects_enabled: bool = True
|
||||||
imaging_plane_y_m: float = 0.0
|
imaging_plane_y_m: float = 0.0
|
||||||
|
# Acquisition timing model for motion correction: "segmented_interleaved"
|
||||||
|
# (real hardware: Tx segments run sequentially) or "interleaved" (legacy:
|
||||||
|
# all Tx interleave over the whole frame).
|
||||||
|
timing_mode: str = "segmented_interleaved"
|
||||||
|
# Fallback segment size when per-trace capture timestamps are absent.
|
||||||
|
tx_per_segment: int = 2
|
||||||
|
# Imaging plane: "ground" follows the ground given the frame pitch;
|
||||||
|
# "const" is the legacy fixed y = imaging_plane_y_m.
|
||||||
|
plane_mode: str = "ground"
|
||||||
|
ground_depth_m: float = 1.25
|
||||||
|
ground_pitch_override_enabled: bool = False
|
||||||
|
ground_pitch_deg: float = 6.5
|
||||||
render_mode: str = "heatmap"
|
render_mode: str = "heatmap"
|
||||||
visible_x_min_m: float = -2.0
|
visible_x_min_m: float = -2.0
|
||||||
visible_x_max_m: float = 2.0
|
visible_x_max_m: float = 2.0
|
||||||
|
|||||||
@@ -62,6 +62,25 @@ class ProcessingLiveConfig:
|
|||||||
gpr_background_mean_count: int = 10
|
gpr_background_mean_count: int = 10
|
||||||
gpr_remove_sidelobe_objects_enabled: bool = True
|
gpr_remove_sidelobe_objects_enabled: bool = True
|
||||||
gpr_imaging_plane_y_m: float = 0.0
|
gpr_imaging_plane_y_m: float = 0.0
|
||||||
|
# Acquisition timing model for backprojection motion correction:
|
||||||
|
# "segmented_interleaved" — Tx outputs run in sequential segments, each
|
||||||
|
# segment sweeps all frequencies over its window while its Tx interleave
|
||||||
|
# per frequency (real hardware scheme, notebook FREQUENCY_TIMING_MODE);
|
||||||
|
# "interleaved" — legacy model, all Tx interleave over the whole frame.
|
||||||
|
gpr_timing_mode: str = "segmented_interleaved"
|
||||||
|
# Fallback segment size when per-trace capture timestamps are absent;
|
||||||
|
# timestamp grouping wins when available.
|
||||||
|
gpr_tx_per_segment: int = 2
|
||||||
|
# Imaging plane for the 2D BP map: "ground" — plane follows the ground
|
||||||
|
# given the frame pitch (notebook BP_PLANE_MODE='ground'), "const" —
|
||||||
|
# legacy fixed y = gpr_imaging_plane_y_m.
|
||||||
|
gpr_plane_mode: str = "ground"
|
||||||
|
# Height of the LOWER antenna row above the ground, metres (structural
|
||||||
|
# constant, notebook BP_GROUND_DEPTH_M).
|
||||||
|
gpr_ground_depth_m: float = 1.25
|
||||||
|
# Frame pitch used by the ground plane. Disabled -> gpr_look_angle_deg.
|
||||||
|
gpr_ground_pitch_override_enabled: bool = False
|
||||||
|
gpr_ground_pitch_deg: float = 6.5
|
||||||
# Locator filter parameter consumed by the C++ TCP locator server. Coherent BP
|
# Locator filter parameter consumed by the C++ TCP locator server. Coherent BP
|
||||||
# objects are already finalized in the processor (no score threshold); only legacy
|
# objects are already finalized in the processor (no score threshold); only legacy
|
||||||
# GPR still thresholds, on a pair count.
|
# GPR still thresholds, on a pair count.
|
||||||
@@ -149,6 +168,12 @@ class ProcessingLiveConfig:
|
|||||||
"gpr_background_mean_count": int(self.gpr_background_mean_count),
|
"gpr_background_mean_count": int(self.gpr_background_mean_count),
|
||||||
"gpr_remove_sidelobe_objects_enabled": bool(self.gpr_remove_sidelobe_objects_enabled),
|
"gpr_remove_sidelobe_objects_enabled": bool(self.gpr_remove_sidelobe_objects_enabled),
|
||||||
"gpr_imaging_plane_y_m": float(self.gpr_imaging_plane_y_m),
|
"gpr_imaging_plane_y_m": float(self.gpr_imaging_plane_y_m),
|
||||||
|
"gpr_timing_mode": str(self.gpr_timing_mode),
|
||||||
|
"gpr_tx_per_segment": int(self.gpr_tx_per_segment),
|
||||||
|
"gpr_plane_mode": str(self.gpr_plane_mode),
|
||||||
|
"gpr_ground_depth_m": float(self.gpr_ground_depth_m),
|
||||||
|
"gpr_ground_pitch_override_enabled": bool(self.gpr_ground_pitch_override_enabled),
|
||||||
|
"gpr_ground_pitch_deg": float(self.gpr_ground_pitch_deg),
|
||||||
"legacy_gpr_min_visible_pair_count": float(self.legacy_gpr_min_visible_pair_count),
|
"legacy_gpr_min_visible_pair_count": float(self.legacy_gpr_min_visible_pair_count),
|
||||||
"gpr_visible_x_min_m": float(self.gpr_visible_x_min_m),
|
"gpr_visible_x_min_m": float(self.gpr_visible_x_min_m),
|
||||||
"gpr_visible_x_max_m": float(self.gpr_visible_x_max_m),
|
"gpr_visible_x_max_m": float(self.gpr_visible_x_max_m),
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Notebook-formula reference for the C++ `gpr_timing_check` tool.
|
||||||
|
|
||||||
|
Recomputes the same synthetic 4x4 scenario with the Multi_antenna_3D notebook
|
||||||
|
formulas (compute_pair_timestamps_for_snapshot + compute_frequency_sample_times
|
||||||
|
+ apply_intra_sweep_phase_correction, 'segmented_interleaved' branch) and
|
||||||
|
prints the motion phase at the same probe indices in the same format. Run:
|
||||||
|
|
||||||
|
make gpr_timing_check && build/bin/gpr_timing_check > /tmp/cpp.txt
|
||||||
|
python python_app/scripts/gpr_timing_check_reference.py > /tmp/ref.txt
|
||||||
|
|
||||||
|
The `int_minus` / `int_focus` lines of both outputs must match to ~1e-9 rad
|
||||||
|
(phases are compared wrapped to (-pi, pi]).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
SPEED_OF_LIGHT = 299_792_458.0 # C++ kSpeedOfLightMetersPerSec
|
||||||
|
SPEED_M_S = 1.42
|
||||||
|
LOOK_ANGLE_DEG = 19.5
|
||||||
|
DIRECTION_SIGN = 1.0
|
||||||
|
F_START = 1.5e9
|
||||||
|
F_STOP = 6.0e9
|
||||||
|
N_FREQ = 11
|
||||||
|
|
||||||
|
# Trace windows of the synthetic collection (absolute seconds): segment A is
|
||||||
|
# Tx0/Tx1, segment B is Tx2/Tx3 (260 ms, stretched).
|
||||||
|
WINDOWS = {0: (1.000, 1.223), 1: (1.000, 1.223), 2: (1.223, 1.483), 3: (1.223, 1.483)}
|
||||||
|
TX_ORDER = [0, 1, 2, 3]
|
||||||
|
PROBE_INDICES = (0, 5, 10)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap(phase: np.ndarray) -> np.ndarray:
|
||||||
|
"""Wrap phases to (-pi, pi] the way -arg(exp(-1j*phi)) reports them."""
|
||||||
|
return -np.angle(np.exp(-1j * phase))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
freqs = np.linspace(F_START, F_STOP, N_FREQ)
|
||||||
|
first = min(start for start, _ in WINDOWS.values())
|
||||||
|
last = max(end for _, end in WINDOWS.values())
|
||||||
|
acq_s = last - first
|
||||||
|
t_ref = 0.5 * acq_s
|
||||||
|
|
||||||
|
# Group Tx with identical windows into segments (snapshot_acquisition_timing
|
||||||
|
# + compute_pair_timestamps_for_snapshot).
|
||||||
|
groups: dict[tuple[float, float], list[int]] = {}
|
||||||
|
for tx in TX_ORDER:
|
||||||
|
groups.setdefault(WINDOWS[tx], []).append(tx)
|
||||||
|
seg_of = {}
|
||||||
|
for window in sorted(groups):
|
||||||
|
for slot, tx in enumerate(groups[window]):
|
||||||
|
seg_of[tx] = (window, slot, len(groups[window]))
|
||||||
|
|
||||||
|
cos_theta = np.cos(np.radians(LOOK_ANGLE_DEG))
|
||||||
|
motion_factor = DIRECTION_SIGN * SPEED_M_S * cos_theta
|
||||||
|
|
||||||
|
print(f"reference: frame_center_s={t_ref:.9f} acq_s={acq_s:.9f}")
|
||||||
|
phi_by_tx = {}
|
||||||
|
for tx in TX_ORDER:
|
||||||
|
(t0_abs, t1_abs), slot, n_slots = seg_of[tx]
|
||||||
|
seg_start = t0_abs - first
|
||||||
|
seg_end = t1_abs - first
|
||||||
|
t_seg = seg_end - seg_start
|
||||||
|
t_center = 0.5 * (seg_start + seg_end)
|
||||||
|
|
||||||
|
# compute_frequency_sample_times, segmented branch.
|
||||||
|
dt_event = t_seg / (n_slots * N_FREQ)
|
||||||
|
t_abs = seg_start + (n_slots * np.arange(N_FREQ) + slot) * dt_event
|
||||||
|
|
||||||
|
dt_intra = t_abs - t_center
|
||||||
|
dtau_intra = 2.0 * motion_factor * dt_intra / SPEED_OF_LIGHT
|
||||||
|
dt_pair = t_center - t_ref
|
||||||
|
dtau_pair = 2.0 * motion_factor * dt_pair / SPEED_OF_LIGHT
|
||||||
|
phi = 2.0 * np.pi * freqs * (dtau_intra + dtau_pair)
|
||||||
|
phi_by_tx[tx] = phi
|
||||||
|
|
||||||
|
wrapped = wrap(phi)
|
||||||
|
print(
|
||||||
|
f"int_minus tx o{tx}: "
|
||||||
|
+ " ".join(f"phi(k={k})={wrapped[k]:.12e}" for k in PROBE_INDICES)
|
||||||
|
)
|
||||||
|
|
||||||
|
for tx in TX_ORDER:
|
||||||
|
phi = phi_by_tx[tx]
|
||||||
|
# int_focus: remove the constant + linear-in-frequency component
|
||||||
|
# (least squares on [1, f_rel], notebook intra_phase_focus_only).
|
||||||
|
f_rel = freqs - freqs.mean()
|
||||||
|
design = np.column_stack([np.ones_like(f_rel), f_rel])
|
||||||
|
beta, *_ = np.linalg.lstsq(design, phi, rcond=None)
|
||||||
|
residual = phi - design @ beta
|
||||||
|
wrapped = wrap(residual)
|
||||||
|
print(
|
||||||
|
f"int_focus tx o{tx}: "
|
||||||
|
+ " ".join(f"phi(k={k})={wrapped[k]:.12e}" for k in PROBE_INDICES)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -68,6 +68,18 @@ class GuiProfileCodecTest(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError): # combos_text must be a JSON string, not a number
|
with self.assertRaises(ValueError): # combos_text must be a JSON string, not a number
|
||||||
GuiProfileModel.from_dict({"gui": {"switches": {"combos_text": 123}}})
|
GuiProfileModel.from_dict({"gui": {"switches": {"combos_text": 123}}})
|
||||||
|
|
||||||
|
def test_invalid_gpr_timing_mode_is_rejected(self) -> None:
|
||||||
|
with self.assertRaisesRegex(ValueError, "timing_mode"):
|
||||||
|
GuiProfileModel.from_dict({"gui": {"processing": {"gpr": {"timing_mode": "bogus"}}}})
|
||||||
|
|
||||||
|
def test_invalid_gpr_plane_mode_is_rejected(self) -> None:
|
||||||
|
with self.assertRaisesRegex(ValueError, "plane_mode"):
|
||||||
|
GuiProfileModel.from_dict({"gui": {"processing": {"gpr": {"plane_mode": "bogus"}}}})
|
||||||
|
|
||||||
|
def test_invalid_gpr_tx_per_segment_is_rejected(self) -> None:
|
||||||
|
with self.assertRaisesRegex(ValueError, "tx_per_segment"):
|
||||||
|
GuiProfileModel.from_dict({"gui": {"processing": {"gpr": {"tx_per_segment": 0}}}})
|
||||||
|
|
||||||
def test_legacy_gpr_payload_migrates_selected_mode(self) -> None:
|
def test_legacy_gpr_payload_migrates_selected_mode(self) -> None:
|
||||||
# An old payload that selects 'gpr' but carries a legacy root gpr.mode is migrated.
|
# An old payload that selects 'gpr' but carries a legacy root gpr.mode is migrated.
|
||||||
decoded = GuiProfileModel.from_dict({
|
decoded = GuiProfileModel.from_dict({
|
||||||
|
|||||||
Reference in New Issue
Block a user