diff --git a/Makefile b/Makefile index 84e4c0d..1a31ce8 100644 --- a/Makefile +++ b/Makefile @@ -139,7 +139,7 @@ TARGETS := \ $(BIN_DIR)/data_preprocessor \ $(BIN_DIR)/data_processor -.PHONY: all clean kamil_adc_collector sar3d_parity gpr_timing_check +.PHONY: all clean kamil_adc_collector sar3d_parity all: $(TARGETS) @@ -157,20 +157,6 @@ $(BIN_DIR)/sar3d_parity: $(SAR3D_PARITY_OBJS) $(GPR_CPP_LIB) @mkdir -p $(BIN_DIR) $(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) @mkdir -p $(BIN_DIR) $(CXX) $(SWEEP_ORCH_OBJS) -o $@ $(ORCH_LDFLAGS) diff --git a/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp b/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp index 2123074..d55a605 100644 --- a/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp +++ b/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp @@ -74,28 +74,7 @@ struct ProcessingLiveConfig { bool gpr_remove_sidelobe_objects_enabled = true; // 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. - // Used only when gpr_plane_mode == "const". 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 // the processor itself, matching Horns_motion_3libre.py — there is NO score // threshold for it. Only legacy GPR still thresholds, on a pair count. diff --git a/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp b/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp index a566241..2b236ca 100644 --- a/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp +++ b/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp @@ -67,20 +67,6 @@ using Json = nlohmann::json; 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) { if (value == "backprojection") { return; @@ -369,45 +355,6 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s } config.gpr_imaging_plane_y_m = static_cast(found->get()); } - 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(), "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(), "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(found->get()); - 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(); - } - 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(found->get()); - } for (const auto& [key, target] : { 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}, diff --git a/data_acq_and_processing/processing/processors/src/gpr_backprojection_processor.ipp b/data_acq_and_processing/processing/processors/src/gpr_backprojection_processor.ipp index 43f4ae5..868f63f 100644 --- a/data_acq_and_processing/processing/processors/src/gpr_backprojection_processor.ipp +++ b/data_acq_and_processing/processing/processors/src/gpr_backprojection_processor.ipp @@ -653,242 +653,52 @@ void validate_collection_trace_order( return traces; } -// ── Frame acquisition timing ──────────────────────────────────────────────── -// -// The real acquisition is SEGMENTED (notebook FREQUENCY_TIMING_MODE = -// 'segmented_interleaved'): the Tx outputs are grouped into segments that run -// one after another; each segment sweeps ALL frequencies over its own window, -// and within a segment its Tx interleave per frequency while all Rx receive in -// parallel. The dominant motion shift is therefore BETWEEN segments (a pair- -// 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 segments{}; - std::unordered_map 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 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(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> 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::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(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> 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(std::max(windows.size(), 1U)); - timing.segments.reserve(windows.size()); - for (const auto& window : windows) { - SegmentWindow segment{}; - segment.start_s = static_cast(window.first - first_start_ns) * 1e-9; - segment.end_s = static_cast(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::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::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(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(segment_count); - timing.segments.reserve(segment_count); - for (std::size_t index = 0U; index < segment_count; ++index) { - SegmentWindow segment{}; - segment.start_s = static_cast(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. +// 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. // -// 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( +// Unlike the offline Python script, every physical input is taken from the live +// system: `speed_mps` is the socket-fed velocity, `look_angle_deg`/`direction_sign` +// are config, and `tx_sweep_time_s` is derived per-collection from the acquisition +// timestamps (capture_span / Tx count). A zero speed or non-positive sweep time +// makes this a no-op. +void apply_intra_sweep_motion_correction( SelectedTrace& trace, - const SegmentWindow& segment, std::size_t slot, std::size_t slot_count, - double frame_center_s, double start_hz, double stop_hz, double velocity_mps, double speed_mps, double look_angle_deg, double direction_sign, + double tx_sweep_time_s, MotionMode motion_mode ) { 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 - || !(velocity_mps > 0.0) || !(segment_span_s > 0.0)) { + || !(velocity_mps > 0.0) || !(tx_sweep_time_s > 0.0)) { return; } const double low_hz = std::min(start_hz, stop_hz); const double high_hz = std::max(start_hz, stop_hz); - // The segment sweeps all frequencies over its window; on each frequency its - // 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(slot_count) * static_cast(n_full)); + // Absolute sample time of full-sweep frequency index k for this Tx slot. + const double dt_base = tx_sweep_time_s / static_cast(n_full - 1U); const auto sample_time_s = [&](std::size_t index) { - return segment.start_s - + ((static_cast(slot_count) * static_cast(index)) + static_cast(slot)) - * dt_event_s; + return ((static_cast(slot_count) * static_cast(index)) + static_cast(slot)) * dt_base; }; + // 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 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 band_indices{}; std::vector band_frequency_hz{}; std::vector phi{}; @@ -902,9 +712,10 @@ void apply_motion_correction( if (frequency < low_hz || frequency > high_hz) { continue; } - const double dt_intra_s = sample_time_s(index) - segment.center_s; - const double delta_path_m = 2.0 * motion_factor * dt_intra_s; - const double dtau_s = (delta_path_m / velocity_mps) + dtau_pair_s; + const double dt_intra_s = sample_time_s(index) - t_center_s; + const double delta_range_m = motion_factor * dt_intra_s; + const double delta_path_m = 2.0 * delta_range_m; + const double dtau_s = delta_path_m / velocity_mps; if (dtau_s != 0.0) { any_nonzero = true; } @@ -1081,32 +892,11 @@ void normalize_pair_ascans( 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( const GeometrySelection& selection, double max_depth_m, double min_z_m, - const ImagingPlane& plane + double imaging_plane_y_m ) -> GridDefinition { GridDefinition grid{}; if (selection.x_tx.empty() || selection.x_rx.empty() || !(max_depth_m > min_z_m)) { @@ -1127,8 +917,6 @@ struct ImagingPlane { for (std::size_t row = 0U; row < grid.z_grid.size(); ++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) { const double x_value = grid.x_grid[col]; const auto cell_index = (row * grid.x_grid.size()) + col; @@ -1136,14 +924,14 @@ struct ImagingPlane { 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( x_value - selection.x_tx[tx_index], - plane_y_m - selection.y_tx[tx_index], + imaging_plane_y_m - selection.y_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) { grid.rx_distance_grids[rx_index][cell_index] = distance_3d( x_value - selection.x_rx[rx_index], - plane_y_m - selection.y_rx[rx_index], + imaging_plane_y_m - selection.y_rx[rx_index], z_value - selection.z_rx[rx_index] ); } @@ -1160,10 +948,10 @@ struct ImagingPlane { const GeometrySelection& selection, double max_depth_m, double min_z_m, - const ImagingPlane& plane + double imaging_plane_y_m ) -> std::vector { std::vector signature; - signature.reserve((selection.x_tx.size() + selection.x_rx.size()) * 3U + 6U); + signature.reserve((selection.x_tx.size() + selection.x_rx.size()) * 3U + 3U); const auto append = [&](const std::vector& axis) { signature.insert(signature.end(), axis.begin(), axis.end()); }; @@ -1175,10 +963,7 @@ struct ImagingPlane { append(selection.z_rx); signature.push_back(max_depth_m); signature.push_back(min_z_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); + signature.push_back(imaging_plane_y_m); return signature; } @@ -1218,20 +1003,19 @@ struct ImagingPlane { std::uint32_t tx_index, std::uint32_t rx_index, const GeometrySelection& selection, - const ImagingPlane& plane + double imaging_plane_y_m ) -> std::pair { 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_rx = kCompensationReferenceDepthM - selection.z_rx[rx_index]; const double r_tx = distance_3d( x_center - selection.x_tx[tx_index], - plane_y_m - selection.y_tx[tx_index], + imaging_plane_y_m - selection.y_tx[tx_index], dz_tx ); const double r_rx = distance_3d( x_center - selection.x_rx[rx_index], - plane_y_m - selection.y_rx[rx_index], + imaging_plane_y_m - selection.y_rx[rx_index], dz_rx ); return attenuation_components(r_tx, r_rx, dz_tx, dz_rx); @@ -1269,7 +1053,7 @@ struct ImagingPlane { const std::unordered_map& ascans_by_pair, const GridDefinition& grid, const GeometrySelection& selection, - const ImagingPlane& plane, + double imaging_plane_y_m, double velocity_mps, double min_depth_m, double max_depth_m, @@ -1311,7 +1095,7 @@ struct ImagingPlane { trace.tx_local_index, trace.rx_local_index, selection, - plane + imaging_plane_y_m ); contributions.push_back(PairContribution{ &grid.tx_distance_grids[trace.tx_local_index], @@ -1721,22 +1505,21 @@ void apply_depth_gate( const ObjectRecord& object, const std::vector& selected_traces, const GeometrySelection& selection, - const ImagingPlane& plane + double imaging_plane_y_m ) -> std::vector { std::vector signature{}; signature.reserve(selected_traces.size()); - const double plane_y_m = plane.y_at(object.z_m); for (const auto& trace : selected_traces) { const auto tx = trace.tx_local_index; const auto rx = trace.rx_local_index; const double r_tx = distance_3d( object.x_m - selection.x_tx[tx], - plane_y_m - selection.y_tx[tx], + imaging_plane_y_m - selection.y_tx[tx], object.z_m - selection.z_tx[tx] ); const double r_rx = distance_3d( object.x_m - selection.x_rx[rx], - plane_y_m - selection.y_rx[rx], + imaging_plane_y_m - selection.y_rx[rx], object.z_m - selection.z_rx[rx] ); signature.push_back(0.5 * (r_tx + r_rx)); @@ -1748,12 +1531,12 @@ void mark_sidelobe_candidates( std::vector& objects, const std::vector& selected_traces, const GeometrySelection& selection, - const ImagingPlane& plane + double imaging_plane_y_m ) { std::vector> signatures{}; signatures.reserve(objects.size()); for (const auto& object : objects) { - signatures.push_back(bistatic_depth_signature(object, selected_traces, selection, plane)); + signatures.push_back(bistatic_depth_signature(object, selected_traces, selection, imaging_plane_y_m)); } for (std::size_t object_index = 0U; object_index < objects.size(); ++object_index) { @@ -2088,48 +1871,34 @@ void add_bp_score_metrics( return results; } - // Motion correction before the IFFT: pair-level (between segments) plus - // intra-segment sweep shift, one phase model (see apply_motion_correction). - // The frame timing is built from the FULL collection — including traces of - // deselected Tx — so disabling channels never shrinks the frame model. + // Intra-sweep speed correction before the IFFT. The Tx interleave slot is the + // Tx local index (outputs are sorted), with slot_count = number of selected Tx. + // tx_sweep_time is derived per-collection from the acquisition timestamps: + // one Tx's interleaved sweep spans the whole frame, so capture_span / N_tx. // 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 frame_timing = build_frame_timing( - run_config, - collection, - static_cast(std::max(live_config.gpr_tx_per_segment, 1U)), - live_config.gpr_timing_mode == "segmented_interleaved" - ); - 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 std::size_t motion_slot_count = selection.output_positions.size(); + const double capture_span_s = + collection.capture_end_ns > collection.capture_start_ns + ? static_cast(collection.capture_end_ns - collection.capture_start_ns) * 1e-9 + : 0.0; + const double tx_sweep_time_s = + motion_slot_count > 0U ? capture_span_s / static_cast(motion_slot_count) : 0.0; const double motion_speed_mps = static_cast(live_config.gpr_speed_m_s); const double motion_look_angle_deg = static_cast(live_config.gpr_look_angle_deg); const double motion_direction_sign = static_cast(live_config.gpr_direction_sign); for (auto& trace : selected_traces) { - 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( + apply_intra_sweep_motion_correction( trace, - frame_timing.segments[tx_slot.segment_index], - tx_slot.slot, - tx_slot.slot_count, - frame_timing.frame_center_s, + trace.tx_local_index, + motion_slot_count, start_hz, stop_hz, velocity_mps, motion_speed_mps, motion_look_angle_deg, motion_direction_sign, + tx_sweep_time_s, motion_mode ); } @@ -2150,28 +1919,18 @@ void add_bp_score_metrics( } normalize_pair_ascans(ascans_by_pair, velocity_mps, min_depth_m, max_depth_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(live_config.gpr_imaging_plane_y_m), - static_cast(live_config.gpr_ground_depth_m), - live_config.gpr_ground_pitch_override_enabled - ? static_cast(live_config.gpr_ground_pitch_deg) - : static_cast(live_config.gpr_look_angle_deg), - }; + const double imaging_plane_y_m = static_cast(live_config.gpr_imaging_plane_y_m); // The imaging grid (90k cells x 6 distance fields) only depends on antenna - // geometry, the depth window and the imaging plane, which are constant - // across a run, so it is memoized between collections and rebuilt only when - // that signature changes. process_backprojection_gpr is entered by a single - // processing thread (the backprojection workers join before it returns), so - // a local static is safe. + // geometry and the depth window, which are constant across a run, so it is + // memoized between collections and rebuilt only when that signature changes. + // process_backprojection_gpr is entered by a single processing thread (the + // backprojection workers join before it returns), so a local static is safe. static std::vector cached_grid_signature; static GridDefinition cached_grid; - const auto signature = grid_signature(selection, max_depth_m, kGridZMinM, plane); + const auto signature = grid_signature(selection, max_depth_m, kGridZMinM, imaging_plane_y_m); if (cached_grid.x_grid.empty() || signature != cached_grid_signature) { - cached_grid = build_grid(selection, max_depth_m, kGridZMinM, plane); + cached_grid = build_grid(selection, max_depth_m, kGridZMinM, imaging_plane_y_m); cached_grid_signature = signature; } const GridDefinition& grid = cached_grid; @@ -2184,7 +1943,7 @@ void add_bp_score_metrics( ascans_by_pair, grid, selection, - plane, + imaging_plane_y_m, velocity_mps, min_depth_m, max_depth_m, @@ -2202,7 +1961,7 @@ void add_bp_score_metrics( auto objects = find_bp_objects(display_map, grid, static_cast(live_config.gpr_object_min_frac)); 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); - mark_sidelobe_candidates(objects, selected_traces, selection, plane); + mark_sidelobe_candidates(objects, selected_traces, selection, imaging_plane_y_m); add_bp_score_metrics(objects, live_config); if (live_config.gpr_remove_sidelobe_objects_enabled) { diff --git a/data_acq_and_processing/processing/processors/src/gpr_legacy_processor.ipp b/data_acq_and_processing/processing/processors/src/gpr_legacy_processor.ipp index 94a1972..aa5b386 100644 --- a/data_acq_and_processing/processing/processors/src/gpr_legacy_processor.ipp +++ b/data_acq_and_processing/processing/processors/src/gpr_legacy_processor.ipp @@ -918,12 +918,7 @@ void apply_legacy_motion_correction( return results; } - // 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} - ); + const auto grid = build_grid(selection, max_depth_m, kLegacyGridZMinM, imaging_plane_y_m); if (grid.x_grid.empty() || grid.z_grid.empty()) { return results; } diff --git a/data_acq_and_processing/processing/processors/src/gpr_processor.cpp b/data_acq_and_processing/processing/processors/src/gpr_processor.cpp index 64f8f91..43172ce 100644 --- a/data_acq_and_processing/processing/processors/src/gpr_processor.cpp +++ b/data_acq_and_processing/processing/processors/src/gpr_processor.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include diff --git a/data_acq_and_processing/processing/tools/gpr_timing_check_main.cpp b/data_acq_and_processing/processing/tools/gpr_timing_check_main.cpp deleted file mode 100644 index 6ff498d..0000000 --- a/data_acq_and_processing/processing/tools/gpr_timing_check_main.cpp +++ /dev/null @@ -1,170 +0,0 @@ -// 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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 { - if (!with_trace_timestamps) { - return {0U, 0U}; - } - return output < 2U - ? std::pair{1'000'000'000ULL, 1'223'000'000ULL} - : std::pair{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(1.0, 0.0)); - for (std::size_t index = 0U; index < kFreqCount; ++index) { - trace.frequency_hz[index] = - 1.5e9 + (static_cast(index) * (6.0e9 - 1.5e9) / static_cast(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 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(); -} diff --git a/python_app/gui/controllers/app_window_config/live_processing_mixin.py b/python_app/gui/controllers/app_window_config/live_processing_mixin.py index 10f93bf..0b410c3 100644 --- a/python_app/gui/controllers/app_window_config/live_processing_mixin.py +++ b/python_app/gui/controllers/app_window_config/live_processing_mixin.py @@ -64,10 +64,6 @@ _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_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_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_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"})), @@ -86,8 +82,6 @@ _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_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_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_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"})), diff --git a/python_app/gui/controllers/app_window_config/profile_io_mixin.py b/python_app/gui/controllers/app_window_config/profile_io_mixin.py index e457402..8fbd169 100644 --- a/python_app/gui/controllers/app_window_config/profile_io_mixin.py +++ b/python_app/gui/controllers/app_window_config/profile_io_mixin.py @@ -309,12 +309,6 @@ class AppWindowConfigProfileIOMixin: self._gpr_background_mean_count, self._gpr_remove_sidelobe_objects_enabled, 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_visible_x_min_m, self._gpr_visible_x_max_m, @@ -513,16 +507,6 @@ class AppWindowConfigProfileIOMixin: 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._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._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)) diff --git a/python_app/gui/controllers/app_window_config/state_builders.py b/python_app/gui/controllers/app_window_config/state_builders.py index beb1d88..e2b8b09 100644 --- a/python_app/gui/controllers/app_window_config/state_builders.py +++ b/python_app/gui/controllers/app_window_config/state_builders.py @@ -391,12 +391,6 @@ class AppWindowConfigStateBuildersMixin: background_mean_count=int(self._gpr_background_mean_count.value()), remove_sidelobe_objects_enabled=bool(self._gpr_remove_sidelobe_objects_enabled.isChecked()), 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(), visible_x_min_m=float(self._gpr_visible_x_min_m.value()), visible_x_max_m=float(self._gpr_visible_x_max_m.value()), diff --git a/python_app/gui/controllers/sections/processing_section.py b/python_app/gui/controllers/sections/processing_section.py index 8683bb0..161d59a 100644 --- a/python_app/gui/controllers/sections/processing_section.py +++ b/python_app/gui/controllers/sections/processing_section.py @@ -376,58 +376,9 @@ def build_processing_group(owner) -> QGroupBox: 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.setToolTip( - "Y coordinate of the BP imaging slice (m) in 'const' plane mode. " - "Use 0 for legacy 1D antenna layouts." + "Y coordinate of the BP imaging slice (m). 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( owner._processing_mode_pages, [ @@ -449,12 +400,6 @@ def build_processing_group(owner) -> QGroupBox: ("Approach min frames", owner._gpr_object_approach_min_frames), ("Start MHz", owner._gpr_start_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), ("Visible X min m", owner._gpr_visible_x_min_m), ("Visible X max m", owner._gpr_visible_x_max_m), @@ -848,13 +793,6 @@ def build_processing_group(owner) -> QGroupBox: 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_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_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) diff --git a/python_app/models/gui_profile_codec.py b/python_app/models/gui_profile_codec.py index 6c95b84..2e73211 100644 --- a/python_app/models/gui_profile_codec.py +++ b/python_app/models/gui_profile_codec.py @@ -380,42 +380,6 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: gui.processing.gpr.imaging_plane_y_m, "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( gpr_object, "render_mode", @@ -634,16 +598,6 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: 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"}: 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"}: 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"}: @@ -796,12 +750,6 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]: "background_mean_count": gui.processing.gpr.background_mean_count, "remove_sidelobe_objects_enabled": gui.processing.gpr.remove_sidelobe_objects_enabled, "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, "visible_x_min_m": gui.processing.gpr.visible_x_min_m, "visible_x_max_m": gui.processing.gpr.visible_x_max_m, diff --git a/python_app/models/gui_profile_schema.py b/python_app/models/gui_profile_schema.py index 6ac21e6..70d0036 100644 --- a/python_app/models/gui_profile_schema.py +++ b/python_app/models/gui_profile_schema.py @@ -87,18 +87,6 @@ class GuiGprStateModel: background_mean_count: int = 10 remove_sidelobe_objects_enabled: bool = True 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" visible_x_min_m: float = -2.0 visible_x_max_m: float = 2.0 diff --git a/python_app/orchestration/live_processing_config.py b/python_app/orchestration/live_processing_config.py index fe2549a..1b84cf0 100644 --- a/python_app/orchestration/live_processing_config.py +++ b/python_app/orchestration/live_processing_config.py @@ -62,25 +62,6 @@ class ProcessingLiveConfig: gpr_background_mean_count: int = 10 gpr_remove_sidelobe_objects_enabled: bool = True 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 # objects are already finalized in the processor (no score threshold); only legacy # GPR still thresholds, on a pair count. @@ -168,12 +149,6 @@ class ProcessingLiveConfig: "gpr_background_mean_count": int(self.gpr_background_mean_count), "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_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), "gpr_visible_x_min_m": float(self.gpr_visible_x_min_m), "gpr_visible_x_max_m": float(self.gpr_visible_x_max_m), diff --git a/python_app/scripts/gpr_timing_check_reference.py b/python_app/scripts/gpr_timing_check_reference.py deleted file mode 100644 index e86b77e..0000000 --- a/python_app/scripts/gpr_timing_check_reference.py +++ /dev/null @@ -1,101 +0,0 @@ -"""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() diff --git a/python_app/tests/test_gui_profile_codec.py b/python_app/tests/test_gui_profile_codec.py index 2503aba..fdd5099 100644 --- a/python_app/tests/test_gui_profile_codec.py +++ b/python_app/tests/test_gui_profile_codec.py @@ -68,18 +68,6 @@ class GuiProfileCodecTest(unittest.TestCase): with self.assertRaises(ValueError): # combos_text must be a JSON string, not a number 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: # An old payload that selects 'gpr' but carries a legacy root gpr.mode is migrated. decoded = GuiProfileModel.from_dict({