perf(wifi-densepose-sar): incremental phasor rotation in backprojection (~4.4-4.5x, MEASURED)

focus_at_point called Complex64::from_polar (a sin/cos pair) once per
(pose, frequency) term. FrequencySweep::frequencies() produces evenly
spaced frequencies by construction, so the per-term phase is an
arithmetic progression in the frequency index -- the phasor can be
evaluated once per pose and advanced by a fixed complex-multiply step
per frequency instead, turning K trig evaluations into 2.

focus_at_point's signature changes from a raw &[f64] frequency slice
to &FrequencySweep, so the evenly-spaced-frequencies precondition
this optimization depends on is a type-level invariant rather than a
caller-observed one -- an arbitrary non-uniform frequency list is no
longer constructible through this API at all.

MEASURED (criterion regression detection, p < 0.001): ~4.4-4.5x
faster across 512/4096/32768-voxel grids (300us/1.97ms/14.5ms vs the
prior 1.47ms/10.4ms/73.5ms). Proven equivalent, not just faster: a new
test independently reimplements the direct per-frequency computation
as a reference and checks the optimized path against it across four
sweep sizes (incl. the n_steps=1 degenerate case) and both on-target
and off-target points, to <1e-9 relative error.

25 tests (22 unit + 3 integration), 0 failed, clippy-clean.
This commit is contained in:
ruv
2026-07-30 21:09:57 -04:00
parent d781f20e1a
commit 895c04747e
5 changed files with 118 additions and 16 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- **`wifi-densepose-sar` — coherent wideband RF tomography research crate (ADR-283).** New standalone leaf crate (the `nvsim` pattern; zero coupling to `wifi-densepose-hardware` or any real ingestion path) implementing the synthetic-aperture-radar reconstruction primitive a handheld through-wall RF imaging device would need — motivated by comparison against Applied Electrodynamics' "WaveSight" launch, and explicitly scoped below ADR-278's RISE/DiffRadar/GeRaF reproduction gates. Ships: (1) a stepped-frequency, multi-position complex forward measurement simulator (`y_{m,k} = Σ σ_j/R² · exp(-i·4π·f·R/c) + noise`, deterministic ChaCha20 seeding); (2) delay-and-sum backprojection reconstruction onto a 3D voxel grid, rayon-parallelized over voxels; (3) threshold + local-maximum point-cloud extraction; (4) closed-form range/cross-range resolution and antenna-pose coherence-budget formulas (`ΔR=c/2B`, `δ_CR≈λR/2L`, `Δp≤λ/8`) checked against the reconstruction's *actual* behavior in `tests/physics_validation.rs` rather than merely documented — forward-simulating two targets at controlled separations and proving they resolve or merge exactly where the formulas predict, and that reconstructed focus at a known target degrades as injected antenna-pose error grows. Every number is SYNTHETIC/L0 (ADR-282) — no real wideband RF hardware backs this crate; see the crate README and `docs/tutorials/coherent-rf-tomography-backprojection.md` for the full honesty boundary and a worked walkthrough. 24 tests (21 unit + 3 integration), 0 failed, clippy-clean; MEASURED backprojection throughput ~350-450K voxels/sec (criterion, 21 poses × 32 freq steps). - **`wifi-densepose-sar` — coherent wideband RF tomography research crate (ADR-283).** New standalone leaf crate (the `nvsim` pattern; zero coupling to `wifi-densepose-hardware` or any real ingestion path) implementing the synthetic-aperture-radar reconstruction primitive a handheld through-wall RF imaging device would need — motivated by comparison against Applied Electrodynamics' "WaveSight" launch, and explicitly scoped below ADR-278's RISE/DiffRadar/GeRaF reproduction gates. Ships: (1) a stepped-frequency, multi-position complex forward measurement simulator (`y_{m,k} = Σ σ_j/R² · exp(-i·4π·f·R/c) + noise`, deterministic ChaCha20 seeding); (2) delay-and-sum backprojection reconstruction onto a 3D voxel grid, rayon-parallelized over voxels; (3) threshold + local-maximum point-cloud extraction; (4) closed-form range/cross-range resolution and antenna-pose coherence-budget formulas (`ΔR=c/2B`, `δ_CR≈λR/2L`, `Δp≤λ/8`) checked against the reconstruction's *actual* behavior in `tests/physics_validation.rs` rather than merely documented — forward-simulating two targets at controlled separations and proving they resolve or merge exactly where the formulas predict, and that reconstructed focus at a known target degrades as injected antenna-pose error grows. Every number is SYNTHETIC/L0 (ADR-282) — no real wideband RF hardware backs this crate; see the crate README and `docs/tutorials/coherent-rf-tomography-backprojection.md` for the full honesty boundary and a worked walkthrough. `focus_at_point` exploits the evenly-spaced-by-construction frequency sweep (an arithmetic progression in per-term phase) to evaluate each pose's phasor once and advance it by a fixed complex-multiply step per frequency instead of one `sin`/`cos` pair per frequency — **MEASURED ~4.4-4.5x faster** (criterion regression detection, p < 0.001) than the first-shipped direct-computation version, proven equivalent (not just faster) to an independently reimplemented reference across four sweep sizes and on-/off-target points. 25 tests (22 unit + 3 integration), 0 failed, clippy-clean; MEASURED backprojection throughput ~1.7-2.3M voxels/sec (criterion, 21 poses × 32 freq steps).
- **HOMECORE platform runtime completion — secure native/Wasmtime plugins, authenticated HAP IP, expanded Home Assistant APIs, durable restoration/migration, and voice protocols.** `homecore-server` now owns deterministic compiled-in native plugin registration plus explicitly configured, path-bounded, Ed25519 publisher-verified Wasm packages executed through Wasmtime with setup/state-change/teardown lifecycle; arbitrary native dynamic libraries remain intentionally unsupported. The optional HAP server implements persisted accessory identity and controller records, SRP-6a Pair-Setup M1M6, X25519/Ed25519 Pair-Verify M1M4, HKDF-SHA512/ChaCha20-Poly1305 record framing, authenticated/admin endpoint gates, replay/tamper closure, live entity synchronization, and paired-state `_hap._tcp` mDNS updates (45 focused tests; external Apple certification is not claimed). Startup restores device/entity registries and deterministic latest recorder states before plugins, and migration now atomically preserves forward-compatible device/config-entry fields. The HA-compatible surface adds events, templates, config checks, components, registries, history/logbook with SQL-enforced global response bounds, calendar/camera provider routes, and modern WebSocket negotiation while retaining a machine-readable limitations matrix for integration-specific behavior. Assist adds bounded PCM16, async STT/TTS contracts, an end-to-end speech pipeline, and an authenticated satellite session protocol; real deployments still provide the speech engines. - **HOMECORE platform runtime completion — secure native/Wasmtime plugins, authenticated HAP IP, expanded Home Assistant APIs, durable restoration/migration, and voice protocols.** `homecore-server` now owns deterministic compiled-in native plugin registration plus explicitly configured, path-bounded, Ed25519 publisher-verified Wasm packages executed through Wasmtime with setup/state-change/teardown lifecycle; arbitrary native dynamic libraries remain intentionally unsupported. The optional HAP server implements persisted accessory identity and controller records, SRP-6a Pair-Setup M1M6, X25519/Ed25519 Pair-Verify M1M4, HKDF-SHA512/ChaCha20-Poly1305 record framing, authenticated/admin endpoint gates, replay/tamper closure, live entity synchronization, and paired-state `_hap._tcp` mDNS updates (45 focused tests; external Apple certification is not claimed). Startup restores device/entity registries and deterministic latest recorder states before plugins, and migration now atomically preserves forward-compatible device/config-entry fields. The HA-compatible surface adds events, templates, config checks, components, registries, history/logbook with SQL-enforced global response bounds, calendar/camera provider routes, and modern WebSocket negotiation while retaining a machine-readable limitations matrix for integration-specific behavior. Assist adds bounded PCM16, async STT/TTS contracts, an end-to-end speech pipeline, and an authenticated satellite session protocol; real deployments still provide the speech engines.
- **`ruview-unified` increment 3 — Gaussian update-loop completion, separable delay-Doppler, and property-tested boundary hardening.** (1) `GaussianMap::merge_overlapping` (ADR-275 step 5: mutual-Mahalanobis + semantic-compatibility dedup catching drift the insert-time gate misses) and lifetime-aware decay (`τ_eff = τ·(1+ln(1+lifetime/τ))` — confirmed structures outlive transients at equal nominal τ). (2) `delay_doppler_map` reimplemented separably (`O(B²S+S²B)`), proven equivalent to the direct reference to <1e-10 and **measured 8.3× faster** (520 µs vs 4.34 ms at 56×8). (3) `tests/security_boundaries.rs` — 8 `proptest` properties over the boundary surfaces (arbitrary values incl. NaN/±inf via `f64::from_bits`) that found and fixed three input-controlled defects: a BLE-CS phase-unwrap infinite loop on non-finite phases and an ~1e299-iteration loop on finite-huge phases (now O(1) modular unwrap + plausibility bound), and a subnormal Gaussian scale overflowing `1/σ²` to NaN density (now physical σ/occupancy bounds). (4) New criterion benches for all increment-2 hot paths (`to_canonical` 38 µs, `ble_cs_range` 481 ns, AoI planner 647 ns/200 regions, coherent fusion 1.5 µs/32 members, factorized pose 521 ns). ruview-unified now 98 tests (87 lib + 3 acceptance + 8 security), 0 failed, clippy-clean. - **`ruview-unified` increment 3 — Gaussian update-loop completion, separable delay-Doppler, and property-tested boundary hardening.** (1) `GaussianMap::merge_overlapping` (ADR-275 step 5: mutual-Mahalanobis + semantic-compatibility dedup catching drift the insert-time gate misses) and lifetime-aware decay (`τ_eff = τ·(1+ln(1+lifetime/τ))` — confirmed structures outlive transients at equal nominal τ). (2) `delay_doppler_map` reimplemented separably (`O(B²S+S²B)`), proven equivalent to the direct reference to <1e-10 and **measured 8.3× faster** (520 µs vs 4.34 ms at 56×8). (3) `tests/security_boundaries.rs` — 8 `proptest` properties over the boundary surfaces (arbitrary values incl. NaN/±inf via `f64::from_bits`) that found and fixed three input-controlled defects: a BLE-CS phase-unwrap infinite loop on non-finite phases and an ~1e299-iteration loop on finite-huge phases (now O(1) modular unwrap + plausibility bound), and a subnormal Gaussian scale overflowing `1/σ²` to NaN density (now physical σ/occupancy bounds). (4) New criterion benches for all increment-2 hot paths (`to_canonical` 38 µs, `ble_cs_range` 481 ns, AoI planner 647 ns/200 regions, coherent fusion 1.5 µs/32 members, factorized pose 521 ns). ruview-unified now 98 tests (87 lib + 3 acceptance + 8 security), 0 failed, clippy-clean.
- **`ruview-unified` increment 2 — native frame contract + programmable perception (ADR-279..282).** (1) `RfFrameV2` becomes the authoritative RF record: native complex IQ with explicit validity masks, declared `PhaseState`, TX/RX poses + antenna geometry in one building frame, calibration/quality state, and a provenance rule enforced at construction — `Synthetic ⇒ L0Simulation` and `Measured ⇒ ≥ L1CapturedReplay` can never alias (the public L0L5 evidence ladder is now a type); the 56-bin canonical tensor is demoted to a derived compatibility view (`to_canonical`, mask-aware gap-filling through the same normalization path as every adapter; native samples proven byte-untouched). (2) Active sensing control plane (`control.rs`): ETSI-ISAC-vocabulary `SensingTask` admission (raw export always refused; identity requires consent), `SensingAction`/`InformationGoal`, an age-of-information `ActiveSensingPlanner` (priority = uncertainty × change rate × criticality ÷ cost; **measured 95% sensing-traffic reduction** vs uniform refresh on a 20-region scenario), fail-closed `CoherentSensorGroup` fusion gates (time/phase/geometry bounds; five denial paths tested), policy-authorized RIS/movable-antenna actuation receipts, and purpose-scoped `TaskSufficientRepresentation` leakage validation. (3) New modality surfaces: BLE Channel Sounding adapter + `ble_cs_range` treating phase-slope and RTT as **separate cross-validated evidence** (exact distance recovery on synthetic tones; relay-style divergence flagged, never averaged), delay-Doppler-native `FieldAxis` + `delay_doppler_map` (unit-peak tone test), IEEE P3162 synthetic-aperture import profile. (4) RePos-factorized pose head (relative skeleton on the content representation, root on the geometry-conditioned one, calibrated per-joint uncertainties): held-out-room MPJPE 0.0003 m vs 0.2534 m for the monolithic baseline in the room-shortcut leakage experiment; ≤2% structured-adapter budget (740 params). (5) Age gate input now `log(1+age_ms)` per the age-aware-CSI recipe (gradient check re-proven); Gaussian primitives gained `first_seen_ns`/`doppler_variance`/bounded `source_receipts` lineage; `PartitionKey` gained a `session` dimension and `SplitManifest` certifies disjointness across all seven dimensions. 87 tests, 0 failed; crate clippy-clean. Docker images unaffected (no shipped binary consumes the crate yet); Python proof re-verified PASS. - **`ruview-unified` increment 2 — native frame contract + programmable perception (ADR-279..282).** (1) `RfFrameV2` becomes the authoritative RF record: native complex IQ with explicit validity masks, declared `PhaseState`, TX/RX poses + antenna geometry in one building frame, calibration/quality state, and a provenance rule enforced at construction — `Synthetic ⇒ L0Simulation` and `Measured ⇒ ≥ L1CapturedReplay` can never alias (the public L0L5 evidence ladder is now a type); the 56-bin canonical tensor is demoted to a derived compatibility view (`to_canonical`, mask-aware gap-filling through the same normalization path as every adapter; native samples proven byte-untouched). (2) Active sensing control plane (`control.rs`): ETSI-ISAC-vocabulary `SensingTask` admission (raw export always refused; identity requires consent), `SensingAction`/`InformationGoal`, an age-of-information `ActiveSensingPlanner` (priority = uncertainty × change rate × criticality ÷ cost; **measured 95% sensing-traffic reduction** vs uniform refresh on a 20-region scenario), fail-closed `CoherentSensorGroup` fusion gates (time/phase/geometry bounds; five denial paths tested), policy-authorized RIS/movable-antenna actuation receipts, and purpose-scoped `TaskSufficientRepresentation` leakage validation. (3) New modality surfaces: BLE Channel Sounding adapter + `ble_cs_range` treating phase-slope and RTT as **separate cross-validated evidence** (exact distance recovery on synthetic tones; relay-style divergence flagged, never averaged), delay-Doppler-native `FieldAxis` + `delay_doppler_map` (unit-peak tone test), IEEE P3162 synthetic-aperture import profile. (4) RePos-factorized pose head (relative skeleton on the content representation, root on the geometry-conditioned one, calibrated per-joint uncertainties): held-out-room MPJPE 0.0003 m vs 0.2534 m for the monolithic baseline in the room-shortcut leakage experiment; ≤2% structured-adapter budget (740 params). (5) Age gate input now `log(1+age_ms)` per the age-aware-CSI recipe (gradient check re-proven); Gaussian primitives gained `first_seen_ns`/`doppler_variance`/bounded `source_receipts` lineage; `PartitionKey` gained a `session` dimension and `SplitManifest` certifies disjointness across all seven dimensions. 87 tests, 0 failed; crate clippy-clean. Docker images unaffected (no shipped binary consumes the crate yet); Python proof re-verified PASS.
@@ -53,4 +53,10 @@ This is deliberately scoped **one level below** ADR-278's RISE/DiffRadar/GeRaF r
- The workspace gains a real (if intentionally scoped-down) coherent-imaging primitive where before there was none — useful groundwork for ADR-278 if that research program proceeds, and a direct, honest answer to "could this repo build a WaveSight-like device" (no, not without the hardware program described in the motivating comparison; yes, this is the reconstruction-algorithm groundwork such a program would need). - The workspace gains a real (if intentionally scoped-down) coherent-imaging primitive where before there was none — useful groundwork for ADR-278 if that research program proceeds, and a direct, honest answer to "could this repo build a WaveSight-like device" (no, not without the hardware program described in the motivating comparison; yes, this is the reconstruction-algorithm groundwork such a program would need).
- Zero risk to the existing `wifi-densepose-signal::ruvsense::tomography` (RSS-based RTI) code path or any production pipeline — this crate is not referenced by any of them. - Zero risk to the existing `wifi-densepose-signal::ruvsense::tomography` (RSS-based RTI) code path or any production pipeline — this crate is not referenced by any of them.
- 24 tests (21 unit + 3 integration physics-validation), 0 failed, clippy-clean. Criterion bench: MEASURED 512/4096/32768-voxel backprojection reconstruction throughput (see crate README for the numbers as last recorded). - 25 tests (22 unit + 3 integration physics-validation), 0 failed, clippy-clean. Criterion bench: MEASURED 512/4096/32768-voxel backprojection reconstruction throughput (see crate README for the numbers as last recorded). The incremental-phasor-rotation optimization (§7) cut reconstruction time ~4.4-4.5x, proven equivalent to the direct per-frequency computation it replaced.
## 7. Follow-up optimization: incremental phasor rotation (2026-07-30, MEASURED)
`focus_at_point` originally called `Complex64::from_polar` (one `sin`/`cos` pair) per (pose, frequency) term. Since [`FrequencySweep::frequencies`](../../v2/crates/wifi-densepose-sar/src/measurement.rs) produces evenly-spaced frequencies by construction, the per-term phase is an arithmetic progression in the frequency index — so the phasor can be evaluated once per pose and advanced by a fixed complex-multiply step per frequency, replacing K trig evaluations with 2. `focus_at_point`'s signature changed from a raw `&[f64]` frequency slice to `&FrequencySweep`, making the evenly-spaced-frequencies precondition this optimization depends on a type-level invariant rather than a caller-observed one.
**MEASURED (criterion regression detection, p < 0.001): ~4.4-4.5x faster** across 512/4096/32768-voxel grids. **Proven equivalent**, not just faster: `reconstruct::tests::backprojection_incremental_rotation_matches_direct_per_frequency_computation` checks the optimized path against an independently reimplemented direct per-frequency reference, across four sweep sizes (including the `n_steps=1` degenerate case) and both on-target and off-target evaluation points, to <1e-9 relative error.
+16 -4
View File
@@ -42,7 +42,7 @@ cargo bench -p wifi-densepose-sar
`tests/physics_validation.rs` checks the reconstruction's actual behavior `tests/physics_validation.rs` checks the reconstruction's actual behavior
against the closed-form formulas in `resolution.rs` (range resolution, against the closed-form formulas in `resolution.rs` (range resolution,
cross-range/synthetic-aperture resolution, and the antenna-pose coherence cross-range/synthetic-aperture resolution, and the antenna-pose coherence
budget) rather than merely asserting them: 24 tests (21 unit + 3 budget) rather than merely asserting them: 25 tests (22 unit + 3
integration), 0 failed, clippy-clean. integration), 0 failed, clippy-clean.
## Performance (MEASURED) ## Performance (MEASURED)
@@ -53,10 +53,22 @@ machine, release profile:
| Voxels | Median time | Throughput | | Voxels | Median time | Throughput |
|-------:|------------:|-----------:| |-------:|------------:|-----------:|
| 512 | 1.47 ms | ~348,000 voxels/s | | 512 | 300 µs | ~1.71M voxels/s |
| 4,096 | 10.4 ms | ~394,000 voxels/s | | 4,096 | 1.97 ms | ~2.08M voxels/s |
| 32,768 | 73.5 ms | ~446,000 voxels/s | | 32,768 | 14.5 ms | ~2.26M voxels/s |
Scales as expected: each voxel's cost is independent (`O(poses × freqs)` Scales as expected: each voxel's cost is independent (`O(poses × freqs)`
per voxel, embarrassingly parallel), so throughput is roughly constant per voxel, embarrassingly parallel), so throughput is roughly constant
across grid sizes and total time scales linearly with voxel count. across grid sizes and total time scales linearly with voxel count.
**Optimization (MEASURED, criterion regression detection, p < 0.001): ~4.4-4.5x
faster** than the first-shipped implementation, across all three grid
sizes. Frequencies in a [`FrequencySweep`](src/measurement.rs) are evenly
spaced by construction, so the per-(pose, frequency) phase term is an
arithmetic progression; `focus_at_point` now evaluates the phasor once per
pose and advances it by a fixed complex-multiply step per frequency,
instead of one `sin`/`cos` pair (`Complex64::from_polar`) per frequency —
K trig evaluations become 2. Proven equivalent (not just faster) to an
independently-reimplemented direct per-frequency reference in
`reconstruct::tests::backprojection_incremental_rotation_matches_direct_per_frequency_computation`,
across several sweep sizes and both on-target and off-target points.
@@ -120,8 +120,7 @@ pub fn backproject(
grid: &VoxelGrid, grid: &VoxelGrid,
) -> ReflectivityImage { ) -> ReflectivityImage {
assert_eq!(poses.len(), measurement.n_poses, "pose count must match measurement"); assert_eq!(poses.len(), measurement.n_poses, "pose count must match measurement");
let freqs = sweep.frequencies(); assert_eq!(sweep.n_steps, measurement.n_freqs, "frequency count must match measurement");
assert_eq!(freqs.len(), measurement.n_freqs, "frequency count must match measurement");
let n = grid.len(); let n = grid.len();
@@ -130,7 +129,7 @@ pub fn backproject(
.map(|linear| { .map(|linear| {
let (i, j, k) = grid.unflatten(linear); let (i, j, k) = grid.unflatten(linear);
let voxel = grid.voxel_center(i, j, k); let voxel = grid.voxel_center(i, j, k);
focus_at_point(measurement, poses, &freqs, &voxel) focus_at_point(measurement, poses, sweep, &voxel)
}) })
.collect(); .collect();
@@ -143,11 +142,30 @@ pub fn backproject(
/// (and tests) can measure focus quality exactly at a location of /// (and tests) can measure focus quality exactly at a location of
/// interest -- e.g. a known target position -- rather than only at /// interest -- e.g. a known target position -- rather than only at
/// whatever grid points happen to be sampled. /// whatever grid points happen to be sampled.
pub fn focus_at_point(measurement: &Measurement, poses: &[AntennaPose], freqs: &[f64], point: &Point3) -> f64 { ///
/// Takes `sweep` rather than a raw frequency slice specifically so the
/// evenly-spaced-frequencies guarantee ([`FrequencySweep::frequencies`])
/// is a type-level invariant, not a caller-observed precondition: the
/// implementation below relies on it (see the comment inside the pose
/// loop). Passing an arbitrary non-uniform frequency list is not possible
/// through this signature.
pub fn focus_at_point(measurement: &Measurement, poses: &[AntennaPose], sweep: &FrequencySweep, point: &Point3) -> f64 {
assert_eq!(poses.len(), measurement.n_poses, "pose count must match measurement"); assert_eq!(poses.len(), measurement.n_poses, "pose count must match measurement");
assert_eq!(freqs.len(), measurement.n_freqs, "frequency count must match measurement"); assert_eq!(sweep.n_steps, measurement.n_freqs, "frequency count must match measurement");
let n_terms = (measurement.n_poses * measurement.n_freqs) as f64; let n_terms = (measurement.n_poses * measurement.n_freqs) as f64;
let k = sweep.n_steps;
// Frequencies are evenly spaced by construction: f_kf = start_hz + kf *
// delta_f. That makes the per-term phase phase_kf = 4*pi*f_kf*r/c an
// arithmetic progression in kf, so instead of K trig evaluations
// (Complex64::from_polar per frequency step) the phasor is evaluated
// once and advanced by a fixed per-step rotation -- one complex
// multiply per step instead of a sin/cos pair. Proven equivalent to
// the direct per-frequency computation (independently reimplemented,
// not reusing this code) in
// `backprojection_incremental_rotation_matches_direct_per_frequency_computation`.
let delta_f = if k > 1 { (sweep.stop_hz - sweep.start_hz) / (k - 1) as f64 } else { 0.0 };
let mut acc = Complex64::new(0.0, 0.0); let mut acc = Complex64::new(0.0, 0.0);
for (m, pose) in poses.iter().enumerate() { for (m, pose) in poses.iter().enumerate() {
let r = pose.position.distance(point); let r = pose.position.distance(point);
@@ -155,9 +173,15 @@ pub fn focus_at_point(measurement: &Measurement, poses: &[AntennaPose], freqs: &
continue; continue;
} }
let gain_compensation = r * r; let gain_compensation = r * r;
for (kf, &f) in freqs.iter().enumerate() { let base_phase = 4.0 * PI * sweep.start_hz * r / SPEED_OF_LIGHT_M_PER_S;
let phase = 4.0 * PI * f * r / SPEED_OF_LIGHT_M_PER_S; let step_phase = 4.0 * PI * delta_f * r / SPEED_OF_LIGHT_M_PER_S;
acc += measurement.get(m, kf) * gain_compensation * Complex64::from_polar(1.0, phase); let step = Complex64::from_polar(1.0, step_phase);
let mut rot = Complex64::from_polar(1.0, base_phase);
for kf in 0..k {
acc += measurement.get(m, kf) * gain_compensation * rot;
if kf + 1 < k {
rot *= step;
}
} }
} }
acc.norm() / n_terms acc.norm() / n_terms
@@ -211,4 +235,65 @@ mod tests {
assert!(peak_mag > mean_mag * 5.0, "coherent focus at the target must dominate the incoherent background: peak={peak_mag}, mean={mean_mag}"); assert!(peak_mag > mean_mag * 5.0, "coherent focus at the target must dominate the incoherent background: peak={peak_mag}, mean={mean_mag}");
} }
/// Independent reference: the direct per-frequency computation
/// `focus_at_point` used before the incremental-phasor-rotation
/// optimization (one `Complex64::from_polar` per (pose, frequency)
/// term, no recurrence). Deliberately reimplemented here rather than
/// calling any shared helper, so this test cannot pass by construction.
fn focus_at_point_direct_reference(
measurement: &Measurement,
poses: &[AntennaPose],
sweep: &FrequencySweep,
point: &Point3,
) -> f64 {
let freqs = sweep.frequencies();
let n_terms = (measurement.n_poses * measurement.n_freqs) as f64;
let mut acc = Complex64::new(0.0, 0.0);
for (m, pose) in poses.iter().enumerate() {
let r = pose.position.distance(point);
if r < 1e-6 {
continue;
}
let gain_compensation = r * r;
for (kf, &f) in freqs.iter().enumerate() {
let phase = 4.0 * PI * f * r / SPEED_OF_LIGHT_M_PER_S;
acc += measurement.get(m, kf) * gain_compensation * Complex64::from_polar(1.0, phase);
}
}
acc.norm() / n_terms
}
/// PERF PROOF: the incremental-phasor-rotation `focus_at_point` (2
/// trig evaluations/pose instead of K) matches the direct
/// per-frequency reference to within f64 rounding, across several
/// sweep sizes, ranges, and off-axis points (not just the on-target
/// case, where errors could cancel).
#[test]
fn backprojection_incremental_rotation_matches_direct_per_frequency_computation() {
let poses = linear_aperture(Point3::new(-0.7, 0.0, 0.0), Point3::new(0.6, 0.1, 0.0), 17);
let targets = vec![
ScatteringTarget::new(Point3::new(0.1, 2.3, -0.2), 1.0),
ScatteringTarget::new(Point3::new(-0.4, 1.9, 0.3), 0.6),
];
let test_points = [
Point3::new(0.1, 2.3, -0.2), // on a target
Point3::new(-0.4, 1.9, 0.3), // on the other target
Point3::new(0.0, 2.0, 0.0), // off-target
Point3::new(-0.55, 2.6, 0.4), // off-target, far corner
];
for &(n_steps, start_hz, stop_hz) in &[(1usize, 3.0e9, 3.0e9), (2, 2.0e9, 6.0e9), (8, 1.0e9, 9.0e9), (64, 2.4e9, 2.5e9)] {
let sweep = FrequencySweep::new(start_hz, stop_hz, n_steps);
let measurement = simulate_measurement(&poses, &sweep, &targets, 0.0, 42);
for point in test_points {
let fast = focus_at_point(&measurement, &poses, &sweep, &point);
let reference = focus_at_point_direct_reference(&measurement, &poses, &sweep, &point);
let scale = reference.max(1e-12);
assert!(
(fast - reference).abs() / scale < 1e-9,
"incremental rotation diverged from the direct reference at n_steps={n_steps}, point={point:?}: fast={fast}, reference={reference}"
);
}
}
}
} }
@@ -179,7 +179,6 @@ fn phase_error_from_pose_jitter_degrades_focus_beyond_pose_budget() {
.collect() .collect()
}; };
let freqs = sweep.frequencies();
let focus_at_epsilon = |epsilon: f64| -> f64 { let focus_at_epsilon = |epsilon: f64| -> f64 {
let true_poses = jittered_poses_at(epsilon); let true_poses = jittered_poses_at(epsilon);
// The measurement is recorded at the (jittered) TRUE antenna // The measurement is recorded at the (jittered) TRUE antenna
@@ -191,7 +190,7 @@ fn phase_error_from_pose_jitter_degrades_focus_beyond_pose_budget() {
// happens to focus slightly better and mask the coherence loss // happens to focus slightly better and mask the coherence loss
// this test is measuring). // this test is measuring).
let measurement = simulate_measurement(&true_poses, &sweep, &[target], 0.0, 1); let measurement = simulate_measurement(&true_poses, &sweep, &[target], 0.0, 1);
focus_at_point(&measurement, &nominal_poses, &freqs, &target.position) focus_at_point(&measurement, &nominal_poses, &sweep, &target.position)
}; };
let levels = [0.0, 0.5 * budget, budget, 2.0 * budget, 4.0 * budget]; let levels = [0.0, 0.5 * budget, budget, 2.0 * budget, 4.0 * budget];