mirror of
https://github.com/ruvnet/RuView
synced 2026-08-11 20:41:44 +00:00
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
This commit is contained in:
@@ -17,8 +17,8 @@
|
||||
//! then feed CSI frames through the pipeline stages.
|
||||
|
||||
use ruvector_crv::{
|
||||
AOLDetection, ConvergenceResult, CrvConfig, CrvError, CrvSessionManager, GestaltType,
|
||||
GeometricKind, SensoryModality, SketchElement, SpatialRelationType, SpatialRelationship,
|
||||
AOLDetection, ConvergenceResult, CrvConfig, CrvError, CrvSessionManager, GeometricKind,
|
||||
GestaltType, SensoryModality, SketchElement, SpatialRelationType, SpatialRelationship,
|
||||
StageIData, StageIIData, StageIIIData, StageIVData, StageVData, StageVIData,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -203,8 +203,7 @@ impl CsiGestaltClassifier {
|
||||
// Movement: high variance + periodic.
|
||||
// Suppress when water or energy are strong indicators.
|
||||
let movement_suppress = water_score.max(energy_score);
|
||||
let movement_score = if variance > self.thresholds.variance_high
|
||||
&& movement_suppress < 0.6
|
||||
let movement_score = if variance > self.thresholds.variance_high && movement_suppress < 0.6
|
||||
{
|
||||
0.6 + 0.4 * periodicity
|
||||
} else if variance > self.thresholds.variance_high {
|
||||
@@ -241,13 +240,12 @@ impl CsiGestaltClassifier {
|
||||
.max(energy_score)
|
||||
.max(movement_score)
|
||||
.max(natural_score);
|
||||
let manmade_score = if structure > self.thresholds.structure_threshold
|
||||
&& manmade_suppress < 0.5
|
||||
{
|
||||
0.5 + 0.5 * structure
|
||||
} else {
|
||||
0.15 * structure * (1.0 - manmade_suppress).max(0.0)
|
||||
};
|
||||
let manmade_score =
|
||||
if structure > self.thresholds.structure_threshold && manmade_suppress < 0.5 {
|
||||
0.5 + 0.5 * structure
|
||||
} else {
|
||||
0.15 * structure * (1.0 - manmade_suppress).max(0.0)
|
||||
};
|
||||
scores[4] = (GestaltType::Manmade, manmade_score);
|
||||
|
||||
// Pick the highest-scoring type.
|
||||
@@ -346,10 +344,7 @@ impl CsiGestaltClassifier {
|
||||
}
|
||||
|
||||
// Compute successive differences.
|
||||
let diffs: Vec<f32> = amplitudes
|
||||
.windows(2)
|
||||
.map(|w| (w[1] - w[0]).abs())
|
||||
.collect();
|
||||
let diffs: Vec<f32> = amplitudes.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
|
||||
let mean_diff = diffs.iter().sum::<f32>() / diffs.len().max(1) as f32;
|
||||
let var_diff = if diffs.len() > 1 {
|
||||
diffs.iter().map(|d| (d - mean_diff).powi(2)).sum::<f32>() / (diffs.len() - 1) as f32
|
||||
@@ -419,11 +414,7 @@ impl CsiSensoryEncoder {
|
||||
///
|
||||
/// Returns a list of `(SensoryModality, descriptor_string)` pairs
|
||||
/// suitable for feeding into [`ruvector_crv::StageIIEncoder`].
|
||||
pub fn extract(
|
||||
&self,
|
||||
amplitudes: &[f32],
|
||||
phases: &[f32],
|
||||
) -> Vec<(SensoryModality, String)> {
|
||||
pub fn extract(&self, amplitudes: &[f32], phases: &[f32]) -> Vec<(SensoryModality, String)> {
|
||||
let mut impressions = Vec::new();
|
||||
|
||||
// Texture: amplitude roughness (high-freq variance).
|
||||
@@ -605,11 +596,7 @@ impl WifiCrvPipeline {
|
||||
/// The `session_id` identifies the sensing session and `room_id`
|
||||
/// acts as the CRV target coordinate so that cross-session
|
||||
/// convergence can be computed per room.
|
||||
pub fn create_session(
|
||||
&mut self,
|
||||
session_id: &str,
|
||||
room_id: &str,
|
||||
) -> Result<(), CrvError> {
|
||||
pub fn create_session(&mut self, session_id: &str, room_id: &str) -> Result<(), CrvError> {
|
||||
self.manager
|
||||
.create_session(session_id.to_string(), room_id.to_string())
|
||||
}
|
||||
@@ -625,9 +612,7 @@ impl WifiCrvPipeline {
|
||||
phases: &[f32],
|
||||
) -> Result<CsiCrvResult, CrvError> {
|
||||
if amplitudes.is_empty() {
|
||||
return Err(CrvError::EmptyInput(
|
||||
"CSI amplitudes are empty".to_string(),
|
||||
));
|
||||
return Err(CrvError::EmptyInput("CSI amplitudes are empty".to_string()));
|
||||
}
|
||||
|
||||
// Stage I: Gestalt classification.
|
||||
@@ -789,9 +774,7 @@ impl WifiCrvPipeline {
|
||||
query_embedding: &[f32],
|
||||
) -> Result<StageVData, CrvError> {
|
||||
if query_embedding.is_empty() {
|
||||
return Err(CrvError::EmptyInput(
|
||||
"Query embedding is empty".to_string(),
|
||||
));
|
||||
return Err(CrvError::EmptyInput("Query embedding is empty".to_string()));
|
||||
}
|
||||
|
||||
// Probe all stages 1-4 with the query.
|
||||
@@ -814,10 +797,7 @@ impl WifiCrvPipeline {
|
||||
/// Uses MinCut to partition the accumulated session data into
|
||||
/// distinct target aspects -- in the WiFi sensing context these
|
||||
/// correspond to distinct persons or environment zones.
|
||||
pub fn partition_persons(
|
||||
&mut self,
|
||||
session_id: &str,
|
||||
) -> Result<StageVIData, CrvError> {
|
||||
pub fn partition_persons(&mut self, session_id: &str) -> Result<StageVIData, CrvError> {
|
||||
self.manager.run_stage_vi(session_id)
|
||||
}
|
||||
|
||||
@@ -876,7 +856,13 @@ mod tests {
|
||||
/// Generate a periodic amplitude signal.
|
||||
fn periodic_signal(n: usize, freq: f32, amplitude: f32) -> Vec<f32> {
|
||||
(0..n)
|
||||
.map(|i| amplitude * (2.0 * std::f32::consts::PI * freq * i as f32 / n as f32).sin().abs() + 0.1)
|
||||
.map(|i| {
|
||||
amplitude
|
||||
* (2.0 * std::f32::consts::PI * freq * i as f32 / n as f32)
|
||||
.sin()
|
||||
.abs()
|
||||
+ 0.1
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -906,7 +892,10 @@ mod tests {
|
||||
let phases = linear_phases(64);
|
||||
let (gestalt, conf) = classifier.classify(&s, &phases);
|
||||
assert_eq!(gestalt, GestaltType::Movement);
|
||||
assert!(conf > 0.3, "movement confidence should be reasonable: {conf}");
|
||||
assert!(
|
||||
conf > 0.3,
|
||||
"movement confidence should be reasonable: {conf}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -951,7 +940,9 @@ mod tests {
|
||||
..GestaltThresholds::default()
|
||||
});
|
||||
// Perfectly regular alternating pattern.
|
||||
let amps: Vec<f32> = (0..64).map(|i| if i % 2 == 0 { 1.0 } else { 0.8 }).collect();
|
||||
let amps: Vec<f32> = (0..64)
|
||||
.map(|i| if i % 2 == 0 { 1.0 } else { 0.8 })
|
||||
.collect();
|
||||
let phases = linear_phases(64);
|
||||
let (gestalt, conf) = classifier.classify(&s, &phases);
|
||||
assert_eq!(gestalt, GestaltType::Manmade);
|
||||
@@ -1024,7 +1015,9 @@ mod tests {
|
||||
let amps = static_signal(32, 1.0);
|
||||
let phases = vec![0.5f32; 32]; // identical phases = high coherence
|
||||
let impressions = encoder.extract(&s, &phases);
|
||||
let lum = impressions.iter().find(|(m, _)| *m == SensoryModality::Luminosity);
|
||||
let lum = impressions
|
||||
.iter()
|
||||
.find(|(m, _)| *m == SensoryModality::Luminosity);
|
||||
assert!(lum.is_some());
|
||||
let desc = &lum.unwrap().1;
|
||||
assert!(
|
||||
@@ -1039,7 +1032,9 @@ mod tests {
|
||||
let amps = static_signal(32, 0.01);
|
||||
let phases = linear_phases(32);
|
||||
let impressions = encoder.extract(&s, &phases);
|
||||
let temp = impressions.iter().find(|(m, _)| *m == SensoryModality::Temperature);
|
||||
let temp = impressions
|
||||
.iter()
|
||||
.find(|(m, _)| *m == SensoryModality::Temperature);
|
||||
assert!(temp.is_some());
|
||||
assert!(
|
||||
temp.unwrap().1.contains("cold"),
|
||||
@@ -1174,8 +1169,16 @@ mod tests {
|
||||
|
||||
// Add mesh topology.
|
||||
let nodes = vec![
|
||||
ApNode { id: "ap-1".into(), position: (0.0, 0.0), coverage_radius: 10.0 },
|
||||
ApNode { id: "ap-2".into(), position: (5.0, 3.0), coverage_radius: 8.0 },
|
||||
ApNode {
|
||||
id: "ap-1".into(),
|
||||
position: (0.0, 0.0),
|
||||
coverage_radius: 10.0,
|
||||
},
|
||||
ApNode {
|
||||
id: "ap-2".into(),
|
||||
position: (5.0, 3.0),
|
||||
coverage_radius: 8.0,
|
||||
},
|
||||
];
|
||||
let links = vec![ApLink {
|
||||
from: "ap-1".into(),
|
||||
@@ -1252,9 +1255,7 @@ mod tests {
|
||||
.process_csi_frame("viewer-b", &s, &phases)
|
||||
.unwrap();
|
||||
|
||||
let convergence = pipeline
|
||||
.find_cross_room_convergence("room-1", 0.5)
|
||||
.unwrap();
|
||||
let convergence = pipeline.find_cross_room_convergence("room-1", 0.5).unwrap();
|
||||
assert!(
|
||||
!convergence.scores.is_empty(),
|
||||
"identical frames should converge"
|
||||
@@ -1273,12 +1274,8 @@ mod tests {
|
||||
let amps_b = static_signal(32, 0.01);
|
||||
let phases = linear_phases(32);
|
||||
|
||||
pipeline
|
||||
.process_csi_frame("a", &s_a, &phases)
|
||||
.unwrap();
|
||||
pipeline
|
||||
.process_csi_frame("b", &s_b, &phases)
|
||||
.unwrap();
|
||||
pipeline.process_csi_frame("a", &s_a, &phases).unwrap();
|
||||
pipeline.process_csi_frame("b", &s_b, &phases).unwrap();
|
||||
|
||||
let convergence = pipeline.find_cross_room_convergence("room-2", 0.95);
|
||||
// May or may not converge at high threshold; the key is no panic.
|
||||
@@ -1370,7 +1367,10 @@ mod tests {
|
||||
#[test]
|
||||
fn compute_null_fraction_all_zeros() {
|
||||
let f = CsiGestaltClassifier::compute_null_fraction(&[0.0; 32]);
|
||||
assert!((f - 1.0).abs() < 1e-6, "all zeros should give null fraction 1.0");
|
||||
assert!(
|
||||
(f - 1.0).abs() < 1e-6,
|
||||
"all zeros should give null fraction 1.0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1397,14 +1397,20 @@ mod tests {
|
||||
fn signal_energy_known() {
|
||||
let encoder = CsiSensoryEncoder::new();
|
||||
let energy = encoder.signal_energy(&[2.0, 2.0, 2.0, 2.0]);
|
||||
assert!((energy - 4.0).abs() < 1e-6, "energy of [2,2,2,2] should be 4.0");
|
||||
assert!(
|
||||
(energy - 4.0).abs() < 1e-6,
|
||||
"energy of [2,2,2,2] should be 4.0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_coherence_identical() {
|
||||
let encoder = CsiSensoryEncoder::new();
|
||||
let c = encoder.phase_coherence(&[1.0; 100]);
|
||||
assert!(c > 0.99, "identical phases should give coherence ~1.0, got {c}");
|
||||
assert!(
|
||||
c > 0.99,
|
||||
"identical phases should give coherence ~1.0, got {c}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1418,7 +1424,10 @@ mod tests {
|
||||
fn subcarrier_spread_all_active() {
|
||||
let encoder = CsiSensoryEncoder::new();
|
||||
let spread = encoder.subcarrier_spread(&[1.0; 32]);
|
||||
assert!((spread - 1.0).abs() < 1e-6, "all active should give spread 1.0");
|
||||
assert!(
|
||||
(spread - 1.0).abs() < 1e-6,
|
||||
"all active should give spread 1.0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -209,7 +209,10 @@ mod tests {
|
||||
log_b.push(&s, 0.25, 999_999);
|
||||
let wa = log_a.iter().next().unwrap().witness_sha256;
|
||||
let wb = log_b.iter().next().unwrap().witness_sha256;
|
||||
assert_eq!(wa, wb, "witness must be content-addressable, not time-addressable");
|
||||
assert_eq!(
|
||||
wa, wb,
|
||||
"witness must be content-addressable, not time-addressable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -36,6 +36,6 @@ pub mod viewpoint;
|
||||
|
||||
pub use event_log::{NoveltyEvent, PrivacyEventLog};
|
||||
pub use sketch::{
|
||||
Sketch, SketchBank, SketchError, WireSketch, WireSketchError,
|
||||
WIRE_SKETCH_FORMAT_VERSION, WIRE_SKETCH_MAGIC, WIRE_SKETCH_MAX_BYTES,
|
||||
Sketch, SketchBank, SketchError, WireSketch, WireSketchError, WIRE_SKETCH_FORMAT_VERSION,
|
||||
WIRE_SKETCH_MAGIC, WIRE_SKETCH_MAX_BYTES,
|
||||
};
|
||||
|
||||
@@ -89,11 +89,17 @@ mod tests {
|
||||
let mut buf = CompressedBreathingBuffer::new(n_subcarriers, 1);
|
||||
|
||||
for i in 0..20 {
|
||||
let amplitudes: Vec<f32> = (0..n_subcarriers).map(|s| (i * n_subcarriers + s) as f32 * 0.01).collect();
|
||||
let amplitudes: Vec<f32> = (0..n_subcarriers)
|
||||
.map(|s| (i * n_subcarriers + s) as f32 * 0.01)
|
||||
.collect();
|
||||
buf.push_frame(&litudes);
|
||||
}
|
||||
|
||||
assert_eq!(buf.frame_count(), 20, "frame_count must equal the number of pushed frames");
|
||||
assert_eq!(
|
||||
buf.frame_count(),
|
||||
20,
|
||||
"frame_count must equal the number of pushed frames"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -85,11 +85,17 @@ mod tests {
|
||||
let mut spec = CompressedHeartbeatSpectrogram::new(n_freq_bins);
|
||||
|
||||
for i in 0..10 {
|
||||
let column: Vec<f32> = (0..n_freq_bins).map(|b| (i * n_freq_bins + b) as f32 * 0.01).collect();
|
||||
let column: Vec<f32> = (0..n_freq_bins)
|
||||
.map(|b| (i * n_freq_bins + b) as f32 * 0.01)
|
||||
.collect();
|
||||
spec.push_column(&column);
|
||||
}
|
||||
|
||||
assert_eq!(spec.frame_count(), 10, "frame_count must equal the number of pushed columns");
|
||||
assert_eq!(
|
||||
spec.frame_count(),
|
||||
10,
|
||||
"frame_count must equal the number of pushed columns"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -46,8 +46,7 @@ pub fn solve_triangulation(
|
||||
col0.push(xi - xj);
|
||||
col1.push(yi - yj);
|
||||
b.push(
|
||||
C * tdoa / 2.0
|
||||
+ ((xi * xi - xj * xj) + (yi * yi - yj * yj)) / 2.0
|
||||
C * tdoa / 2.0 + ((xi * xi - xj * xj) + (yi * yi - yj * yj)) / 2.0
|
||||
- x_ref * (xi - xj)
|
||||
- y_ref * (yi - yj),
|
||||
);
|
||||
@@ -99,9 +98,8 @@ mod tests {
|
||||
((survivor.0 - ap.0).powi(2) + (survivor.1 - ap.1).powi(2)).sqrt()
|
||||
};
|
||||
|
||||
let tdoa = |i: usize, j: usize| -> f32 {
|
||||
(dist(ap_positions[i]) - dist(ap_positions[j])) / c
|
||||
};
|
||||
let tdoa =
|
||||
|i: usize, j: usize| -> f32 { (dist(ap_positions[i]) - dist(ap_positions[j])) / c };
|
||||
|
||||
let measurements = vec![
|
||||
(1, 0, tdoa(1, 0)),
|
||||
@@ -133,6 +131,9 @@ mod tests {
|
||||
fn triangulation_too_few_measurements_returns_none() {
|
||||
let ap_positions = vec![(0.0_f32, 0.0), (10.0, 0.0), (10.0, 10.0)];
|
||||
let result = solve_triangulation(&[(0, 1, 1e-9), (1, 2, 1e-9)], &ap_positions);
|
||||
assert!(result.is_none(), "fewer than 3 measurements must return None");
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"fewer than 3 measurements must return None"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,11 @@ mod tests {
|
||||
let n_velocity_bins = 8;
|
||||
|
||||
let stft_rows: Vec<Vec<f32>> = (0..n_subcarriers)
|
||||
.map(|sc| (0..n_velocity_bins).map(|v| (sc * n_velocity_bins + v) as f32 * 0.1).collect())
|
||||
.map(|sc| {
|
||||
(0..n_velocity_bins)
|
||||
.map(|v| (sc * n_velocity_bins + v) as f32 * 0.1)
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let sensitivity = vec![0.5_f32, 0.3, 0.8];
|
||||
|
||||
|
||||
@@ -72,7 +72,10 @@ mod tests {
|
||||
];
|
||||
|
||||
let result = solve_fresnel_geometry(&observations, d_total);
|
||||
assert!(result.is_some(), "solver must return Some for 5 observations");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"solver must return Some for 5 observations"
|
||||
);
|
||||
|
||||
let (d1, d2) = result.unwrap();
|
||||
let sum = d1 + d2;
|
||||
@@ -87,6 +90,9 @@ mod tests {
|
||||
#[test]
|
||||
fn fresnel_too_few_observations_returns_none() {
|
||||
let result = solve_fresnel_geometry(&[(0.125, 0.3), (0.130, 0.25)], 5.0);
|
||||
assert!(result.is_none(), "fewer than 3 observations must return None");
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"fewer than 3 observations must return None"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,21 @@ use ruvector_attn_mincut::attn_mincut;
|
||||
/// # Returns
|
||||
///
|
||||
/// Gated spectrogram of the same length `n_freq * n_time`.
|
||||
pub fn gate_spectrogram(spectrogram: &[f32], n_freq: usize, n_time: usize, lambda: f32) -> Vec<f32> {
|
||||
pub fn gate_spectrogram(
|
||||
spectrogram: &[f32],
|
||||
n_freq: usize,
|
||||
n_time: usize,
|
||||
lambda: f32,
|
||||
) -> Vec<f32> {
|
||||
let out = attn_mincut(
|
||||
spectrogram, // q
|
||||
spectrogram, // k
|
||||
spectrogram, // v
|
||||
n_freq, // d: feature dimension
|
||||
n_time, // seq_len: number of time frames
|
||||
lambda, // lambda: min-cut threshold
|
||||
2, // tau: temporal hysteresis window
|
||||
1e-7_f32, // eps: numerical epsilon
|
||||
spectrogram, // q
|
||||
spectrogram, // k
|
||||
spectrogram, // v
|
||||
n_freq, // d: feature dimension
|
||||
n_time, // seq_len: number of time frames
|
||||
lambda, // lambda: min-cut threshold
|
||||
2, // tau: temporal hysteresis window
|
||||
1e-7_f32, // eps: numerical epsilon
|
||||
);
|
||||
out.output
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ pub fn mincut_subcarrier_partition(sensitivity: &[f32]) -> (Vec<usize>, Vec<usiz
|
||||
|
||||
// Source connects to subcarriers with above-average sensitivity.
|
||||
// Sink connects to subcarriers with below-average sensitivity.
|
||||
for i in 0..n {
|
||||
let cap = (sensitivity[i] as f64).abs() + 1e-6;
|
||||
if sensitivity[i] >= mean_sens {
|
||||
for (i, &sens) in sensitivity.iter().enumerate().take(n) {
|
||||
let cap = (sens as f64).abs() + 1e-6;
|
||||
if sens >= mean_sens {
|
||||
edges.push((source, i as u64, cap));
|
||||
} else {
|
||||
edges.push((i as u64, sink, cap));
|
||||
@@ -176,10 +176,17 @@ mod tests {
|
||||
|
||||
// Both groups must be non-empty for a non-trivial input.
|
||||
assert!(!sensitive.is_empty(), "sensitive group must not be empty");
|
||||
assert!(!insensitive.is_empty(), "insensitive group must not be empty");
|
||||
assert!(
|
||||
!insensitive.is_empty(),
|
||||
"insensitive group must not be empty"
|
||||
);
|
||||
|
||||
// Together they must cover every index exactly once.
|
||||
let mut all_indices: Vec<usize> = sensitive.iter().chain(insensitive.iter()).cloned().collect();
|
||||
let mut all_indices: Vec<usize> = sensitive
|
||||
.iter()
|
||||
.chain(insensitive.iter())
|
||||
.cloned()
|
||||
.collect();
|
||||
all_indices.sort_unstable();
|
||||
let expected: Vec<usize> = (0..10).collect();
|
||||
assert_eq!(all_indices, expected, "partition must cover all 10 indices");
|
||||
@@ -214,7 +221,7 @@ mod tests {
|
||||
// the same way (either all sensitive or all insensitive after mincut).
|
||||
// At minimum, no weight should exceed 2.0 or be negative.
|
||||
for &wt in &w {
|
||||
assert!(wt >= 0.5 && wt <= 2.0, "weight {wt} out of range");
|
||||
assert!((0.5..=2.0).contains(&wt), "weight {wt} out of range");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -141,10 +141,7 @@ impl Sketch {
|
||||
/// over-long input should fail loudly rather than silently
|
||||
/// produce a sketch that disagrees with its source on
|
||||
/// `embedding_dim`.
|
||||
pub fn try_from_embedding(
|
||||
embedding: &[f32],
|
||||
sketch_version: u16,
|
||||
) -> Result<Self, SketchError> {
|
||||
pub fn try_from_embedding(embedding: &[f32], sketch_version: u16) -> Result<Self, SketchError> {
|
||||
if embedding.len() > u16::MAX as usize {
|
||||
return Err(SketchError::EmbeddingDimOverflow {
|
||||
got: embedding.len(),
|
||||
@@ -376,7 +373,7 @@ impl WireSketch {
|
||||
let embedding_dim = u16::from_le_bytes(buf[8..10].try_into().expect("2-byte slice"));
|
||||
let nov_q15 = u16::from_le_bytes(buf[10..12].try_into().expect("2-byte slice"));
|
||||
|
||||
let expected_bits = ((embedding_dim as usize) + 7) / 8;
|
||||
let expected_bits = (embedding_dim as usize).div_ceil(8);
|
||||
let got_bits = buf.len() - Self::HEADER_BYTES;
|
||||
if expected_bits != got_bits {
|
||||
return Err(WireSketchError::PayloadSizeMismatch {
|
||||
@@ -566,10 +563,8 @@ impl SketchBank {
|
||||
}
|
||||
// Drain heap into a Vec — already in (Reverse) descending order;
|
||||
// sort to expose ascending-by-distance per the public contract.
|
||||
let mut scored: Vec<(u32, u32)> = heap
|
||||
.into_iter()
|
||||
.map(|Reverse((d, id))| (id, d))
|
||||
.collect();
|
||||
let mut scored: Vec<(u32, u32)> =
|
||||
heap.into_iter().map(|Reverse((d, id))| (id, d)).collect();
|
||||
scored.sort_by_key(|&(_, d)| d);
|
||||
Ok(scored)
|
||||
}
|
||||
@@ -638,11 +633,14 @@ mod tests {
|
||||
fn bank_topk_returns_sorted_by_distance() {
|
||||
let mut bank = SketchBank::new();
|
||||
// id 10: identical
|
||||
bank.insert(10, Sketch::from_embedding(&[0.5, 0.5, 0.5, 0.5], 1)).unwrap();
|
||||
bank.insert(10, Sketch::from_embedding(&[0.5, 0.5, 0.5, 0.5], 1))
|
||||
.unwrap();
|
||||
// id 20: 1 bit different (last dim flipped)
|
||||
bank.insert(20, Sketch::from_embedding(&[0.5, 0.5, 0.5, -0.5], 1)).unwrap();
|
||||
bank.insert(20, Sketch::from_embedding(&[0.5, 0.5, 0.5, -0.5], 1))
|
||||
.unwrap();
|
||||
// id 30: 2 bits different
|
||||
bank.insert(30, Sketch::from_embedding(&[-0.5, 0.5, -0.5, 0.5], 1)).unwrap();
|
||||
bank.insert(30, Sketch::from_embedding(&[-0.5, 0.5, -0.5, 0.5], 1))
|
||||
.unwrap();
|
||||
|
||||
let query = Sketch::from_embedding(&[0.5, 0.5, 0.5, 0.5], 1);
|
||||
let topk = bank.topk(&query, 3).unwrap();
|
||||
@@ -658,7 +656,8 @@ mod tests {
|
||||
#[test]
|
||||
fn bank_topk_zero_returns_empty() {
|
||||
let mut bank = SketchBank::new();
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5, 0.5], 1)).unwrap();
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5, 0.5], 1))
|
||||
.unwrap();
|
||||
let q = Sketch::from_embedding(&[0.5, 0.5], 1);
|
||||
assert_eq!(bank.topk(&q, 0).unwrap().len(), 0);
|
||||
}
|
||||
@@ -666,8 +665,10 @@ mod tests {
|
||||
#[test]
|
||||
fn bank_topk_more_than_size_returns_all() {
|
||||
let mut bank = SketchBank::new();
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5, 0.5], 1)).unwrap();
|
||||
bank.insert(2, Sketch::from_embedding(&[-0.5, 0.5], 1)).unwrap();
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5, 0.5], 1))
|
||||
.unwrap();
|
||||
bank.insert(2, Sketch::from_embedding(&[-0.5, 0.5], 1))
|
||||
.unwrap();
|
||||
let q = Sketch::from_embedding(&[0.5, 0.5], 1);
|
||||
assert_eq!(bank.topk(&q, 100).unwrap().len(), 2);
|
||||
}
|
||||
@@ -675,7 +676,8 @@ mod tests {
|
||||
#[test]
|
||||
fn bank_locks_schema_on_first_insert() {
|
||||
let mut bank = SketchBank::new();
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5, 0.5, 0.5, 0.5], 1)).unwrap();
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5, 0.5, 0.5, 0.5], 1))
|
||||
.unwrap();
|
||||
// Different version → reject
|
||||
let err = bank
|
||||
.insert(2, Sketch::from_embedding(&[0.5, 0.5, 0.5, 0.5], 2))
|
||||
@@ -712,7 +714,8 @@ mod tests {
|
||||
fn novelty_is_proportional_to_min_distance() {
|
||||
let mut bank = SketchBank::new();
|
||||
// Bank has one sketch with all 8 dims positive.
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5; 8], 1)).unwrap();
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5; 8], 1))
|
||||
.unwrap();
|
||||
// Query flips half the dims → 4 bit difference / 8 dims = 0.5.
|
||||
let query = Sketch::from_embedding(&[0.5, 0.5, 0.5, 0.5, -0.5, -0.5, -0.5, -0.5], 1);
|
||||
let novelty = bank.novelty(&query).unwrap();
|
||||
@@ -796,7 +799,10 @@ mod tests {
|
||||
// Bump format_version to 99 — beyond what this build supports.
|
||||
bytes[4..6].copy_from_slice(&99_u16.to_le_bytes());
|
||||
let err = WireSketch::deserialize(&bytes).unwrap_err();
|
||||
assert!(matches!(err, WireSketchError::UnsupportedVersion { got: 99, .. }));
|
||||
assert!(matches!(
|
||||
err,
|
||||
WireSketchError::UnsupportedVersion { got: 99, .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -823,13 +829,18 @@ mod tests {
|
||||
let v: Vec<f32> = (0..128).map(|i| (i as f32).sin()).collect();
|
||||
let sketch = Sketch::from_embedding(&v, 1);
|
||||
let bytes = WireSketch::serialize(&sketch, 0.5);
|
||||
assert_eq!(bytes.len(), 28, "AETHER 128-d must wire to exactly 28 bytes");
|
||||
assert_eq!(
|
||||
bytes.len(),
|
||||
28,
|
||||
"AETHER 128-d must wire to exactly 28 bytes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topk_rejects_query_with_wrong_schema() {
|
||||
let mut bank = SketchBank::with_schema(4, 1);
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5, 0.5, 0.5, 0.5], 1)).unwrap();
|
||||
bank.insert(1, Sketch::from_embedding(&[0.5, 0.5, 0.5, 0.5], 1))
|
||||
.unwrap();
|
||||
let bad_dim = Sketch::from_embedding(&[0.5, 0.5], 1);
|
||||
assert!(matches!(
|
||||
bank.topk(&bad_dim, 1).unwrap_err(),
|
||||
|
||||
@@ -61,16 +61,26 @@ impl std::fmt::Display for AttentionError {
|
||||
match self {
|
||||
AttentionError::EmptyViewpoints => write!(f, "no viewpoint embeddings provided"),
|
||||
AttentionError::DimensionMismatch { expected, actual } => {
|
||||
write!(f, "embedding dimension mismatch: expected {expected}, got {actual}")
|
||||
write!(
|
||||
f,
|
||||
"embedding dimension mismatch: expected {expected}, got {actual}"
|
||||
)
|
||||
}
|
||||
AttentionError::BiasDimensionMismatch { n_viewpoints, bias_rows, bias_cols } => {
|
||||
AttentionError::BiasDimensionMismatch {
|
||||
n_viewpoints,
|
||||
bias_rows,
|
||||
bias_cols,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"geometric bias matrix is {bias_rows}x{bias_cols} but {n_viewpoints} viewpoints require {n_viewpoints}x{n_viewpoints}"
|
||||
)
|
||||
}
|
||||
AttentionError::WeightDimensionMismatch { expected, actual } => {
|
||||
write!(f, "weight matrix dimension mismatch: expected {expected}, got {actual}")
|
||||
write!(
|
||||
f,
|
||||
"weight matrix dimension mismatch: expected {expected}, got {actual}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,7 +136,11 @@ pub struct ViewpointGeometry {
|
||||
impl GeometricBias {
|
||||
/// Create a new geometric bias with the given parameters.
|
||||
pub fn new(w_angle: f32, w_dist: f32, d_ref: f32) -> Self {
|
||||
GeometricBias { w_angle, w_dist, d_ref }
|
||||
GeometricBias {
|
||||
w_angle,
|
||||
w_dist,
|
||||
d_ref,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the bias value for a single viewpoint pair.
|
||||
@@ -241,7 +255,13 @@ impl ProjectionWeights {
|
||||
actual: w_v.len(),
|
||||
});
|
||||
}
|
||||
Ok(ProjectionWeights { w_q, w_k, w_v, d_in, d_out })
|
||||
Ok(ProjectionWeights {
|
||||
w_q,
|
||||
w_k,
|
||||
w_v,
|
||||
d_in,
|
||||
d_out,
|
||||
})
|
||||
}
|
||||
|
||||
/// Project a single embedding vector through a weight matrix.
|
||||
@@ -262,17 +282,26 @@ impl ProjectionWeights {
|
||||
|
||||
/// Project all viewpoint embeddings through W_q.
|
||||
pub fn project_queries(&self, embeddings: &[Vec<f32>]) -> Vec<Vec<f32>> {
|
||||
embeddings.iter().map(|e| self.project(&self.w_q, e)).collect()
|
||||
embeddings
|
||||
.iter()
|
||||
.map(|e| self.project(&self.w_q, e))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Project all viewpoint embeddings through W_k.
|
||||
pub fn project_keys(&self, embeddings: &[Vec<f32>]) -> Vec<Vec<f32>> {
|
||||
embeddings.iter().map(|e| self.project(&self.w_k, e)).collect()
|
||||
embeddings
|
||||
.iter()
|
||||
.map(|e| self.project(&self.w_k, e))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Project all viewpoint embeddings through W_v.
|
||||
pub fn project_values(&self, embeddings: &[Vec<f32>]) -> Vec<Vec<f32>> {
|
||||
embeddings.iter().map(|e| self.project(&self.w_v, e)).collect()
|
||||
embeddings
|
||||
.iter()
|
||||
.map(|e| self.project(&self.w_v, e))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,8 +422,8 @@ impl CrossViewpointAttention {
|
||||
let mut output = vec![0.0_f32; d];
|
||||
for j in 0..n {
|
||||
let w = attention_weights[i * n + j];
|
||||
for k in 0..d {
|
||||
output[k] += w * values[j][k];
|
||||
for (out_k, &val_k) in output.iter_mut().zip(values[j].iter()) {
|
||||
*out_k += w * val_k;
|
||||
}
|
||||
}
|
||||
attended.push(output);
|
||||
@@ -427,13 +456,13 @@ impl CrossViewpointAttention {
|
||||
let mut fused = vec![0.0_f32; d];
|
||||
|
||||
for row in &attended {
|
||||
for k in 0..d {
|
||||
fused[k] += row[k];
|
||||
for (fk, &rk) in fused.iter_mut().zip(row.iter()) {
|
||||
*fk += rk;
|
||||
}
|
||||
}
|
||||
let n_f = n as f32;
|
||||
for k in 0..d {
|
||||
fused[k] /= n_f;
|
||||
for fk in fused.iter_mut() {
|
||||
*fk /= n_f;
|
||||
}
|
||||
|
||||
Ok(fused)
|
||||
@@ -511,7 +540,9 @@ mod tests {
|
||||
fn make_test_embeddings(n: usize, dim: usize) -> Vec<Vec<f32>> {
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
(0..dim).map(|d| ((i * dim + d) as f32 * 0.01).sin()).collect()
|
||||
(0..dim)
|
||||
.map(|d| ((i * dim + d) as f32 * 0.01).sin())
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -593,12 +624,18 @@ mod tests {
|
||||
let bias = GeometricBias::new(1.0, 1.0, 5.0);
|
||||
// Same position: theta=0, d=0 -> cos(0) + exp(0) = 2.0
|
||||
let val = bias.compute_pair(0.0, 0.0);
|
||||
assert!((val - 2.0).abs() < 1e-5, "self-bias should be 2.0, got {val}");
|
||||
assert!(
|
||||
(val - 2.0).abs() < 1e-5,
|
||||
"self-bias should be 2.0, got {val}"
|
||||
);
|
||||
|
||||
// Orthogonal, far apart: theta=PI/2, d=5.0
|
||||
let val_orth = bias.compute_pair(std::f32::consts::FRAC_PI_2, 5.0);
|
||||
// cos(PI/2) ~ 0 + exp(-1) ~ 0.368
|
||||
assert!(val_orth < 1.0, "orthogonal far-apart viewpoints should have low bias");
|
||||
assert!(
|
||||
val_orth < 1.0,
|
||||
"orthogonal far-apart viewpoints should have low bias"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -641,8 +678,8 @@ mod tests {
|
||||
let dim = 4;
|
||||
// Swap first two dimensions in Q.
|
||||
let mut w_q = vec![0.0_f32; dim * dim];
|
||||
w_q[0 * dim + 1] = 1.0; // row 0 picks dim 1
|
||||
w_q[1 * dim + 0] = 1.0; // row 1 picks dim 0
|
||||
w_q[1] = 1.0; // row 0 picks dim 1 (0 * dim + 1)
|
||||
w_q[dim] = 1.0; // row 1 picks dim 0 (1 * dim + 0)
|
||||
w_q[2 * dim + 2] = 1.0;
|
||||
w_q[3 * dim + 3] = 1.0;
|
||||
let w_id = {
|
||||
@@ -662,6 +699,9 @@ mod tests {
|
||||
let bias = GeometricBias::new(0.0, 1.0, 2.0); // only distance component
|
||||
let close = bias.compute_pair(0.0, 0.5);
|
||||
let far = bias.compute_pair(0.0, 10.0);
|
||||
assert!(close > far, "closer viewpoints should have higher distance bias");
|
||||
assert!(
|
||||
close > far,
|
||||
"closer viewpoints should have higher distance bias"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,11 +229,9 @@ pub fn coherence_gate(phase_diffs: &[f32], threshold: f32) -> bool {
|
||||
if phase_diffs.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let (sum_cos, sum_sin) = phase_diffs
|
||||
.iter()
|
||||
.fold((0.0_f32, 0.0_f32), |(c, s), &dp| {
|
||||
(c + dp.cos(), s + dp.sin())
|
||||
});
|
||||
let (sum_cos, sum_sin) = phase_diffs.iter().fold((0.0_f32, 0.0_f32), |(c, s), &dp| {
|
||||
(c + dp.cos(), s + dp.sin())
|
||||
});
|
||||
let n = phase_diffs.len() as f32;
|
||||
let coherence = ((sum_cos / n).powi(2) + (sum_sin / n).powi(2)).sqrt();
|
||||
coherence > threshold
|
||||
@@ -246,11 +244,9 @@ pub fn compute_coherence(phase_diffs: &[f32]) -> f32 {
|
||||
if phase_diffs.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let (sum_cos, sum_sin) = phase_diffs
|
||||
.iter()
|
||||
.fold((0.0_f32, 0.0_f32), |(c, s), &dp| {
|
||||
(c + dp.cos(), s + dp.sin())
|
||||
});
|
||||
let (sum_cos, sum_sin) = phase_diffs.iter().fold((0.0_f32, 0.0_f32), |(c, s), &dp| {
|
||||
(c + dp.cos(), s + dp.sin())
|
||||
});
|
||||
let n = phase_diffs.len() as f32;
|
||||
((sum_cos / n).powi(2) + (sum_sin / n).powi(2)).sqrt()
|
||||
}
|
||||
@@ -268,7 +264,10 @@ mod tests {
|
||||
// All phase diffs are the same -> coherence ~ 1.0
|
||||
let phase_diffs = vec![0.5_f32; 100];
|
||||
let c = compute_coherence(&phase_diffs);
|
||||
assert!(c > 0.99, "identical phases should give coherence ~ 1.0, got {c}");
|
||||
assert!(
|
||||
c > 0.99,
|
||||
"identical phases should give coherence ~ 1.0, got {c}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -279,7 +278,10 @@ mod tests {
|
||||
.map(|i| 2.0 * std::f32::consts::PI * i as f32 / n as f32)
|
||||
.collect();
|
||||
let c = compute_coherence(&phase_diffs);
|
||||
assert!(c < 0.05, "uniformly spread phases should give coherence ~ 0.0, got {c}");
|
||||
assert!(
|
||||
c < 0.05,
|
||||
"uniformly spread phases should give coherence ~ 0.0, got {c}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -336,11 +338,17 @@ mod tests {
|
||||
|
||||
// Coherence drops to 0.65 (below threshold but within hysteresis band).
|
||||
assert!(gate.evaluate(0.65));
|
||||
assert!(gate.is_open(), "gate should stay open within hysteresis band");
|
||||
assert!(
|
||||
gate.is_open(),
|
||||
"gate should stay open within hysteresis band"
|
||||
);
|
||||
|
||||
// Coherence drops below hysteresis boundary (0.7 - 0.1 = 0.6).
|
||||
assert!(!gate.evaluate(0.55));
|
||||
assert!(!gate.is_open(), "gate should close below hysteresis boundary");
|
||||
assert!(
|
||||
!gate.is_open(),
|
||||
"gate should close below hysteresis boundary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -25,6 +25,9 @@ use crate::viewpoint::geometry::{GeometricDiversityIndex, NodeId};
|
||||
/// Unique identifier for a multistatic array deployment.
|
||||
pub type ArrayId = u64;
|
||||
|
||||
/// Extracted viewpoint data used during fusion: (node id, embedding, azimuth, position).
|
||||
type ExtractedViewpoint = (NodeId, Vec<f32>, f32, (f32, f32));
|
||||
|
||||
/// Per-viewpoint embedding with geometric metadata.
|
||||
///
|
||||
/// Represents a single CSI observation processed through the per-viewpoint
|
||||
@@ -139,14 +142,21 @@ impl std::fmt::Display for FusionError {
|
||||
FusionError::AllFiltered { rejected } => {
|
||||
write!(f, "all {rejected} viewpoints filtered by SNR threshold")
|
||||
}
|
||||
FusionError::CoherenceGateClosed { coherence, threshold } => {
|
||||
FusionError::CoherenceGateClosed {
|
||||
coherence,
|
||||
threshold,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"coherence gate closed: coherence={coherence:.3} < threshold={threshold:.3}"
|
||||
)
|
||||
}
|
||||
FusionError::AttentionError(e) => write!(f, "attention error: {e}"),
|
||||
FusionError::DimensionMismatch { expected, actual, node_id } => {
|
||||
FusionError::DimensionMismatch {
|
||||
expected,
|
||||
actual,
|
||||
node_id,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"node {node_id} embedding dim {actual} != expected {expected}"
|
||||
@@ -351,7 +361,7 @@ impl MultistaticArray {
|
||||
// Extract all needed data from viewpoints upfront to avoid borrow conflicts.
|
||||
let min_snr = self.config.min_snr_db;
|
||||
let total_viewpoints = self.viewpoints.len();
|
||||
let extracted: Vec<(NodeId, Vec<f32>, f32, (f32, f32))> = self
|
||||
let extracted: Vec<ExtractedViewpoint> = self
|
||||
.viewpoints
|
||||
.iter()
|
||||
.filter(|v| v.snr_db >= min_snr)
|
||||
@@ -429,7 +439,7 @@ impl MultistaticArray {
|
||||
pub fn fuse_ungated(&mut self) -> Result<FusedEmbedding, FusionError> {
|
||||
let min_snr = self.config.min_snr_db;
|
||||
let total_viewpoints = self.viewpoints.len();
|
||||
let extracted: Vec<(NodeId, Vec<f32>, f32, (f32, f32))> = self
|
||||
let extracted: Vec<ExtractedViewpoint> = self
|
||||
.viewpoints
|
||||
.iter()
|
||||
.filter(|v| v.snr_db >= min_snr)
|
||||
@@ -514,12 +524,19 @@ impl MultistaticArray {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_viewpoint(node_id: NodeId, angle_idx: usize, n: usize, dim: usize) -> ViewpointEmbedding {
|
||||
fn make_viewpoint(
|
||||
node_id: NodeId,
|
||||
angle_idx: usize,
|
||||
n: usize,
|
||||
dim: usize,
|
||||
) -> ViewpointEmbedding {
|
||||
let angle = 2.0 * std::f32::consts::PI * angle_idx as f32 / n as f32;
|
||||
let r = 3.0;
|
||||
ViewpointEmbedding {
|
||||
node_id,
|
||||
embedding: (0..dim).map(|d| ((node_id as usize * dim + d) as f32 * 0.01).sin()).collect(),
|
||||
embedding: (0..dim)
|
||||
.map(|d| ((node_id as usize * dim + d) as f32 * 0.01).sin())
|
||||
.collect(),
|
||||
azimuth: angle,
|
||||
elevation: 0.0,
|
||||
baseline: r,
|
||||
@@ -549,7 +566,9 @@ mod tests {
|
||||
let dim = 16;
|
||||
let mut array = setup_coherent_array(dim);
|
||||
for i in 0..4 {
|
||||
array.submit_viewpoint(make_viewpoint(i, i as usize, 4, dim)).unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(i, i as usize, 4, dim))
|
||||
.unwrap();
|
||||
}
|
||||
let fused = array.fuse().unwrap();
|
||||
assert_eq!(fused.embedding.len(), dim);
|
||||
@@ -577,10 +596,17 @@ mod tests {
|
||||
for i in 0..100 {
|
||||
array.push_phase_diff(i as f32 * 0.5);
|
||||
}
|
||||
array.submit_viewpoint(make_viewpoint(0, 0, 4, dim)).unwrap();
|
||||
array.submit_viewpoint(make_viewpoint(1, 1, 4, dim)).unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(0, 0, 4, dim))
|
||||
.unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(1, 1, 4, dim))
|
||||
.unwrap();
|
||||
let result = array.fuse();
|
||||
assert!(matches!(result, Err(FusionError::CoherenceGateClosed { .. })));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(FusionError::CoherenceGateClosed { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -598,8 +624,12 @@ mod tests {
|
||||
for i in 0..100 {
|
||||
array.push_phase_diff(i as f32 * 0.5);
|
||||
}
|
||||
array.submit_viewpoint(make_viewpoint(0, 0, 4, dim)).unwrap();
|
||||
array.submit_viewpoint(make_viewpoint(1, 1, 4, dim)).unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(0, 0, 4, dim))
|
||||
.unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(1, 1, 4, dim))
|
||||
.unwrap();
|
||||
let fused = array.fuse_ungated().unwrap();
|
||||
assert_eq!(fused.embedding.len(), dim);
|
||||
}
|
||||
@@ -652,8 +682,12 @@ mod tests {
|
||||
fn events_are_emitted_on_fusion() {
|
||||
let dim = 8;
|
||||
let mut array = setup_coherent_array(dim);
|
||||
array.submit_viewpoint(make_viewpoint(0, 0, 4, dim)).unwrap();
|
||||
array.submit_viewpoint(make_viewpoint(1, 1, 4, dim)).unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(0, 0, 4, dim))
|
||||
.unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(1, 1, 4, dim))
|
||||
.unwrap();
|
||||
array.clear_events();
|
||||
let _ = array.fuse();
|
||||
assert!(!array.events().is_empty(), "fusion should emit events");
|
||||
@@ -663,8 +697,12 @@ mod tests {
|
||||
fn remove_viewpoint_works() {
|
||||
let dim = 8;
|
||||
let mut array = setup_coherent_array(dim);
|
||||
array.submit_viewpoint(make_viewpoint(10, 0, 4, dim)).unwrap();
|
||||
array.submit_viewpoint(make_viewpoint(20, 1, 4, dim)).unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(10, 0, 4, dim))
|
||||
.unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(20, 1, 4, dim))
|
||||
.unwrap();
|
||||
assert_eq!(array.n_viewpoints(), 2);
|
||||
array.remove_viewpoint(10);
|
||||
assert_eq!(array.n_viewpoints(), 1);
|
||||
@@ -675,11 +713,19 @@ mod tests {
|
||||
let dim = 16;
|
||||
let mut array = setup_coherent_array(dim);
|
||||
for i in 0..4 {
|
||||
array.submit_viewpoint(make_viewpoint(i, i as usize, 4, dim)).unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(i, i as usize, 4, dim))
|
||||
.unwrap();
|
||||
}
|
||||
let fused = array.fuse().unwrap();
|
||||
assert!(fused.gdi > 0.0, "GDI should be positive for spread viewpoints");
|
||||
assert!(fused.n_effective > 1.0, "effective viewpoints should be > 1");
|
||||
assert!(
|
||||
fused.gdi > 0.0,
|
||||
"GDI should be positive for spread viewpoints"
|
||||
);
|
||||
assert!(
|
||||
fused.n_effective > 1.0,
|
||||
"effective viewpoints should be > 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -687,7 +733,9 @@ mod tests {
|
||||
let dim = 8;
|
||||
let mut array = setup_coherent_array(dim);
|
||||
for i in 0..6 {
|
||||
array.submit_viewpoint(make_viewpoint(i, i as usize, 6, dim)).unwrap();
|
||||
array
|
||||
.submit_viewpoint(make_viewpoint(i, i as usize, 6, dim))
|
||||
.unwrap();
|
||||
}
|
||||
let gdi = array.compute_gdi().unwrap();
|
||||
assert!(gdi.value > 0.0);
|
||||
|
||||
@@ -363,7 +363,12 @@ mod tests {
|
||||
#[test]
|
||||
fn gdi_uniform_spacing_is_optimal() {
|
||||
// 4 viewpoints at 0, 90, 180, 270 degrees
|
||||
let azimuths = vec![0.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI, 3.0 * std::f32::consts::FRAC_PI_2];
|
||||
let azimuths = vec![
|
||||
0.0,
|
||||
std::f32::consts::FRAC_PI_2,
|
||||
std::f32::consts::PI,
|
||||
3.0 * std::f32::consts::FRAC_PI_2,
|
||||
];
|
||||
let ids = vec![0, 1, 2, 3];
|
||||
let gdi = GeometricDiversityIndex::compute(&azimuths, &ids).unwrap();
|
||||
// Minimum separation = PI/2 for each viewpoint, so GDI = PI/2
|
||||
@@ -399,13 +404,21 @@ mod tests {
|
||||
let azimuths = vec![0.0, 1.0, 2.0, 3.0];
|
||||
let ids = vec![0, 1, 2, 3];
|
||||
let gdi = GeometricDiversityIndex::compute(&azimuths, &ids).unwrap();
|
||||
assert!(gdi.efficiency() > 0.0 && gdi.efficiency() <= 1.0,
|
||||
"efficiency should be in (0, 1], got {}", gdi.efficiency());
|
||||
assert!(
|
||||
gdi.efficiency() > 0.0 && gdi.efficiency() <= 1.0,
|
||||
"efficiency should be in (0, 1], got {}",
|
||||
gdi.efficiency()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gdi_is_sufficient_for_uniform_layout() {
|
||||
let azimuths = vec![0.0, std::f32::consts::FRAC_PI_2, std::f32::consts::PI, 3.0 * std::f32::consts::FRAC_PI_2];
|
||||
let azimuths = vec![
|
||||
0.0,
|
||||
std::f32::consts::FRAC_PI_2,
|
||||
std::f32::consts::PI,
|
||||
3.0 * std::f32::consts::FRAC_PI_2,
|
||||
];
|
||||
let ids = vec![0, 1, 2, 3];
|
||||
let gdi = GeometricDiversityIndex::compute(&azimuths, &ids).unwrap();
|
||||
assert!(gdi.is_sufficient(), "uniform layout should be sufficient");
|
||||
@@ -451,13 +464,21 @@ mod tests {
|
||||
let vp3: Vec<ViewpointPosition> = (0..3)
|
||||
.map(|i| {
|
||||
let a = 2.0 * std::f32::consts::PI * i as f32 / 3.0;
|
||||
ViewpointPosition { x: 5.0 * a.cos(), y: 5.0 * a.sin(), noise_std: 0.1 }
|
||||
ViewpointPosition {
|
||||
x: 5.0 * a.cos(),
|
||||
y: 5.0 * a.sin(),
|
||||
noise_std: 0.1,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let vp6: Vec<ViewpointPosition> = (0..6)
|
||||
.map(|i| {
|
||||
let a = 2.0 * std::f32::consts::PI * i as f32 / 6.0;
|
||||
ViewpointPosition { x: 5.0 * a.cos(), y: 5.0 * a.sin(), noise_std: 0.1 }
|
||||
ViewpointPosition {
|
||||
x: 5.0 * a.cos(),
|
||||
y: 5.0 * a.sin(),
|
||||
noise_std: 0.1,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -475,8 +496,16 @@ mod tests {
|
||||
fn crb_too_few_viewpoints_returns_none() {
|
||||
let target = (0.0, 0.0);
|
||||
let vps = vec![
|
||||
ViewpointPosition { x: 1.0, y: 0.0, noise_std: 0.1 },
|
||||
ViewpointPosition { x: 0.0, y: 1.0, noise_std: 0.1 },
|
||||
ViewpointPosition {
|
||||
x: 1.0,
|
||||
y: 0.0,
|
||||
noise_std: 0.1,
|
||||
},
|
||||
ViewpointPosition {
|
||||
x: 0.0,
|
||||
y: 1.0,
|
||||
noise_std: 0.1,
|
||||
},
|
||||
];
|
||||
assert!(CramerRaoBound::estimate(target, &vps).is_none());
|
||||
}
|
||||
@@ -487,13 +516,20 @@ mod tests {
|
||||
let vps: Vec<ViewpointPosition> = (0..4)
|
||||
.map(|i| {
|
||||
let a = 2.0 * std::f32::consts::PI * i as f32 / 4.0;
|
||||
ViewpointPosition { x: 3.0 * a.cos(), y: 3.0 * a.sin(), noise_std: 0.1 }
|
||||
ViewpointPosition {
|
||||
x: 3.0 * a.cos(),
|
||||
y: 3.0 * a.sin(),
|
||||
noise_std: 0.1,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let crb = CramerRaoBound::estimate_regularised(target, &vps, 1e-4);
|
||||
// May return None if Neumann solver doesn't converge, but should not panic.
|
||||
if let Some(crb) = crb {
|
||||
assert!(crb.rmse_lower_bound >= 0.0, "RMSE bound must be non-negative");
|
||||
assert!(
|
||||
crb.rmse_lower_bound >= 0.0,
|
||||
"RMSE bound must be non-negative"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user