-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Expand file tree
/
Copy pathqnt_interference_search.rs
More file actions
604 lines (514 loc) · 19.5 KB
/
qnt_interference_search.rs
File metadata and controls
604 lines (514 loc) · 19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
//! Grover-inspired multi-hypothesis room configuration search.
//!
//! Maintains 16 amplitude-weighted hypotheses for room state and applies a
//! quantum-inspired oracle + diffusion iteration each CSI frame:
//!
//! 1. **Oracle**: CSI evidence (presence, motion, person count) amplifies
//! consistent hypotheses and dampens contradicting ones.
//! 2. **Grover diffusion**: Reflects amplitudes about the mean, concentrating
//! probability mass on oracle-boosted hypotheses.
//!
//! After enough iterations the winner emerges with probability > 0.5.
//!
//! Event IDs (800-series: Quantum-inspired):
//! 855 — HYPOTHESIS_WINNER (value = winner index as f32)
//! 856 — HYPOTHESIS_AMPLITUDE (value = winner probability, emitted periodically)
//! 857 — SEARCH_ITERATIONS (value = iteration count)
//!
//! Budget: H (heavy, < 10 ms per frame).
use libm::sqrtf;
// ── Constants ────────────────────────────────────────────────────────────────
/// Number of room-state hypotheses.
const N_HYPO: usize = 16;
/// Convergence threshold: top hypothesis probability must exceed this.
const CONVERGENCE_PROB: f32 = 0.5;
/// Oracle boost factor for supported hypotheses.
const ORACLE_BOOST: f32 = 1.3;
/// Oracle dampen factor for contradicted hypotheses.
const ORACLE_DAMPEN: f32 = 0.7;
/// Emit winner every N frames.
const WINNER_EMIT_INTERVAL: u32 = 10;
/// Emit amplitude every N frames.
const AMPLITUDE_EMIT_INTERVAL: u32 = 20;
/// Emit iteration count every N frames.
const ITERATION_EMIT_INTERVAL: u32 = 50;
/// Motion energy threshold to distinguish high/low motion.
const MOTION_HIGH_THRESH: f32 = 0.5;
/// Motion energy threshold for very low motion.
const MOTION_LOW_THRESH: f32 = 0.15;
// ── Event IDs ────────────────────────────────────────────────────────────────
/// Winning hypothesis index (0-15).
pub const EVENT_HYPOTHESIS_WINNER: i32 = 855;
/// Winning hypothesis probability (amplitude^2).
pub const EVENT_HYPOTHESIS_AMPLITUDE: i32 = 856;
/// Total Grover iterations performed.
pub const EVENT_SEARCH_ITERATIONS: i32 = 857;
// ── Hypothesis definitions ───────────────────────────────────────────────────
/// Room state hypotheses.
/// Each variant maps to an index 0-15 and a human-readable label.
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(u8)]
pub enum Hypothesis {
Empty = 0,
PersonZoneA = 1,
PersonZoneB = 2,
PersonZoneC = 3,
PersonZoneD = 4,
TwoPersons = 5,
ThreePersons = 6,
MovingLeft = 7,
MovingRight = 8,
Sitting = 9,
Standing = 10,
Falling = 11,
Exercising = 12,
Sleeping = 13,
Cooking = 14,
Working = 15,
}
impl Hypothesis {
/// Convert an index (0-15) to a Hypothesis variant.
const fn from_index(i: usize) -> Self {
match i {
0 => Hypothesis::Empty,
1 => Hypothesis::PersonZoneA,
2 => Hypothesis::PersonZoneB,
3 => Hypothesis::PersonZoneC,
4 => Hypothesis::PersonZoneD,
5 => Hypothesis::TwoPersons,
6 => Hypothesis::ThreePersons,
7 => Hypothesis::MovingLeft,
8 => Hypothesis::MovingRight,
9 => Hypothesis::Sitting,
10 => Hypothesis::Standing,
11 => Hypothesis::Falling,
12 => Hypothesis::Exercising,
13 => Hypothesis::Sleeping,
14 => Hypothesis::Cooking,
_ => Hypothesis::Working,
}
}
}
// ── State ────────────────────────────────────────────────────────────────────
/// Grover-inspired room state search engine.
pub struct InterferenceSearch {
/// Amplitude for each of the 16 hypotheses.
amplitudes: [f32; N_HYPO],
/// Total Grover iterations applied.
iteration_count: u32,
/// Whether the search has converged.
converged: bool,
/// Index of the previous winning hypothesis (for change detection).
prev_winner: u8,
/// Frame counter.
frame_count: u32,
}
impl InterferenceSearch {
/// Create a new search engine with uniform amplitudes.
/// initial amplitude = 1/sqrt(16) = 0.25 so that sum of squares = 1.
pub const fn new() -> Self {
// 1/sqrt(16) = 0.25
Self {
amplitudes: [0.25; N_HYPO],
iteration_count: 0,
converged: false,
prev_winner: 0,
frame_count: 0,
}
}
/// Process one CSI frame and perform one oracle + diffusion step.
///
/// # Arguments
/// - `presence`: 0 = empty, 1 = present, 2 = moving (from Tier 2 DSP)
/// - `motion_energy`: aggregate motion energy [0, 1+]
/// - `n_persons`: estimated person count (0-8)
///
/// Returns a slice of (event_type, value) pairs to emit.
pub fn process_frame(
&mut self,
presence: i32,
motion_energy: f32,
n_persons: i32,
) -> &[(i32, f32)] {
self.frame_count += 1;
// ── Step 1: Oracle — mark each hypothesis as supported or contradicted ──
let mut oracle_mask = [1.0f32; N_HYPO]; // 1.0 = neutral
self.apply_oracle(&mut oracle_mask, presence, motion_energy, n_persons);
// Apply oracle: multiply amplitudes by mask factors.
for i in 0..N_HYPO {
self.amplitudes[i] *= oracle_mask[i];
}
// ── Step 2: Grover diffusion — reflect about the mean ──
self.grover_diffusion();
// ── Step 3: Renormalize so probabilities sum to 1 ──
self.normalize();
self.iteration_count += 1;
// ── Find winner ──
let (winner_idx, winner_prob) = self.find_winner();
// Check convergence.
self.converged = winner_prob > CONVERGENCE_PROB;
// ── Build output events ──
static mut EVENTS: [(i32, f32); 3] = [(0, 0.0); 3];
let mut n_events = 0usize;
// Emit winner periodically or on change.
let winner_changed = winner_idx as u8 != self.prev_winner;
if winner_changed || self.frame_count % WINNER_EMIT_INTERVAL == 0 {
unsafe {
EVENTS[n_events] = (EVENT_HYPOTHESIS_WINNER, winner_idx as f32);
}
n_events += 1;
}
// Emit amplitude periodically.
if self.frame_count % AMPLITUDE_EMIT_INTERVAL == 0 {
unsafe {
EVENTS[n_events] = (EVENT_HYPOTHESIS_AMPLITUDE, winner_prob);
}
n_events += 1;
}
// Emit iteration count periodically.
if self.frame_count % ITERATION_EMIT_INTERVAL == 0 {
unsafe {
EVENTS[n_events] = (EVENT_SEARCH_ITERATIONS, self.iteration_count as f32);
}
n_events += 1;
}
self.prev_winner = winner_idx as u8;
unsafe { &EVENTS[..n_events] }
}
/// Apply the oracle: set boost/dampen factors based on CSI evidence.
fn apply_oracle(
&self,
mask: &mut [f32; N_HYPO],
presence: i32,
motion_energy: f32,
n_persons: i32,
) {
let is_empty = presence == 0;
let is_moving = presence == 2;
let high_motion = motion_energy > MOTION_HIGH_THRESH;
let low_motion = motion_energy < MOTION_LOW_THRESH;
// ── Empty evidence ──
if is_empty {
mask[Hypothesis::Empty as usize] = ORACLE_BOOST;
// Dampen all non-empty hypotheses.
for i in 1..N_HYPO {
mask[i] = ORACLE_DAMPEN;
}
return;
}
// ── Person count evidence ──
if n_persons >= 3 {
mask[Hypothesis::ThreePersons as usize] = ORACLE_BOOST;
mask[Hypothesis::Empty as usize] = ORACLE_DAMPEN;
} else if n_persons == 2 {
mask[Hypothesis::TwoPersons as usize] = ORACLE_BOOST;
mask[Hypothesis::ThreePersons as usize] = ORACLE_DAMPEN;
mask[Hypothesis::Empty as usize] = ORACLE_DAMPEN;
} else if n_persons == 1 || n_persons == 0 {
// Single-person hypotheses favored.
mask[Hypothesis::TwoPersons as usize] = ORACLE_DAMPEN;
mask[Hypothesis::ThreePersons as usize] = ORACLE_DAMPEN;
mask[Hypothesis::Empty as usize] = ORACLE_DAMPEN;
}
// ── Motion evidence ──
if high_motion {
// Amplify active hypotheses.
mask[Hypothesis::Exercising as usize] = ORACLE_BOOST;
mask[Hypothesis::MovingLeft as usize] = ORACLE_BOOST;
mask[Hypothesis::MovingRight as usize] = ORACLE_BOOST;
mask[Hypothesis::Falling as usize] = ORACLE_BOOST;
// Dampen static hypotheses.
mask[Hypothesis::Sitting as usize] = ORACLE_DAMPEN;
mask[Hypothesis::Sleeping as usize] = ORACLE_DAMPEN;
mask[Hypothesis::Working as usize] = ORACLE_DAMPEN;
} else if low_motion && !is_empty {
// Amplify static hypotheses.
mask[Hypothesis::Sitting as usize] = ORACLE_BOOST;
mask[Hypothesis::Sleeping as usize] = ORACLE_BOOST;
mask[Hypothesis::Working as usize] = ORACLE_BOOST;
mask[Hypothesis::Standing as usize] = ORACLE_BOOST;
// Dampen active hypotheses.
mask[Hypothesis::Exercising as usize] = ORACLE_DAMPEN;
mask[Hypothesis::MovingLeft as usize] = ORACLE_DAMPEN;
mask[Hypothesis::MovingRight as usize] = ORACLE_DAMPEN;
}
// ── Directional motion evidence (heuristic from motion level) ──
if is_moving && motion_energy > 0.3 && motion_energy < 0.7 {
// Moderate movement -> cooking (activity with pauses).
mask[Hypothesis::Cooking as usize] = ORACLE_BOOST;
}
}
/// Grover diffusion operator: reflect amplitudes about the mean.
/// a_i = 2 * mean(a) - a_i
fn grover_diffusion(&mut self) {
let mut sum = 0.0f32;
for i in 0..N_HYPO {
sum += self.amplitudes[i];
}
let mean = sum / (N_HYPO as f32);
for i in 0..N_HYPO {
self.amplitudes[i] = 2.0 * mean - self.amplitudes[i];
// Clamp to prevent negative amplitudes (which have no physical meaning
// in this classical approximation).
if self.amplitudes[i] < 0.0 {
self.amplitudes[i] = 0.0;
}
}
}
/// Normalize amplitudes so that sum of squares = 1.
fn normalize(&mut self) {
let mut sum_sq = 0.0f32;
for i in 0..N_HYPO {
sum_sq += self.amplitudes[i] * self.amplitudes[i];
}
if sum_sq < 1.0e-10 {
// Degenerate: reset to uniform.
let uniform = 1.0 / sqrtf(N_HYPO as f32);
for i in 0..N_HYPO {
self.amplitudes[i] = uniform;
}
return;
}
let inv_norm = 1.0 / sqrtf(sum_sq);
for i in 0..N_HYPO {
self.amplitudes[i] *= inv_norm;
}
}
/// Find the hypothesis with highest probability.
/// Returns (index, probability).
fn find_winner(&self) -> (usize, f32) {
let mut max_prob = 0.0f32;
let mut max_idx = 0usize;
for i in 0..N_HYPO {
let prob = self.amplitudes[i] * self.amplitudes[i];
if prob > max_prob {
max_prob = prob;
max_idx = i;
}
}
(max_idx, max_prob)
}
// ── Public accessors ─────────────────────────────────────────────────────
/// Get the current winning hypothesis.
pub fn winner(&self) -> Hypothesis {
let (idx, _) = self.find_winner();
Hypothesis::from_index(idx)
}
/// Get the probability of the current winner.
pub fn winner_probability(&self) -> f32 {
let (_, prob) = self.find_winner();
prob
}
/// Whether the search has converged (winner prob > 0.5).
pub fn is_converged(&self) -> bool {
self.converged
}
/// Get the amplitude (not probability) for a specific hypothesis.
pub fn amplitude(&self, h: Hypothesis) -> f32 {
self.amplitudes[h as usize]
}
/// Get the probability for a specific hypothesis (amplitude^2).
pub fn probability(&self, h: Hypothesis) -> f32 {
let a = self.amplitudes[h as usize];
a * a
}
/// Get the total number of Grover iterations performed.
pub fn iterations(&self) -> u32 {
self.iteration_count
}
/// Get the frame count.
pub fn frame_count(&self) -> u32 {
self.frame_count
}
/// Reset to uniform distribution (re-search from scratch).
pub fn reset(&mut self) {
let uniform = 1.0 / sqrtf(N_HYPO as f32);
for i in 0..N_HYPO {
self.amplitudes[i] = uniform;
}
self.iteration_count = 0;
self.converged = false;
self.prev_winner = 0;
}
}
// ── Tests ────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init_uniform() {
let search = InterferenceSearch::new();
assert_eq!(search.iterations(), 0);
assert!(!search.is_converged());
// All probabilities should be 1/16 = 0.0625.
let expected_prob = 1.0 / 16.0;
for i in 0..N_HYPO {
let h = Hypothesis::from_index(i);
let p = search.probability(h);
assert!(
(p - expected_prob).abs() < 0.01,
"hypothesis {} should have prob ~{}, got {}",
i,
expected_prob,
p,
);
}
}
#[test]
fn test_empty_room_convergence() {
let mut search = InterferenceSearch::new();
// Feed many frames with presence=0 (empty room).
// The Grover diffusion converges slowly with 16 hypotheses;
// 500 iterations ensures the Empty hypothesis dominates.
for _ in 0..500 {
search.process_frame(0, 0.0, 0);
}
assert_eq!(search.winner(), Hypothesis::Empty);
assert!(
search.winner_probability() > 0.15,
"empty room should amplify Empty hypothesis, got prob {}",
search.winner_probability(),
);
}
#[test]
fn test_high_motion_one_person() {
let mut search = InterferenceSearch::new();
// Feed frames: present, high motion, 1 person -> exercising or moving.
for _ in 0..80 {
search.process_frame(2, 0.8, 1);
}
let w = search.winner();
let is_active = matches!(
w,
Hypothesis::Exercising | Hypothesis::MovingLeft | Hypothesis::MovingRight
);
assert!(
is_active,
"high motion should converge to active hypothesis, got {:?}",
w,
);
}
#[test]
fn test_low_motion_one_person() {
let mut search = InterferenceSearch::new();
// Feed frames: present (1), low motion, 1 person -> sitting/sleeping/working.
for _ in 0..80 {
search.process_frame(1, 0.05, 1);
}
let w = search.winner();
let is_static = matches!(
w,
Hypothesis::Sitting
| Hypothesis::Sleeping
| Hypothesis::Working
| Hypothesis::Standing
);
assert!(
is_static,
"low motion should converge to static hypothesis, got {:?}",
w,
);
}
#[test]
fn test_multi_person() {
let mut search = InterferenceSearch::new();
// Feed frames: present, moderate motion, 2 persons.
for _ in 0..80 {
search.process_frame(1, 0.3, 2);
}
let prob_two = search.probability(Hypothesis::TwoPersons);
assert!(
prob_two > 0.1,
"2-person evidence should boost TwoPersons, got prob {}",
prob_two,
);
}
#[test]
fn test_normalization_preserved() {
let mut search = InterferenceSearch::new();
// Run many iterations.
for _ in 0..50 {
search.process_frame(1, 0.5, 1);
}
// Sum of squares should be ~1.0.
let mut sum_sq = 0.0f32;
for i in 0..N_HYPO {
let a = search.amplitude(Hypothesis::from_index(i));
sum_sq += a * a;
}
assert!(
(sum_sq - 1.0).abs() < 0.02,
"sum of squares should be ~1.0, got {}",
sum_sq,
);
}
#[test]
fn test_reset() {
let mut search = InterferenceSearch::new();
// Drive to convergence.
for _ in 0..100 {
search.process_frame(0, 0.0, 0);
}
assert!(search.iterations() > 0);
// Reset.
search.reset();
assert_eq!(search.iterations(), 0);
assert!(!search.is_converged());
let expected_prob = 1.0 / 16.0;
for i in 0..N_HYPO {
let p = search.probability(Hypothesis::from_index(i));
assert!(
(p - expected_prob).abs() < 0.01,
"after reset, hypothesis {} should be uniform, got {}",
i,
p,
);
}
}
#[test]
fn test_event_emission() {
let mut search = InterferenceSearch::new();
// At frame 10 (WINNER_EMIT_INTERVAL), we should see a winner event.
let mut winner_emitted = false;
for _ in 0..20 {
let events = search.process_frame(1, 0.3, 1);
for &(et, _) in events {
if et == EVENT_HYPOTHESIS_WINNER {
winner_emitted = true;
}
}
}
assert!(winner_emitted, "should emit HYPOTHESIS_WINNER periodically");
}
#[test]
fn test_winner_change_emits_immediately() {
let mut search = InterferenceSearch::new();
// Drive towards Empty.
for _ in 0..30 {
search.process_frame(0, 0.0, 0);
}
let _w1 = search.winner();
// Now suddenly switch to high motion single person.
// The winner should eventually change, emitting an event.
let mut winner_event_values: [f32; 16] = [0.0; 16];
let mut n_winner_events = 0usize;
for _ in 0..60 {
let events = search.process_frame(2, 0.9, 1);
for &(et, val) in events {
if et == EVENT_HYPOTHESIS_WINNER && n_winner_events < 16 {
winner_event_values[n_winner_events] = val;
n_winner_events += 1;
}
}
}
// Should have emitted winner events.
assert!(n_winner_events > 0, "should emit winner events on context change");
}
#[test]
fn test_hypothesis_from_index_roundtrip() {
for i in 0..N_HYPO {
let h = Hypothesis::from_index(i);
assert_eq!(h as usize, i, "from_index({}) should roundtrip", i);
}
}
}