Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9a0b43ed3 | ||
|
|
ebf0462d85 |
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<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] : {
|
||||
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},
|
||||
|
||||
+63
-304
@@ -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<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.
|
||||
// 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<double>(slot_count) * static_cast<double>(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<double>(n_full - 1U);
|
||||
const auto sample_time_s = [&](std::size_t index) {
|
||||
return segment.start_s
|
||||
+ ((static_cast<double>(slot_count) * static_cast<double>(index)) + static_cast<double>(slot))
|
||||
* dt_event_s;
|
||||
return ((static_cast<double>(slot_count) * static_cast<double>(index)) + static_cast<double>(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<std::size_t> band_indices{};
|
||||
std::vector<double> band_frequency_hz{};
|
||||
std::vector<double> 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<double> {
|
||||
std::vector<double> 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<double>& 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<double, double> {
|
||||
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<PairKey, AscanResult>& 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<SelectedTrace>& selected_traces,
|
||||
const GeometrySelection& selection,
|
||||
const ImagingPlane& plane
|
||||
double imaging_plane_y_m
|
||||
) -> std::vector<double> {
|
||||
std::vector<double> 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<ObjectRecord>& objects,
|
||||
const std::vector<SelectedTrace>& selected_traces,
|
||||
const GeometrySelection& selection,
|
||||
const ImagingPlane& plane
|
||||
double imaging_plane_y_m
|
||||
) {
|
||||
std::vector<std::vector<double>> 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::size_t>(std::max<std::uint32_t>(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<double>(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<double>(motion_slot_count) : 0.0;
|
||||
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_direction_sign = static_cast<double>(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<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),
|
||||
};
|
||||
const double imaging_plane_y_m = static_cast<double>(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<double> 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<double>(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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <complex>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <queue>
|
||||
|
||||
@@ -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 <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,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"})),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -14,12 +14,6 @@ from python_app.orchestration.gpr_locator import (
|
||||
gpr_object_rows as extract_gpr_object_rows,
|
||||
)
|
||||
|
||||
# Fraction of the frame maximum below which the heatmap is not highlighted
|
||||
# (same as cloud-threshold=0.75 in the VNA_AlGo 3D SAR animation): the color
|
||||
# scale is stretched over [frac*max, max], everything below falls into the
|
||||
# darkest LUT color.
|
||||
HEATMAP_DISPLAY_THRESHOLD_FRAC = 0.75
|
||||
|
||||
|
||||
class AppWindowGprPlotMixin:
|
||||
"""Renders GPR accumulator heatmaps and detected object overlays."""
|
||||
@@ -250,9 +244,7 @@ class AppWindowGprPlotMixin:
|
||||
self._gpr_image_item.setImage(image, autoLevels=False)
|
||||
self._gpr_image_item.setRect(rect)
|
||||
self._gpr_image_item.setLookupTable(self._gpr_lookup_table)
|
||||
image_max = float(np.max(image))
|
||||
self._gpr_image_item.setLevels(
|
||||
(HEATMAP_DISPLAY_THRESHOLD_FRAC * image_max, image_max + 1e-6))
|
||||
self._gpr_image_item.setLevels((float(np.min(image)), float(np.max(image) + 1e-6)))
|
||||
self._gpr_image_item.show()
|
||||
|
||||
visible_x_min, visible_x_max, visible_z_min, visible_z_max = self._gpr_visible_bounds()
|
||||
|
||||
@@ -21,12 +21,15 @@ from PyQt6.QtCore import QRectF
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
from python_app.gui.controllers.app_window_plot.gpr_plot_mixin import (
|
||||
HEATMAP_DISPLAY_THRESHOLD_FRAC,
|
||||
)
|
||||
from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label
|
||||
from python_app.models.dataset_model import ResultCollection
|
||||
|
||||
# Fraction of the projection maximum below which the heatmap is not highlighted
|
||||
# (same as cloud-threshold=0.75 in the VNA_AlGo 3D SAR animation): the color
|
||||
# scale is stretched over [frac, 1], everything below falls into the darkest
|
||||
# LUT color.
|
||||
HEATMAP_DISPLAY_THRESHOLD_FRAC = 0.75
|
||||
|
||||
|
||||
class AppWindowSar3dPlotMixin:
|
||||
"""Renders 3D SAR volume projections published by the sar3d processor."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user