Files
wifi-densepose/rust-port/wifi-densepose-rs/crates/wifi-densepose-vitals/src/store.rs
ruv 3e06970428 feat: Training mode, ADR docs, vitals and wifiscan crates
- Add --train CLI flag with dataset loading, graph transformer training,
  cosine-scheduled SGD, PCK/OKS validation, and checkpoint saving
- Refactor main.rs to import training modules from lib.rs instead of
  duplicating mod declarations
- Add ADR-021 (vital sign detection), ADR-022 (Windows WiFi enhanced
  fidelity), ADR-023 (trained DensePose pipeline) documentation
- Add wifi-densepose-vitals crate: breathing, heartrate, anomaly
  detection, preprocessor, and temporal store
- Add wifi-densepose-wifiscan crate: 8-stage signal intelligence
  pipeline with netsh/wlanapi adapters, multi-BSSID registry,
  attention weighting, spatial correlation, and breathing extraction

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-02-28 23:50:20 -05:00

291 lines
8.4 KiB
Rust

//! Vital sign time series store.
//!
//! Stores vital sign readings with configurable retention.
//! Designed for upgrade to `TieredStore` when `ruvector-temporal-tensor`
//! becomes available (ADR-021 phase 2).
use crate::types::{VitalReading, VitalStatus};
/// Simple vital sign store with capacity-limited ring buffer semantics.
pub struct VitalSignStore {
/// Stored readings (oldest first).
readings: Vec<VitalReading>,
/// Maximum number of readings to retain.
max_readings: usize,
}
/// Summary statistics for stored vital sign readings.
#[derive(Debug, Clone)]
pub struct VitalStats {
/// Number of readings in the store.
pub count: usize,
/// Mean respiratory rate (BPM).
pub rr_mean: f64,
/// Mean heart rate (BPM).
pub hr_mean: f64,
/// Min respiratory rate (BPM).
pub rr_min: f64,
/// Max respiratory rate (BPM).
pub rr_max: f64,
/// Min heart rate (BPM).
pub hr_min: f64,
/// Max heart rate (BPM).
pub hr_max: f64,
/// Fraction of readings with Valid status.
pub valid_fraction: f64,
}
impl VitalSignStore {
/// Create a new store with a given maximum capacity.
///
/// When the capacity is exceeded, the oldest readings are evicted.
#[must_use]
pub fn new(max_readings: usize) -> Self {
Self {
readings: Vec::with_capacity(max_readings.min(4096)),
max_readings: max_readings.max(1),
}
}
/// Create with default capacity (3600 readings ~ 1 hour at 1 Hz).
#[must_use]
pub fn default_capacity() -> Self {
Self::new(3600)
}
/// Push a new reading into the store.
///
/// If the store is at capacity, the oldest reading is evicted.
pub fn push(&mut self, reading: VitalReading) {
if self.readings.len() >= self.max_readings {
self.readings.remove(0);
}
self.readings.push(reading);
}
/// Get the most recent reading, if any.
#[must_use]
pub fn latest(&self) -> Option<&VitalReading> {
self.readings.last()
}
/// Get the last `n` readings (most recent last).
///
/// Returns fewer than `n` if the store contains fewer readings.
#[must_use]
pub fn history(&self, n: usize) -> &[VitalReading] {
let start = self.readings.len().saturating_sub(n);
&self.readings[start..]
}
/// Compute summary statistics over all stored readings.
///
/// Returns `None` if the store is empty.
#[must_use]
pub fn stats(&self) -> Option<VitalStats> {
if self.readings.is_empty() {
return None;
}
let n = self.readings.len() as f64;
let mut rr_sum = 0.0;
let mut hr_sum = 0.0;
let mut rr_min = f64::MAX;
let mut rr_max = f64::MIN;
let mut hr_min = f64::MAX;
let mut hr_max = f64::MIN;
let mut valid_count = 0_usize;
for r in &self.readings {
let rr = r.respiratory_rate.value_bpm;
let hr = r.heart_rate.value_bpm;
rr_sum += rr;
hr_sum += hr;
rr_min = rr_min.min(rr);
rr_max = rr_max.max(rr);
hr_min = hr_min.min(hr);
hr_max = hr_max.max(hr);
if r.respiratory_rate.status == VitalStatus::Valid
&& r.heart_rate.status == VitalStatus::Valid
{
valid_count += 1;
}
}
Some(VitalStats {
count: self.readings.len(),
rr_mean: rr_sum / n,
hr_mean: hr_sum / n,
rr_min,
rr_max,
hr_min,
hr_max,
valid_fraction: valid_count as f64 / n,
})
}
/// Number of readings currently stored.
#[must_use]
pub fn len(&self) -> usize {
self.readings.len()
}
/// Whether the store is empty.
#[must_use]
pub fn is_empty(&self) -> bool {
self.readings.is_empty()
}
/// Maximum capacity of the store.
#[must_use]
pub fn capacity(&self) -> usize {
self.max_readings
}
/// Clear all stored readings.
pub fn clear(&mut self) {
self.readings.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{VitalEstimate, VitalReading, VitalStatus};
fn make_reading(rr: f64, hr: f64) -> VitalReading {
VitalReading {
respiratory_rate: VitalEstimate {
value_bpm: rr,
confidence: 0.9,
status: VitalStatus::Valid,
},
heart_rate: VitalEstimate {
value_bpm: hr,
confidence: 0.85,
status: VitalStatus::Valid,
},
subcarrier_count: 56,
signal_quality: 0.9,
timestamp_secs: 0.0,
}
}
#[test]
fn empty_store() {
let store = VitalSignStore::new(10);
assert!(store.is_empty());
assert_eq!(store.len(), 0);
assert!(store.latest().is_none());
assert!(store.stats().is_none());
}
#[test]
fn push_and_retrieve() {
let mut store = VitalSignStore::new(10);
store.push(make_reading(15.0, 72.0));
assert_eq!(store.len(), 1);
assert!(!store.is_empty());
let latest = store.latest().unwrap();
assert!((latest.respiratory_rate.value_bpm - 15.0).abs() < f64::EPSILON);
}
#[test]
fn eviction_at_capacity() {
let mut store = VitalSignStore::new(3);
store.push(make_reading(10.0, 60.0));
store.push(make_reading(15.0, 72.0));
store.push(make_reading(20.0, 80.0));
assert_eq!(store.len(), 3);
// Push one more; oldest should be evicted
store.push(make_reading(25.0, 90.0));
assert_eq!(store.len(), 3);
// Oldest should now be 15.0, not 10.0
let oldest = &store.history(10)[0];
assert!((oldest.respiratory_rate.value_bpm - 15.0).abs() < f64::EPSILON);
}
#[test]
fn history_returns_last_n() {
let mut store = VitalSignStore::new(10);
for i in 0..5 {
store.push(make_reading(10.0 + i as f64, 60.0 + i as f64));
}
let last3 = store.history(3);
assert_eq!(last3.len(), 3);
assert!((last3[0].respiratory_rate.value_bpm - 12.0).abs() < f64::EPSILON);
assert!((last3[2].respiratory_rate.value_bpm - 14.0).abs() < f64::EPSILON);
}
#[test]
fn history_when_fewer_than_n() {
let mut store = VitalSignStore::new(10);
store.push(make_reading(15.0, 72.0));
let all = store.history(100);
assert_eq!(all.len(), 1);
}
#[test]
fn stats_computation() {
let mut store = VitalSignStore::new(10);
store.push(make_reading(10.0, 60.0));
store.push(make_reading(20.0, 80.0));
store.push(make_reading(15.0, 70.0));
let stats = store.stats().unwrap();
assert_eq!(stats.count, 3);
assert!((stats.rr_mean - 15.0).abs() < f64::EPSILON);
assert!((stats.hr_mean - 70.0).abs() < f64::EPSILON);
assert!((stats.rr_min - 10.0).abs() < f64::EPSILON);
assert!((stats.rr_max - 20.0).abs() < f64::EPSILON);
assert!((stats.hr_min - 60.0).abs() < f64::EPSILON);
assert!((stats.hr_max - 80.0).abs() < f64::EPSILON);
assert!((stats.valid_fraction - 1.0).abs() < f64::EPSILON);
}
#[test]
fn stats_valid_fraction() {
let mut store = VitalSignStore::new(10);
store.push(make_reading(15.0, 72.0)); // Valid
store.push(VitalReading {
respiratory_rate: VitalEstimate {
value_bpm: 15.0,
confidence: 0.3,
status: VitalStatus::Degraded,
},
heart_rate: VitalEstimate {
value_bpm: 72.0,
confidence: 0.8,
status: VitalStatus::Valid,
},
subcarrier_count: 56,
signal_quality: 0.5,
timestamp_secs: 1.0,
});
let stats = store.stats().unwrap();
assert!((stats.valid_fraction - 0.5).abs() < f64::EPSILON);
}
#[test]
fn clear_empties_store() {
let mut store = VitalSignStore::new(10);
store.push(make_reading(15.0, 72.0));
store.push(make_reading(16.0, 73.0));
assert_eq!(store.len(), 2);
store.clear();
assert!(store.is_empty());
}
#[test]
fn default_capacity_is_3600() {
let store = VitalSignStore::default_capacity();
assert_eq!(store.capacity(), 3600);
}
}