diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/lib.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/lib.rs index 651b7a5..b2802a8 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/lib.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/lib.rs @@ -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, diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/domain.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/domain.rs index cbb1682..1789c65 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/domain.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/domain.rs @@ -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 diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/geometry.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/geometry.rs index ac6f768..832441a 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/geometry.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/geometry.rs @@ -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 { 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 { 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() } diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/lib.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/lib.rs index 265e994..512aeee 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/lib.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/lib.rs @@ -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"); diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/rapid_adapt.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/rapid_adapt.rs index a8cefc6..9e97906 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/rapid_adapt.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/rapid_adapt.rs @@ -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>, } +/// 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 { + 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::>()); } - 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::>()); } - 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); diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/virtual_aug.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/virtual_aug.rs index b5e4c01..76cbb64 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/virtual_aug.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/virtual_aug.rs @@ -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();