rvcsi_ruvector/
embedding.rs1use rvcsi_core::{CsiEvent, CsiEventKind, CsiWindow};
14
15pub const WINDOW_EMBEDDING_DIM: usize = 68;
28
29pub const EVENT_EMBEDDING_DIM: usize = 12;
40
41const SUBCARRIER_BINS: usize = 32;
43
44fn resample_linear(src: &[f32], m: usize) -> Vec<f32> {
52 let n = src.len();
53 if n == 0 {
54 return vec![0.0; m];
55 }
56 if n == 1 {
57 return vec![src[0]; m];
58 }
59 if m == 0 {
60 return Vec::new();
61 }
62 if m == 1 {
63 return vec![src[0]];
65 }
66 let mut out = Vec::with_capacity(m);
67 let denom = (m - 1) as f32;
68 let span = (n - 1) as f32;
69 for j in 0..m {
70 let pos = j as f32 * span / denom;
71 let lo = pos.floor() as usize;
72 let frac = pos - lo as f32;
73 let hi = (lo + 1).min(n - 1);
74 out.push(src[lo] * (1.0 - frac) + src[hi] * frac);
75 }
76 out
77}
78
79fn l2_norm(v: &[f32]) -> f32 {
81 v.iter().map(|x| x * x).sum::<f32>().sqrt()
82}
83
84fn l2_normalize(v: &mut [f32]) {
87 let norm = l2_norm(v);
88 if norm.is_finite() && norm > 0.0 {
89 for x in v.iter_mut() {
90 *x /= norm;
91 }
92 }
93}
94
95pub fn window_embedding(w: &CsiWindow) -> Vec<f32> {
101 let mut out = Vec::with_capacity(WINDOW_EMBEDDING_DIM);
102 out.extend(resample_linear(&w.mean_amplitude, SUBCARRIER_BINS));
103 out.extend(resample_linear(&w.phase_variance, SUBCARRIER_BINS));
104 out.push(w.motion_energy);
105 out.push(w.presence_score);
106 out.push(w.quality_score);
107 out.push((w.frame_count as f32).ln_1p());
108 debug_assert_eq!(out.len(), WINDOW_EMBEDDING_DIM);
109 l2_normalize(&mut out);
110 out
111}
112
113fn kind_index(k: CsiEventKind) -> usize {
116 match k {
117 CsiEventKind::PresenceStarted => 0,
118 CsiEventKind::PresenceEnded => 1,
119 CsiEventKind::MotionDetected => 2,
120 CsiEventKind::MotionSettled => 3,
121 CsiEventKind::BaselineChanged => 4,
122 CsiEventKind::SignalQualityDropped => 5,
123 CsiEventKind::DeviceDisconnected => 6,
124 CsiEventKind::BreathingCandidate => 7,
125 CsiEventKind::AnomalyDetected => 8,
126 CsiEventKind::CalibrationRequired => 9,
127 }
128}
129
130pub fn event_embedding(e: &CsiEvent) -> Vec<f32> {
135 let mut out = vec![0.0_f32; EVENT_EMBEDDING_DIM];
136 out[kind_index(e.kind)] = 1.0;
137 out[10] = e.confidence;
138 out[11] = (e.evidence_window_ids.len() as f32).ln_1p();
139 out
140}
141
142pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
148 if a.len() != b.len() || a.is_empty() {
149 return 0.0;
150 }
151 let na = l2_norm(a);
152 let nb = l2_norm(b);
153 if !(na.is_finite() && nb.is_finite()) || na == 0.0 || nb == 0.0 {
154 return 0.0;
155 }
156 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
157 (dot / (na * nb)).clamp(-1.0, 1.0)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use rvcsi_core::{EventId, SessionId, SourceId, WindowId};
164
165 fn window() -> CsiWindow {
166 CsiWindow {
167 window_id: WindowId(7),
168 session_id: SessionId(1),
169 source_id: SourceId::from("emb-test"),
170 start_ns: 1_000,
171 end_ns: 2_000,
172 frame_count: 12,
173 mean_amplitude: vec![1.0, 2.0, 3.0, 4.0, 5.0],
174 phase_variance: vec![0.1, 0.2, 0.1, 0.3, 0.2],
175 motion_energy: 0.42,
176 presence_score: 0.8,
177 quality_score: 0.9,
178 }
179 }
180
181 fn event(kind: CsiEventKind) -> CsiEvent {
182 CsiEvent::new(
183 EventId(3),
184 kind,
185 SessionId(1),
186 SourceId::from("emb-test"),
187 5_000,
188 0.75,
189 vec![WindowId(1), WindowId(2)],
190 )
191 }
192
193 #[test]
194 fn resample_edge_cases() {
195 assert_eq!(resample_linear(&[], 4), vec![0.0; 4]);
196 assert_eq!(resample_linear(&[2.5], 3), vec![2.5, 2.5, 2.5]);
197 let r = resample_linear(&[0.0, 1.0, 2.0], 3);
199 assert!((r[0] - 0.0).abs() < 1e-6);
200 assert!((r[1] - 1.0).abs() < 1e-6);
201 assert!((r[2] - 2.0).abs() < 1e-6);
202 let r = resample_linear(&[0.0, 4.0], 5);
204 assert!((r[2] - 2.0).abs() < 1e-6);
205 }
206
207 #[test]
208 fn window_embedding_is_deterministic_and_unit_length() {
209 let w = window();
210 let a = window_embedding(&w);
211 let b = window_embedding(&w);
212 assert_eq!(a, b);
213 assert_eq!(a.len(), WINDOW_EMBEDDING_DIM);
214 let norm = l2_norm(&a);
215 assert!((norm - 1.0).abs() < 1e-5, "norm was {norm}");
216 }
217
218 #[test]
219 fn empty_window_embeds_to_zero() {
220 let mut w = window();
221 w.mean_amplitude.clear();
222 w.phase_variance.clear();
223 w.motion_energy = 0.0;
224 w.presence_score = 0.0;
225 w.quality_score = 0.0;
226 w.frame_count = 0;
227 let e = window_embedding(&w);
228 assert_eq!(e.len(), WINDOW_EMBEDDING_DIM);
229 assert!(e.iter().all(|x| *x == 0.0));
230 }
231
232 #[test]
233 fn window_embedding_length_independent_of_subcarrier_count() {
234 let mut a = window();
235 a.mean_amplitude = vec![1.0; 56];
236 a.phase_variance = vec![0.1; 56];
237 let mut b = window();
238 b.mean_amplitude = vec![1.0; 234];
239 b.phase_variance = vec![0.1; 234];
240 assert_eq!(window_embedding(&a).len(), window_embedding(&b).len());
241 }
242
243 #[test]
244 fn event_embedding_layout() {
245 let e = event(CsiEventKind::MotionDetected);
246 let v = event_embedding(&e);
247 assert_eq!(v.len(), EVENT_EMBEDDING_DIM);
248 assert_eq!(v[kind_index(CsiEventKind::MotionDetected)], 1.0);
249 assert_eq!(v[..10].iter().filter(|x| **x == 1.0).count(), 1);
251 assert!((v[10] - 0.75).abs() < 1e-6);
252 assert!((v[11] - (2.0_f32).ln_1p()).abs() < 1e-6);
253
254 let v2 = event_embedding(&event(CsiEventKind::AnomalyDetected));
256 assert_eq!(v2[kind_index(CsiEventKind::AnomalyDetected)], 1.0);
257 assert_ne!(v, v2);
258 }
259
260 #[test]
261 fn cosine_basic_identities() {
262 let v = window_embedding(&window());
263 assert!((cosine_similarity(&v, &v) - 1.0).abs() < 1e-5);
264 let neg: Vec<f32> = v.iter().map(|x| -x).collect();
265 assert!((cosine_similarity(&v, &neg) + 1.0).abs() < 1e-5);
266 assert_eq!(cosine_similarity(&v, &v[..3]), 0.0);
268 assert_eq!(cosine_similarity(&[0.0; 4], &[1.0; 4]), 0.0);
270 assert_eq!(cosine_similarity(&[], &[]), 0.0);
271 }
272}