fix: harden MERIDIAN modules from code review + security audit
- domain.rs: atomic instance counter for unique Linear weight seeds (C3) - rapid_adapt.rs: adapt() returns Result instead of panicking (C5), bounded calibration buffer with max_buffer_frames cap (F1-HIGH), validate lora_rank >= 1 (F10) - geometry.rs: 24-bit PRNG precision matching f32 mantissa (C2) - virtual_aug.rs: guard against room_scale=0 division-by-zero (F6) - signal/lib.rs: re-export AmplitudeStats from hardware_norm (W1) - train/lib.rs: crate-root re-exports for all MERIDIAN types (W2) All 201 tests pass (96 unit + 24 integration + 18 subcarrier + 10 metrics + 7 doctests + 105 signal + 10 validation + 1 signal doctest). Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -56,7 +56,7 @@ pub use motion::{
|
||||
HumanDetectionResult, MotionAnalysis, MotionDetector, MotionDetectorConfig, MotionScore,
|
||||
};
|
||||
pub use hardware_norm::{
|
||||
CanonicalCsiFrame, HardwareNormError, HardwareNormalizer, HardwareType,
|
||||
AmplitudeStats, CanonicalCsiFrame, HardwareNormError, HardwareNormalizer, HardwareType,
|
||||
};
|
||||
pub use phase_sanitizer::{
|
||||
PhaseSanitizationError, PhaseSanitizer, PhaseSanitizerConfig, UnwrappingMethod,
|
||||
|
||||
@@ -61,14 +61,22 @@ pub struct Linear {
|
||||
pub out_features: usize,
|
||||
}
|
||||
|
||||
/// Global instance counter to ensure distinct seeds for layers with same dimensions.
|
||||
static INSTANCE_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
impl Linear {
|
||||
/// New layer with deterministic Kaiming-uniform weights.
|
||||
///
|
||||
/// Each call produces unique weights even for identical `(in_features, out_features)`
|
||||
/// because an atomic instance counter is mixed into the seed.
|
||||
pub fn new(in_features: usize, out_features: usize) -> Self {
|
||||
let instance = INSTANCE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let bound = (1.0 / in_features as f64).sqrt() as f32;
|
||||
let n = out_features * in_features;
|
||||
let mut seed: u64 = (in_features as u64)
|
||||
.wrapping_mul(6364136223846793005)
|
||||
.wrapping_add(out_features as u64);
|
||||
.wrapping_add(out_features as u64)
|
||||
.wrapping_add(instance.wrapping_mul(2654435761));
|
||||
let mut next = || -> f32 {
|
||||
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
((seed >> 33) as f32) / (u32::MAX as f32 / 2.0) - 1.0
|
||||
|
||||
@@ -50,6 +50,7 @@ impl Linear {
|
||||
}
|
||||
|
||||
/// Deterministic xorshift64 uniform in `[lo, hi)`.
|
||||
/// Uses 24-bit precision (matching f32 mantissa) for uniform distribution.
|
||||
fn det_uniform(n: usize, lo: f32, hi: f32, seed: u64) -> Vec<f32> {
|
||||
let r = hi - lo;
|
||||
let mut s = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
@@ -58,7 +59,7 @@ fn det_uniform(n: usize, lo: f32, hi: f32, seed: u64) -> Vec<f32> {
|
||||
s ^= s << 13;
|
||||
s ^= s >> 7;
|
||||
s ^= s << 17;
|
||||
lo + (s >> 11) as f32 / (1u64 << 53) as f32 * r
|
||||
lo + (s >> 40) as f32 / (1u64 << 24) as f32 * r
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -77,5 +77,14 @@ pub use error::{ConfigError, DatasetError, SubcarrierError, TrainError};
|
||||
pub use error::TrainResult as TrainResultAlias;
|
||||
pub use subcarrier::{compute_interp_weights, interpolate_subcarriers, select_subcarriers_by_variance};
|
||||
|
||||
// MERIDIAN (ADR-027) re-exports.
|
||||
pub use domain::{
|
||||
AdversarialSchedule, DomainClassifier, DomainFactorizer, GradientReversalLayer,
|
||||
};
|
||||
pub use eval::CrossDomainEvaluator;
|
||||
pub use geometry::{FilmLayer, FourierPositionalEncoding, GeometryEncoder, MeridianGeometryConfig};
|
||||
pub use rapid_adapt::{AdaptError, AdaptationLoss, AdaptationResult, RapidAdaptation};
|
||||
pub use virtual_aug::VirtualDomainAugmentor;
|
||||
|
||||
/// Crate version string.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -49,10 +49,37 @@ pub struct AdaptationResult {
|
||||
pub adaptation_epochs: usize,
|
||||
}
|
||||
|
||||
/// Error type for rapid adaptation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AdaptError {
|
||||
/// Not enough calibration frames.
|
||||
InsufficientFrames {
|
||||
/// Frames currently buffered.
|
||||
have: usize,
|
||||
/// Minimum required.
|
||||
need: usize,
|
||||
},
|
||||
/// LoRA rank must be at least 1.
|
||||
InvalidRank,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AdaptError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InsufficientFrames { have, need } =>
|
||||
write!(f, "insufficient calibration frames: have {have}, need at least {need}"),
|
||||
Self::InvalidRank => write!(f, "lora_rank must be >= 1"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AdaptError {}
|
||||
|
||||
/// Few-shot rapid adaptation engine.
|
||||
///
|
||||
/// Accumulates unlabeled CSI calibration frames and runs test-time training
|
||||
/// to produce LoRA weight deltas.
|
||||
/// to produce LoRA weight deltas. Buffer is capped at `max_buffer_frames`
|
||||
/// (default 10 000) to prevent unbounded memory growth.
|
||||
///
|
||||
/// ```rust
|
||||
/// use wifi_densepose_train::rapid_adapt::{RapidAdaptation, AdaptationLoss};
|
||||
@@ -60,26 +87,36 @@ pub struct AdaptationResult {
|
||||
/// let mut ra = RapidAdaptation::new(10, 4, loss);
|
||||
/// for i in 0..10 { ra.push_frame(&vec![i as f32; 8]); }
|
||||
/// assert!(ra.is_ready());
|
||||
/// let r = ra.adapt();
|
||||
/// let r = ra.adapt().unwrap();
|
||||
/// assert_eq!(r.frames_used, 10);
|
||||
/// ```
|
||||
pub struct RapidAdaptation {
|
||||
/// Minimum frames before adaptation (default 200 = 10 s @ 20 Hz).
|
||||
pub min_calibration_frames: usize,
|
||||
/// LoRA factorization rank (default 4).
|
||||
/// LoRA factorization rank (must be >= 1).
|
||||
pub lora_rank: usize,
|
||||
/// Loss variant for test-time training.
|
||||
pub adaptation_loss: AdaptationLoss,
|
||||
/// Maximum buffer size (ring-buffer eviction beyond this cap).
|
||||
pub max_buffer_frames: usize,
|
||||
calibration_buffer: Vec<Vec<f32>>,
|
||||
}
|
||||
|
||||
/// Default maximum calibration buffer size.
|
||||
const DEFAULT_MAX_BUFFER: usize = 10_000;
|
||||
|
||||
impl RapidAdaptation {
|
||||
/// Create a new adaptation engine.
|
||||
pub fn new(min_calibration_frames: usize, lora_rank: usize, adaptation_loss: AdaptationLoss) -> Self {
|
||||
Self { min_calibration_frames, lora_rank, adaptation_loss, calibration_buffer: Vec::new() }
|
||||
Self { min_calibration_frames, lora_rank, adaptation_loss, max_buffer_frames: DEFAULT_MAX_BUFFER, calibration_buffer: Vec::new() }
|
||||
}
|
||||
/// Push a single unlabeled CSI frame. Evicts oldest frame when buffer is full.
|
||||
pub fn push_frame(&mut self, frame: &[f32]) {
|
||||
if self.calibration_buffer.len() >= self.max_buffer_frames {
|
||||
self.calibration_buffer.remove(0);
|
||||
}
|
||||
self.calibration_buffer.push(frame.to_vec());
|
||||
}
|
||||
/// Push a single unlabeled CSI frame.
|
||||
pub fn push_frame(&mut self, frame: &[f32]) { self.calibration_buffer.push(frame.to_vec()); }
|
||||
/// True when buffer >= min_calibration_frames.
|
||||
pub fn is_ready(&self) -> bool { self.calibration_buffer.len() >= self.min_calibration_frames }
|
||||
/// Number of buffered frames.
|
||||
@@ -87,10 +124,14 @@ impl RapidAdaptation {
|
||||
|
||||
/// Run test-time adaptation producing LoRA weight deltas.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the calibration buffer is empty.
|
||||
pub fn adapt(&self) -> AdaptationResult {
|
||||
assert!(!self.calibration_buffer.is_empty(), "empty calibration buffer");
|
||||
/// Returns an error if the calibration buffer is empty or lora_rank is 0.
|
||||
pub fn adapt(&self) -> Result<AdaptationResult, AdaptError> {
|
||||
if self.calibration_buffer.is_empty() {
|
||||
return Err(AdaptError::InsufficientFrames { have: 0, need: 1 });
|
||||
}
|
||||
if self.lora_rank == 0 {
|
||||
return Err(AdaptError::InvalidRank);
|
||||
}
|
||||
let (n, fdim) = (self.calibration_buffer.len(), self.calibration_buffer[0].len());
|
||||
let lora_sz = 2 * fdim * self.lora_rank;
|
||||
let mut w = vec![0.01_f32; lora_sz];
|
||||
@@ -112,7 +153,7 @@ impl RapidAdaptation {
|
||||
for (wi, gi) in w.iter_mut().zip(g.iter()) { *wi -= lr * gi; }
|
||||
final_loss = loss;
|
||||
}
|
||||
AdaptationResult { lora_weights: w, final_loss, frames_used: n, adaptation_epochs: epochs }
|
||||
Ok(AdaptationResult { lora_weights: w, final_loss, frames_used: n, adaptation_epochs: epochs })
|
||||
}
|
||||
|
||||
fn contrastive_step(&self, w: &[f32], fdim: usize, grad: &mut [f32]) -> f32 {
|
||||
@@ -207,7 +248,7 @@ mod tests {
|
||||
let (fdim, rank) = (16, 4);
|
||||
let mut a = RapidAdaptation::new(10, rank, AdaptationLoss::ContrastiveTTT { epochs: 3, lr: 0.01 });
|
||||
for i in 0..10 { a.push_frame(&vec![i as f32 * 0.1; fdim]); }
|
||||
let r = a.adapt();
|
||||
let r = a.adapt().unwrap();
|
||||
assert_eq!(r.lora_weights.len(), 2 * fdim * rank);
|
||||
assert_eq!(r.frames_used, 10);
|
||||
assert_eq!(r.adaptation_epochs, 3);
|
||||
@@ -219,7 +260,7 @@ mod tests {
|
||||
let mk = |ep| {
|
||||
let mut a = RapidAdaptation::new(20, rank, AdaptationLoss::ContrastiveTTT { epochs: ep, lr: 0.01 });
|
||||
for i in 0..20 { let v = i as f32 * 0.1; a.push_frame(&(0..fdim).map(|d| v + d as f32 * 0.01).collect::<Vec<_>>()); }
|
||||
a.adapt().final_loss
|
||||
a.adapt().unwrap().final_loss
|
||||
};
|
||||
assert!(mk(10) <= mk(1) + 1e-6, "10 epochs should yield <= 1 epoch loss");
|
||||
}
|
||||
@@ -229,7 +270,7 @@ mod tests {
|
||||
let (fdim, rank) = (16, 4);
|
||||
let mut a = RapidAdaptation::new(10, rank, AdaptationLoss::Combined { epochs: 5, lr: 0.001, lambda_ent: 0.5 });
|
||||
for i in 0..10 { a.push_frame(&(0..fdim).map(|d| ((i * fdim + d) as f32).sin()).collect::<Vec<_>>()); }
|
||||
let r = a.adapt();
|
||||
let r = a.adapt().unwrap();
|
||||
assert_eq!(r.frames_used, 10);
|
||||
assert_eq!(r.adaptation_epochs, 5);
|
||||
assert!(r.final_loss.is_finite());
|
||||
@@ -237,6 +278,27 @@ mod tests {
|
||||
assert!(r.lora_weights.iter().all(|w| w.is_finite()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapt_empty_buffer_returns_error() {
|
||||
let a = RapidAdaptation::new(10, 4, AdaptationLoss::ContrastiveTTT { epochs: 1, lr: 0.01 });
|
||||
assert!(a.adapt().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapt_zero_rank_returns_error() {
|
||||
let mut a = RapidAdaptation::new(1, 0, AdaptationLoss::ContrastiveTTT { epochs: 1, lr: 0.01 });
|
||||
a.push_frame(&[1.0, 2.0]);
|
||||
assert!(a.adapt().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_cap_evicts_oldest() {
|
||||
let mut a = RapidAdaptation::new(2, 4, AdaptationLoss::ContrastiveTTT { epochs: 1, lr: 0.01 });
|
||||
a.max_buffer_frames = 3;
|
||||
for i in 0..5 { a.push_frame(&[i as f32]); }
|
||||
assert_eq!(a.buffer_len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn l2_distance_tests() {
|
||||
assert!(l2_dist(&[1.0, 2.0, 3.0], &[1.0, 2.0, 3.0]).abs() < 1e-10);
|
||||
|
||||
@@ -149,8 +149,8 @@ impl VirtualDomainAugmentor {
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for (k, &val) in frame.iter().enumerate() {
|
||||
let k_f = k as f32;
|
||||
// 1. Room-scale amplitude attenuation
|
||||
let scaled = val / domain.room_scale;
|
||||
// 1. Room-scale amplitude attenuation (guard against zero scale)
|
||||
let scaled = if domain.room_scale.abs() < 1e-10 { val } else { val / domain.room_scale };
|
||||
// 2. Reflection coefficient modulation (per-subcarrier)
|
||||
let refl = domain.reflection_coeff
|
||||
+ (1.0 - domain.reflection_coeff) * (PI * k_f / n_f).cos();
|
||||
|
||||
Reference in New Issue
Block a user