Files
ruvnet--RuView/docs/tutorials/coherent-rf-tomography-backprojection.md
T
ruv d781f20e1a feat(wifi-densepose-sar): coherent wideband RF tomography research crate (ADR-283)
New standalone leaf crate implementing the synthetic-aperture-radar
reconstruction primitive a handheld through-wall RF imaging device
would need: a stepped-frequency multi-position complex forward
measurement simulator, delay-and-sum backprojection reconstruction,
point-cloud extraction, and closed-form range/cross-range resolution
+ antenna-pose coherence-budget formulas checked against the
reconstruction's actual behavior in tests/physics_validation.rs.

Motivated by comparing this repo against Applied Electrodynamics'
"WaveSight" launch. Scoped explicitly below ADR-278's RISE/DiffRadar/
GeRaF reproduction gates: this is the bare measurement-model +
backprojection primitive, not a reproduction of any published system
or a claim about real hardware capability. Every number is
SYNTHETIC/L0 (ADR-282) -- no wideband RF hardware backs this crate.

24 tests (21 unit + 3 integration), 0 failed, clippy-clean. Adds a
tutorial walkthrough and MEASURED backprojection benchmark numbers.
2026-07-30 17:52:31 -04:00

307 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Coherent Wideband RF Tomography: Simulating and Reconstructing with `wifi-densepose-sar`
A walkthrough of the `wifi-densepose-sar` crate (ADR-283): 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)](#1-what-this-is-and-isnt)
2. [Prerequisites](#2-prerequisites)
3. [The Physics in Five Minutes](#3-the-physics-in-five-minutes)
4. [Your First Reconstruction](#4-your-first-reconstruction)
5. [Range Resolution: Why Bandwidth Matters](#5-range-resolution-why-bandwidth-matters)
6. [Cross-Range Resolution: Why You Need to Move the Antenna](#6-cross-range-resolution-why-you-need-to-move-the-antenna)
7. [The Antenna-Pose Coherence Budget](#7-the-antenna-pose-coherence-budget)
8. [Extracting a Point Cloud](#8-extracting-a-point-cloud)
9. [Benchmarking Your Own Scenario](#9-benchmarking-your-own-scenario)
10. [Where This Could Go Next](#10-where-this-could-go-next)
11. [Troubleshooting](#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](https://www.ae-dyn.com/)
— a handheld device that images through walls using radio waves? The
honest answer, worked out in ADR-283, 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](../adr/ADR-282-ruview-ecosystem-positioning.md)
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.
```bash
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:
```text
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:
```text
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:
```rust
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.
```rust
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:
```text
delta_CR ~= lambda * R / (2 * L)
```
— finer with a longer aperture, a shorter wavelength (higher carrier
frequency), or a closer target.
```rust
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`:
```rust
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):
```rust
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-283 §5): that needs real data to
calibrate against, which this crate does not have.
## 9. Benchmarking Your Own Scenario
```bash
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-283 §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-283'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).