Files
ruvnet--RuView/v2/crates/wifi-densepose-signal/src/csi_ratio.rs
T
rUv 004a63e82d fix(security): audit — fix RUSTSEC vulns, clippy warnings, dead code (#769)
- Upgrade openssl to 0.10.78 (CVE-2026-41676), jsonwebtoken to 9.4
- Suppress unmaintained-only/no-CVE advisories in .cargo/audit.toml
  with per-entry rationale
- Fix all `cargo clippy --all-targets -- -D warnings` errors across
  35 crates: derivable_impls, needless_range_loop, map_or→is_some_and/
  is_none_or, await_holding_lock (drop MutexGuard before .await),
  ptr_arg (&mut Vec→&mut [T]), useless_conversion, approximate_constant
  (2.718→E, 3.14→PI), field_reassign_with_default, manual_inspect,
  useless_vec, lines_filter_map_ok, print_literal, dead_code
- Apply `cargo fmt --all`
- Pre-existing test failure in wifi-densepose-signal
  (test_estimate_occupancy_noise_only) is not introduced by this PR
2026-05-23 05:36:13 -04:00

201 lines
6.7 KiB
Rust
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.
//! Conjugate Multiplication (CSI Ratio Model)
//!
//! Cancels carrier frequency offset (CFO), sampling frequency offset (SFO),
//! and packet detection delay by computing `H_i[k] * conj(H_j[k])` across
//! antenna pairs. The resulting phase reflects only environmental changes
//! (human motion), not hardware artifacts.
//!
//! # References
//! - SpotFi: Decimeter Level Localization Using WiFi (SIGCOMM 2015)
//! - IndoTrack: Device-Free Indoor Human Tracking (MobiCom 2017)
use ndarray::Array2;
use num_complex::Complex64;
/// Compute CSI ratio between two antenna streams.
///
/// For each subcarrier k: `ratio[k] = H_ref[k] * conj(H_target[k])`
///
/// This eliminates hardware phase offsets (CFO, SFO, PDD) that are
/// common to both antennas, preserving only the path-difference phase
/// caused by signal propagation through the environment.
pub fn conjugate_multiply(
h_ref: &[Complex64],
h_target: &[Complex64],
) -> Result<Vec<Complex64>, CsiRatioError> {
if h_ref.len() != h_target.len() {
return Err(CsiRatioError::LengthMismatch {
ref_len: h_ref.len(),
target_len: h_target.len(),
});
}
if h_ref.is_empty() {
return Err(CsiRatioError::EmptyInput);
}
Ok(h_ref
.iter()
.zip(h_target.iter())
.map(|(r, t)| r * t.conj())
.collect())
}
/// Compute CSI ratio matrix for all antenna pairs from a multi-antenna CSI snapshot.
///
/// Input: `csi_complex` is (num_antennas × num_subcarriers) complex CSI.
/// Output: For each pair (i, j) where j > i, a row of conjugate-multiplied values.
/// Returns (num_pairs × num_subcarriers) matrix.
pub fn compute_ratio_matrix(
csi_complex: &Array2<Complex64>,
) -> Result<Array2<Complex64>, CsiRatioError> {
let (n_ant, n_sc) = csi_complex.dim();
if n_ant < 2 {
return Err(CsiRatioError::InsufficientAntennas { count: n_ant });
}
let n_pairs = n_ant * (n_ant - 1) / 2;
let mut ratio_matrix = Array2::zeros((n_pairs, n_sc));
let mut pair_idx = 0;
for i in 0..n_ant {
for j in (i + 1)..n_ant {
let ref_row: Vec<Complex64> = csi_complex.row(i).to_vec();
let target_row: Vec<Complex64> = csi_complex.row(j).to_vec();
let ratio = conjugate_multiply(&ref_row, &target_row)?;
for (k, &val) in ratio.iter().enumerate() {
ratio_matrix[[pair_idx, k]] = val;
}
pair_idx += 1;
}
}
Ok(ratio_matrix)
}
/// Extract sanitized amplitude and phase from a CSI ratio matrix.
///
/// Returns (amplitude, phase) each as (num_pairs × num_subcarriers).
pub fn ratio_to_amplitude_phase(ratio: &Array2<Complex64>) -> (Array2<f64>, Array2<f64>) {
let (nrows, ncols) = ratio.dim();
let mut amplitude = Array2::zeros((nrows, ncols));
let mut phase = Array2::zeros((nrows, ncols));
for ((i, j), val) in ratio.indexed_iter() {
amplitude[[i, j]] = val.norm();
phase[[i, j]] = val.arg();
}
(amplitude, phase)
}
/// Errors from CSI ratio computation
#[derive(Debug, thiserror::Error)]
pub enum CsiRatioError {
#[error("Antenna stream length mismatch: ref={ref_len}, target={target_len}")]
LengthMismatch { ref_len: usize, target_len: usize },
#[error("Empty input")]
EmptyInput,
#[error("Need at least 2 antennas, got {count}")]
InsufficientAntennas { count: usize },
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_conjugate_multiply_cancels_common_phase() {
// Both antennas see the same CFO phase offset θ.
// H_1[k] = A1 * exp(j*(φ1 + θ)), H_2[k] = A2 * exp(j*(φ2 + θ))
// ratio = H_1 * conj(H_2) = A1*A2 * exp(j*(φ1 - φ2))
// The common offset θ is cancelled.
let cfo_offset = 1.7; // arbitrary CFO phase
let phi1 = 0.3;
let phi2 = 0.8;
let h1 = vec![Complex64::from_polar(2.0, phi1 + cfo_offset)];
let h2 = vec![Complex64::from_polar(3.0, phi2 + cfo_offset)];
let ratio = conjugate_multiply(&h1, &h2).unwrap();
let result_phase = ratio[0].arg();
let result_amp = ratio[0].norm();
// Phase should be φ1 - φ2, CFO cancelled
assert!((result_phase - (phi1 - phi2)).abs() < 1e-10);
// Amplitude should be A1 * A2
assert!((result_amp - 6.0).abs() < 1e-10);
}
#[test]
fn test_ratio_matrix_pair_count() {
// 3 antennas → 3 pairs, 4 antennas → 6 pairs
let csi = Array2::from_shape_fn((3, 10), |(i, j)| {
Complex64::from_polar(1.0, (i * 10 + j) as f64 * 0.1)
});
let ratio = compute_ratio_matrix(&csi).unwrap();
assert_eq!(ratio.dim(), (3, 10)); // C(3,2) = 3 pairs
let csi4 = Array2::from_shape_fn((4, 8), |(i, j)| {
Complex64::from_polar(1.0, (i * 8 + j) as f64 * 0.1)
});
let ratio4 = compute_ratio_matrix(&csi4).unwrap();
assert_eq!(ratio4.dim(), (6, 8)); // C(4,2) = 6 pairs
}
#[test]
fn test_ratio_preserves_path_difference() {
// Two antennas separated by d, signal from angle θ
// Phase difference = 2π * d * sin(θ) / λ
let wavelength = 0.06; // 5 GHz
let antenna_spacing = 0.025; // 2.5 cm
let arrival_angle = PI / 6.0; // 30 degrees
let path_diff_phase = 2.0 * PI * antenna_spacing * arrival_angle.sin() / wavelength;
let cfo = 2.5; // large CFO
let n_sc = 56;
let csi = Array2::from_shape_fn((2, n_sc), |(ant, k)| {
let sc_phase = k as f64 * 0.05; // subcarrier-dependent phase
let ant_phase = if ant == 0 { 0.0 } else { path_diff_phase };
Complex64::from_polar(1.0, sc_phase + ant_phase + cfo)
});
let ratio = compute_ratio_matrix(&csi).unwrap();
let (_, phase) = ratio_to_amplitude_phase(&ratio);
// All subcarriers should show the same path-difference phase
for j in 0..n_sc {
assert!(
(phase[[0, j]] - (-path_diff_phase)).abs() < 1e-10,
"Subcarrier {} phase={}, expected={}",
j,
phase[[0, j]],
-path_diff_phase
);
}
}
#[test]
fn test_single_antenna_error() {
let csi = Array2::from_shape_fn((1, 10), |(_, j)| Complex64::new(j as f64, 0.0));
assert!(matches!(
compute_ratio_matrix(&csi),
Err(CsiRatioError::InsufficientAntennas { .. })
));
}
#[test]
fn test_length_mismatch() {
let h1 = vec![Complex64::new(1.0, 0.0); 10];
let h2 = vec![Complex64::new(1.0, 0.0); 5];
assert!(matches!(
conjugate_multiply(&h1, &h2),
Err(CsiRatioError::LengthMismatch { .. })
));
}
}