Files
ruvnet--RuView/docs/tutorials/coherent-rf-tomography-backprojection.md
T
ruv e4695d8c68 fix: renumber wifi-densepose-sar's ADR from 283 to 287 (number collision)
ADR-283 was already taken by ADR-283-ruview-community-metaharness-flywheel.md,
merged to main before this branch's work started -- picked without checking
against main's actual current ADR list. Renumbered to ADR-287, the next free
slot after ADR-286 (the wifi-densepose-sar-harness ADR, no collision there).

Updated every reference across the crate (Cargo.toml description, lib.rs/
geometry.rs/measurement.rs/pointcloud.rs/reconstruct.rs/resolution.rs doc
comments, tests/physics_validation.rs), its README, the tutorial doc,
CHANGELOG.md, and the workspace Cargo.toml's member comment. 25 tests still
pass after the rename (doc-comment-only changes, no logic touched).
2026-07-31 00:34:35 -04:00

13 KiB
Raw Blame History

Coherent Wideband RF Tomography: Simulating and Reconstructing with wifi-densepose-sar

A walkthrough of the wifi-densepose-sar crate (ADR-287): simulating synthetic-aperture radar (SAR) style measurements and reconstructing a 3D reflectivity image from them via delay-and-sum backprojection.

Estimated time: 30 minutes.

What you will build: A small Rust program that simulates a handheld stepped-frequency radar sweep past a couple of point targets, reconstructs a 3D image from the resulting complex measurements, and extracts a sparse point cloud from it — then verifies the reconstruction's resolution against closed-form theory.

Who this is for: Rust developers comfortable with basic signal processing terminology (frequency, bandwidth, phase) who want to understand what a coherent RF imaging pipeline actually computes, or who are evaluating whether this crate is a useful building block for their own radar-imaging research.


Table of Contents

  1. What This Is (and Isn't)
  2. Prerequisites
  3. The Physics in Five Minutes
  4. Your First Reconstruction
  5. Range Resolution: Why Bandwidth Matters
  6. Cross-Range Resolution: Why You Need to Move the Antenna
  7. The Antenna-Pose Coherence Budget
  8. Extracting a Point Cloud
  9. Benchmarking Your Own Scenario
  10. Where This Could Go Next
  11. Troubleshooting

1. What This Is (and Isn't)

This crate exists because of a real question: could this repo build something like Applied Electrodynamics' WaveSight — a handheld device that images through walls using radio waves? The honest answer, worked out in ADR-287, is no, not as a hardware product — that needs a custom coherent RF front end, a calibrated antenna array, and real-time reconstruction hardware, which is an 1836 month, high six-to-seven-figure hardware engineering program, not a software change.

What is useful to build, and what this crate is, is the reconstruction algorithm such a device needs: given coherent, phase-preserving, stepped-frequency measurements recorded from several known antenna positions, recover the 3D locations of the things that reflected the signal. That's a well-understood problem (synthetic-aperture radar, ground-penetrating radar imaging, and microwave tomography all solve versions of it) with textbook closed-form math behind it.

Every number in this crate comes from its own synthetic forward simulator — there is no real radio hardware anywhere in this crate, and none of its tests, benchmarks, or accuracy numbers say anything about how well a real device would perform through a real wall. That's evidence level L0 (Synthetic) in this repo's ADR-282 evidence ladder, and it stays L0 until (if ever) real wideband RF hardware feeds this pipeline real measurements.

2. Prerequisites

  • Rust 1.75+ (workspace MSRV), already set up if you can build the rest of this repo's v2/ workspace.
  • No special hardware. Everything in this tutorial runs from synthetic data.
cd v2
cargo test -p wifi-densepose-sar --no-default-features

If that passes (24 tests, 0 failed), you're ready.

3. The Physics in Five Minutes

A stepped-frequency radar sweeps K frequencies f_0..f_{K-1} across a band of total width B (the bandwidth). At each of M antenna positions p_0..p_{M-1} along a handheld sweep, it records one complex number per frequency — amplitude and phase, not just amplitude, which is what makes this "coherent."

For a point scatterer at position x with reflectivity σ, range R = |p_m - x| from antenna position m, the forward model this crate simulates is:

y_{m,k} = sigma / R^2 * exp(-i * 4*pi * f_k * R / c)

4*pi*f*R/c is the two-way (round-trip) propagation phase; 1/R^2 is the two-way free-space spreading loss. With several targets, the measurement is just the sum of each target's contribution (superposition — this crate never models multipath/interaction between targets, only free-space direct paths).

Reconstruction (backprojection) inverts this: for every candidate voxel x in a 3D grid, it multiplies each measurement by the complex conjugate of the phase the forward model would have applied for a target at x, then sums:

I(x) = | (1/MK) * sum_m sum_k  y_{m,k} * R_{m,x}^2 * exp(+i * 4*pi * f_k * R_{m,x} / c) |

If x coincides with a real target, every term's phase correction exactly cancels the phase the forward model applied — the sum adds up constructively ("coherent gain"). At any other voxel, the phases are essentially uncorrelated across the (m, k) grid and the sum averages toward zero. That's the entire algorithm: matched filtering, done in 3D, one voxel at a time.

4. Your First Reconstruction

Add wifi-densepose-sar to a scratch binary or run this in a workspace example. It simulates two targets, reconstructs, and finds the brightest voxel:

use wifi_densepose_sar::{
    backproject, linear_aperture, simulate_measurement, FrequencySweep,
    Point3, ScatteringTarget, VoxelGrid,
};

fn main() {
    // A 1-meter handheld sweep, 21 antenna positions along it.
    let poses = linear_aperture(
        Point3::new(-0.5, 0.0, 0.0),
        Point3::new(0.5, 0.0, 0.0),
        21,
    );

    // Sweep 2-6 GHz (4 GHz of bandwidth) in 32 steps.
    let sweep = FrequencySweep::new(2.0e9, 6.0e9, 32);

    // One target, 2 meters downrange, reflectivity 1.0 (arbitrary units).
    let target = ScatteringTarget::new(Point3::new(0.0, 2.0, 0.0), 1.0);

    // Simulate the measurement with a touch of noise (seeded -- rerunning
    // with the same seed gives byte-identical output).
    let measurement = simulate_measurement(&poses, &sweep, &[target], 0.01, 42);

    // Reconstruct a 21x21x21 voxel grid around where we expect the target.
    let grid = VoxelGrid::new(Point3::new(-0.3, 1.7, -0.3), 0.03, 21, 21, 21);
    let image = backproject(&measurement, &poses, &sweep, &grid);

    let (peak_location, peak_magnitude) = image.peak();
    println!("true target:          {:?}", target.position);
    println!("reconstructed peak:   {peak_location:?} (magnitude {peak_magnitude:.4})");
}

Run it and you should see the reconstructed peak within a couple of centimeters of the true target position — well inside the voxel spacing used here (3 cm). That's tests/reconstruct.rs::single_point_target_reconstructs_at_its_true_location running live.

5. Range Resolution: Why Bandwidth Matters

How close together can two targets be along the same bearing (same antenna, different distance) before they blur into one blob? The classic radar answer: ΔR = c / (2B) — resolution improves with more swept bandwidth, full stop. Carrier frequency, antenna count, and aperture length don't enter into it at all.

use wifi_densepose_sar::resolution::range_resolution_m;

let dr = range_resolution_m(4.0e9); // 4 GHz swept bandwidth
println!("range resolution: {:.1} cm", dr * 100.0);
// -> range resolution: 3.7 cm

tests/physics_validation.rs::range_separated_targets_resolve_only_beyond_range_resolution proves this isn't just a formula sitting in a doc comment: it forward-simulates two targets 4x ΔR apart (they resolve into two distinct peaks) and 0.25x ΔR apart (they merge into one), using the same range_resolution_m call to pick the separations.

6. Cross-Range Resolution: Why You Need to Move the Antenna

A single antenna position, no matter how much bandwidth it sweeps, cannot tell two targets apart if they're at the same range but different bearing — all it measures is round-trip distance, which is the same for both. This is exactly why "handheld... sweep the antenna around" matters: moving the antenna across a synthetic aperture of length L gives you angular information, with cross-range resolution:

delta_CR ~= lambda * R / (2 * L)

— finer with a longer aperture, a shorter wavelength (higher carrier frequency), or a closer target.

use wifi_densepose_sar::resolution::cross_range_resolution_m;

let short = cross_range_resolution_m(4.0e9, 0.05, 2.0); // 5cm sweep
let long = cross_range_resolution_m(4.0e9, 1.0, 2.0);   // 1m sweep
println!("5cm aperture:  {:.2} m cross-range resolution", short);
println!("1m aperture:   {:.2} m cross-range resolution", long);
// -> a 20x longer aperture gives 20x finer cross-range resolution

tests/physics_validation.rs::cross_range_separated_targets_resolve_only_with_long_enough_aperture demonstrates this end-to-end: the same pair of cross-range-separated targets resolves into two peaks with a 1m synthetic aperture and collapses into one with a 5cm aperture, no other change.

7. The Antenna-Pose Coherence Budget

Backprojection assumes you know exactly where the antenna was at each measurement. If your position tracking (in a real device: visual-inertial odometry, encoders, whatever) is off by Δp, the phase correction applied during reconstruction is wrong by an amount that grows with Δp and with frequency. The classical rule of thumb for "still well focused": keep the round-trip path error under a quarter wavelength, which works out to an antenna-position tolerance of λ/8:

use wifi_densepose_sar::resolution::max_coherent_pose_error_m;

let budget = max_coherent_pose_error_m(8.0e9); // 8 GHz carrier
println!("position tolerance at 8 GHz: {:.1} mm", budget * 1000.0);
// -> position tolerance at 8 GHz: 4.7 mm

tests/physics_validation.rs::phase_error_from_pose_jitter_degrades_focus_beyond_pose_budget verifies this isn't just asserted: it perturbs the true antenna positions away from the assumed ones used in reconstruction, and shows focus at the true target location degrades as that perturbation grows — the concrete mechanism behind why real SAR/GPR imaging systems need accurate pose tracking, not just a good radio.

8. Extracting a Point Cloud

A dense voxel grid isn't a useful end product — you want a short list of detected points:

Continuing the program from §4 (which already has image in scope):

use wifi_densepose_sar::extract_point_cloud;

let points = extract_point_cloud(&image, 0.5); // 50%-of-peak threshold
for p in &points {
    println!("{:?} magnitude={:.3}", p.position, p.magnitude);
}

extract_point_cloud does threshold + 6-connected local-maximum extraction — a real blob will still yield one point, not one per voxel inside it. There is deliberately no clustering, material classification, or confidence calibration here (ADR-287 §5): that needs real data to calibrate against, which this crate does not have.

9. Benchmarking Your Own Scenario

cargo bench -p wifi-densepose-sar

The shipped benchmark (benches/backprojection_bench.rs) sweeps 512 / 4,096 / 32,768-voxel grids with 21 poses x 32 frequencies. Reconstruction is embarrassingly parallel over voxels (each voxel's cost is independent), so it's rayon-parallelized already — see the crate README for the last recorded MEASURED numbers on the reference machine.

10. Where This Could Go Next

This crate deliberately stops short of several things (ADR-287 §5):

  • It's monostatic (one antenna, both TX and RX) — real handheld SAR/MIMO devices often use multiple simultaneous antenna elements.
  • The forward model is free-space only — no multipath, no per-material attenuation (contrast ruview-unified's narrowband Fresnel material model, which isn't yet extended to wideband).
  • It isn't wired into ruview-unified's FmcwRadarCube adapter or GaussianMap — ADR-278 names that as the eventual integration point, once (and if) a reconstruction system is ready for it.

If you're picking this up to extend it, start with ADR-287's "Follow-up" section rather than guessing at scope.

11. Troubleshooting

"My reconstructed peak isn't near my target." Check your voxel grid actually covers the target's true location — backproject happily reconstructs whatever region you ask for; if the target is outside the grid, you'll get whatever's brightest inside it instead (usually noise).

"Two targets I expected to resolve didn't." Compute range_resolution_m/cross_range_resolution_m for your actual bandwidth and aperture length and check your separation against them — resolution is a hard physical limit here, not a tuning parameter.

"Backprojection is slow for my grid size." Cost is O(voxels x poses x freqs) and already parallelized over voxels via rayon; the only way to go faster is fewer voxels, fewer poses, or fewer frequency steps (each is a hard tradeoff against resolution or aperture coverage — see §5/§6).