Files
ruvnet--RuView/v2/crates/wifi-densepose-sar/src/geometry.rs
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

141 lines
4.3 KiB
Rust

//! Antenna positions, synthetic-aperture trajectories, and point geometry.
use serde::{Deserialize, Serialize};
/// A point in 3D space, meters, in an arbitrary right-handed scene frame.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Point3 {
/// X coordinate, meters.
pub x: f64,
/// Y coordinate, meters.
pub y: f64,
/// Z coordinate, meters.
pub z: f64,
}
impl Point3 {
/// Construct a point.
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
/// Euclidean distance to another point, meters.
pub fn distance(&self, other: &Point3) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
let dz = self.z - other.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
/// Unit vector pointing from `self` toward `other`. Returns `None` if
/// the two points coincide (distance below `f64::EPSILON`).
pub fn direction_to(&self, other: &Point3) -> Option<Point3> {
let d = self.distance(other);
if d < f64::EPSILON {
return None;
}
Some(Point3::new(
(other.x - self.x) / d,
(other.y - self.y) / d,
(other.z - self.z) / d,
))
}
/// Translate this point by `dist` meters along a unit vector `dir`.
pub fn translated(&self, dir: Point3, dist: f64) -> Point3 {
Point3::new(
self.x + dir.x * dist,
self.y + dir.y * dist,
self.z + dir.z * dist,
)
}
}
/// A single antenna position along a synthetic-aperture trajectory.
///
/// Only position is modeled (an isotropic-antenna approximation, ADR-287
/// §4) -- no antenna gain pattern / boresight direction is applied to the
/// forward measurement model.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct AntennaPose {
/// Antenna phase-center position, meters.
pub position: Point3,
}
impl AntennaPose {
/// Construct a pose at `position`.
pub fn new(position: Point3) -> Self {
Self { position }
}
}
/// Generate `n` antenna poses evenly spaced along a straight line segment
/// from `start` to `end` (inclusive), the canonical "handheld linear sweep"
/// synthetic aperture. `n` must be >= 2 for a non-degenerate aperture.
pub fn linear_aperture(start: Point3, end: Point3, n: usize) -> Vec<AntennaPose> {
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![AntennaPose::new(start)];
}
(0..n)
.map(|i| {
let t = i as f64 / (n - 1) as f64;
AntennaPose::new(Point3::new(
start.x + (end.x - start.x) * t,
start.y + (end.y - start.y) * t,
start.z + (end.z - start.z) * t,
))
})
.collect()
}
/// The physical length of a synthetic aperture: the distance between its
/// first and last pose. Used by [`crate::resolution::cross_range_resolution`].
pub fn aperture_length(poses: &[AntennaPose]) -> f64 {
match (poses.first(), poses.last()) {
(Some(a), Some(b)) => a.position.distance(&b.position),
_ => 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_aperture_spans_endpoints() {
let start = Point3::new(0.0, 0.0, 0.0);
let end = Point3::new(1.0, 0.0, 0.0);
let poses = linear_aperture(start, end, 5);
assert_eq!(poses.len(), 5);
assert_eq!(poses[0].position, start);
assert_eq!(poses[4].position, end);
// Evenly spaced: 0, 0.25, 0.5, 0.75, 1.0 along x.
assert!((poses[2].position.x - 0.5).abs() < 1e-12);
}
#[test]
fn aperture_length_matches_endpoint_distance() {
let poses = linear_aperture(Point3::new(0.0, 0.0, 0.0), Point3::new(3.0, 4.0, 0.0), 10);
assert!((aperture_length(&poses) - 5.0).abs() < 1e-9);
}
#[test]
fn direction_to_is_unit_length() {
let a = Point3::new(0.0, 0.0, 0.0);
let b = Point3::new(2.0, 0.0, 0.0);
let dir = a.direction_to(&b).unwrap();
assert!((dir.x - 1.0).abs() < 1e-12);
let len = (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z).sqrt();
assert!((len - 1.0).abs() < 1e-12);
}
#[test]
fn direction_to_coincident_points_is_none() {
let a = Point3::new(1.0, 1.0, 1.0);
assert!(a.direction_to(&a).is_none());
}
}