Files
ruvnet--RuView/v2/crates/wifi-densepose-ruvector/src/mat/heartbeat.rs
T
rUv f49c722764 chore(repo): rename rust-port/wifi-densepose-rs → v2/ (flatten to one level) (#427)
The Rust port lived two directories deep (rust-port/wifi-densepose-rs/)
without any sibling under rust-port/ that warranted the extra level.
Move the whole workspace up to v2/ to match v1/ (Python) at the same
depth and shorten every cd / build command across the repo.

git mv preserves history for all tracked files. 60 files updated for
path references (CI workflows, ADRs, docs, scripts, READMEs, internal
.claude-flow state). Two manual fixes for relative-cd paths in
CLAUDE.md and ADR-043 that became wrong after the depth change
(cd ../.. → cd ..).

Validated:
- cargo check --workspace --no-default-features → clean (after target/
  nuke; the gitignored target/ was carried by the OS rename and had
  hard-coded old paths in build scripts)
- cargo test --workspace --no-default-features → 1,539 passed, 0 failed,
  8 ignored (same totals as pre-rename)
- ESP32-S3 on COM7 → still streaming live CSI (cb #40300, RSSI -64 dBm)

After-merge follow-up: contributors should `rm -rf v2/target` once and
let cargo regenerate from the new path.
2026-04-25 21:28:13 -04:00

110 lines
3.8 KiB
Rust

//! Tiered compressed heartbeat spectrogram (ruvector-temporal-tensor).
//!
//! [`CompressedHeartbeatSpectrogram`] stores a rolling spectrogram with one
//! [`TemporalTensorCompressor`] per frequency bin, enabling independent
//! tiering per bin. Hot tier (recent frames) at 8-bit, cold at 3-bit.
//!
//! [`band_power`] extracts mean squared power in any frequency band.
use ruvector_temporal_tensor::segment as tt_segment;
use ruvector_temporal_tensor::{TemporalTensorCompressor, TierPolicy};
/// Tiered compressed heartbeat spectrogram.
///
/// One compressor per frequency bin. Hot tier (recent) at 8-bit, cold at 3-bit.
pub struct CompressedHeartbeatSpectrogram {
bin_buffers: Vec<TemporalTensorCompressor>,
encoded: Vec<Vec<u8>>,
/// Number of frequency bins (e.g. 128).
pub n_freq_bins: usize,
frame_count: u32,
}
impl CompressedHeartbeatSpectrogram {
/// Create with `n_freq_bins` frequency bins (e.g. 128).
///
/// Each frequency bin gets its own [`TemporalTensorCompressor`] instance
/// so the tiering policy operates independently per bin.
pub fn new(n_freq_bins: usize) -> Self {
let bin_buffers = (0..n_freq_bins)
.map(|i| TemporalTensorCompressor::new(TierPolicy::default(), 1, i as u32))
.collect();
Self {
bin_buffers,
encoded: vec![Vec::new(); n_freq_bins],
n_freq_bins,
frame_count: 0,
}
}
/// Push one spectrogram column (one time step, all frequency bins).
///
/// `column` must have length equal to `n_freq_bins`.
pub fn push_column(&mut self, column: &[f32]) {
let ts = self.frame_count;
for (i, (&val, buf)) in column.iter().zip(self.bin_buffers.iter_mut()).enumerate() {
buf.set_access(ts, ts);
buf.push_frame(&[val], ts, &mut self.encoded[i]);
}
self.frame_count += 1;
}
/// Total number of columns pushed.
pub fn frame_count(&self) -> u32 {
self.frame_count
}
/// Extract mean squared power in a frequency band (indices `low_bin..=high_bin`).
///
/// Decodes only the bins in the requested range and returns the mean of
/// the squared decoded values over the last up to 100 frames.
/// Returns `0.0` for an empty range.
pub fn band_power(&self, low_bin: usize, high_bin: usize) -> f32 {
let n = (high_bin.min(self.n_freq_bins - 1) + 1).saturating_sub(low_bin);
if n == 0 {
return 0.0;
}
(low_bin..=high_bin.min(self.n_freq_bins - 1))
.map(|b| {
let mut out = Vec::new();
tt_segment::decode(&self.encoded[b], &mut out);
out.iter().rev().take(100).map(|x| x * x).sum::<f32>()
})
.sum::<f32>()
/ n as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn heartbeat_spectrogram_frame_count() {
let n_freq_bins = 16;
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();
spec.push_column(&column);
}
assert_eq!(spec.frame_count(), 10, "frame_count must equal the number of pushed columns");
}
#[test]
fn heartbeat_band_power_runs() {
let n_freq_bins = 16;
let mut spec = CompressedHeartbeatSpectrogram::new(n_freq_bins);
for i in 0..10 {
let column: Vec<f32> = (0..n_freq_bins).map(|b| (i + b) as f32 * 0.1).collect();
spec.push_column(&column);
}
// band_power must not panic and must return a non-negative value.
let power = spec.band_power(2, 6);
assert!(power >= 0.0, "band_power must be non-negative");
}
}