-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Expand file tree
/
Copy pathlrn_meta_adapt.rs
More file actions
471 lines (409 loc) · 15.7 KB
/
lrn_meta_adapt.rs
File metadata and controls
471 lines (409 loc) · 15.7 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
//! Meta-learning parameter self-optimization with safety constraints.
//!
//! ADR-041 adaptive learning module — Event IDs 740-743.
//!
//! Maintains 8 tunable runtime parameters (thresholds for presence, motion,
//! coherence, gesture DTW, etc.) and optimizes them via hill-climbing on a
//! performance score derived from event feedback.
//!
//! Performance score = true_positive_rate - 2 * false_positive_rate
//! (penalizes false positives more heavily than missing true positives)
//!
//! Optimization loop (runs on_timer, not per-frame):
//! 1. Perturb one parameter by +/- step_size
//! 2. Evaluate performance score over the next evaluation window
//! 3. Keep change if score improved, revert if not
//! 4. Safety: never exceed min/max bounds, rollback all changes if 3
//! consecutive degradations occur
//!
//! Budget: S (standard, < 5 ms — runs on timer, not per-frame).
/// Number of tunable parameters.
const NUM_PARAMS: usize = 8;
/// Maximum consecutive failures before safety rollback.
const MAX_CONSECUTIVE_FAILURES: u8 = 3;
/// Minimum evaluation window (timer ticks) before scoring a perturbation.
const EVAL_WINDOW: u16 = 10;
/// Default parameter step size (fraction of range).
const DEFAULT_STEP_FRAC: f32 = 0.05;
// ── Event IDs (740-series: Meta-learning) ────────────────────────────────────
pub const EVENT_PARAM_ADJUSTED: i32 = 740;
pub const EVENT_ADAPTATION_SCORE: i32 = 741;
pub const EVENT_ROLLBACK_TRIGGERED: i32 = 742;
pub const EVENT_META_LEVEL: i32 = 743;
/// One tunable parameter with bounds and step size.
#[derive(Clone, Copy)]
struct TunableParam {
/// Current value.
value: f32,
/// Minimum allowed value.
min_bound: f32,
/// Maximum allowed value.
max_bound: f32,
/// Perturbation step size.
step_size: f32,
/// Value before the current perturbation (for revert).
prev_value: f32,
}
impl TunableParam {
const fn new(value: f32, min_bound: f32, max_bound: f32, step_size: f32) -> Self {
Self {
value,
min_bound,
max_bound,
step_size,
prev_value: value,
}
}
/// Clamp value to bounds.
fn clamp(&mut self) {
if self.value < self.min_bound {
self.value = self.min_bound;
}
if self.value > self.max_bound {
self.value = self.max_bound;
}
}
}
/// Optimization phase state.
#[derive(Clone, Copy, Debug, PartialEq)]
enum OptPhase {
/// Baseline measurement — collecting score before perturbation.
Baseline,
/// A parameter has been perturbed; evaluating the result.
Evaluating,
}
/// Meta-learning parameter optimizer.
pub struct MetaAdapter {
/// Tunable parameters.
params: [TunableParam; NUM_PARAMS],
/// Snapshot of all parameter values before any perturbation chain
/// (used for safety rollback).
rollback_snapshot: [f32; NUM_PARAMS],
/// Current optimization phase.
phase: OptPhase,
/// Index of the parameter currently being perturbed.
current_param: usize,
/// Direction of current perturbation (+1 or -1).
perturb_direction: i8,
/// Baseline performance score (before perturbation).
baseline_score: f32,
/// Current accumulated performance score.
current_score: f32,
/// Event feedback accumulators (reset each evaluation window).
true_positives: u16,
false_positives: u16,
total_events: u16,
/// Ticks elapsed in the current evaluation window.
eval_ticks: u16,
/// Consecutive failed perturbations (score did not improve).
consecutive_failures: u8,
/// Total perturbation iterations.
iteration_count: u32,
/// Total successful adaptations.
success_count: u32,
/// Meta-level: increases with each full parameter sweep, represents
/// how many optimization rounds have completed.
meta_level: u16,
/// Counter within a sweep (0..NUM_PARAMS).
sweep_idx: usize,
}
impl MetaAdapter {
/// Create a new meta-adapter with default parameter configuration.
///
/// Default parameters (indices correspond to sensing thresholds):
/// 0: presence_threshold (0.05, range 0.01-0.5)
/// 1: motion_threshold (0.10, range 0.02-1.0)
/// 2: coherence_threshold (0.70, range 0.3-0.99)
/// 3: gesture_dtw_threshold (2.50, range 0.5-5.0)
/// 4: anomaly_energy_ratio (50.0, range 10.0-200.0)
/// 5: zone_occupancy_thresh (0.02, range 0.005-0.1)
/// 6: vital_apnea_seconds (20.0, range 10.0-60.0)
/// 7: intrusion_sensitivity (0.30, range 0.05-0.9)
pub const fn new() -> Self {
Self {
params: [
TunableParam::new(0.05, 0.01, 0.50, 0.01),
TunableParam::new(0.10, 0.02, 1.00, 0.02),
TunableParam::new(0.70, 0.30, 0.99, 0.02),
TunableParam::new(2.50, 0.50, 5.00, 0.20),
TunableParam::new(50.0, 10.0, 200.0, 5.0),
TunableParam::new(0.02, 0.005, 0.10, 0.005),
TunableParam::new(20.0, 10.0, 60.0, 2.0),
TunableParam::new(0.30, 0.05, 0.90, 0.03),
],
rollback_snapshot: [0.05, 0.10, 0.70, 2.50, 50.0, 0.02, 20.0, 0.30],
phase: OptPhase::Baseline,
current_param: 0,
perturb_direction: 1,
baseline_score: 0.0,
current_score: 0.0,
true_positives: 0,
false_positives: 0,
total_events: 0,
eval_ticks: 0,
consecutive_failures: 0,
iteration_count: 0,
success_count: 0,
meta_level: 0,
sweep_idx: 0,
}
}
/// Report a true positive event (correct detection confirmed by context).
pub fn report_true_positive(&mut self) {
self.true_positives = self.true_positives.saturating_add(1);
self.total_events = self.total_events.saturating_add(1);
}
/// Report a false positive event (detection that should not have fired).
pub fn report_false_positive(&mut self) {
self.false_positives = self.false_positives.saturating_add(1);
self.total_events = self.total_events.saturating_add(1);
}
/// Report a generic event (for total count normalization).
pub fn report_event(&mut self) {
self.total_events = self.total_events.saturating_add(1);
}
/// Get the current value of a parameter by index.
pub fn get_param(&self, idx: usize) -> f32 {
if idx < NUM_PARAMS {
self.params[idx].value
} else {
0.0
}
}
/// Called on timer (typically 1 Hz). Drives the optimization loop.
///
/// Returns events as `(event_id, value)` pairs.
pub fn on_timer(&mut self) -> &[(i32, f32)] {
static mut EVENTS: [(i32, f32); 4] = [(0, 0.0); 4];
let mut n_ev = 0usize;
self.eval_ticks += 1;
// ── Compute current performance score ────────────────────────────
let score = self.compute_score();
self.current_score = score;
match self.phase {
OptPhase::Baseline => {
if self.eval_ticks >= EVAL_WINDOW {
// Record baseline score and apply perturbation.
self.baseline_score = score;
self.apply_perturbation();
self.reset_accumulators();
self.phase = OptPhase::Evaluating;
}
}
OptPhase::Evaluating => {
if self.eval_ticks >= EVAL_WINDOW {
self.iteration_count += 1;
let improved = score > self.baseline_score;
if improved {
// Keep the perturbation.
self.consecutive_failures = 0;
self.success_count += 1;
unsafe {
EVENTS[n_ev] = (
EVENT_PARAM_ADJUSTED,
self.current_param as f32
+ self.params[self.current_param].value / 1000.0,
);
n_ev += 1;
EVENTS[n_ev] = (EVENT_ADAPTATION_SCORE, score);
n_ev += 1;
}
} else {
// Revert the perturbation.
self.params[self.current_param].value =
self.params[self.current_param].prev_value;
self.consecutive_failures += 1;
}
// ── Safety rollback ──────────────────────────────────
if self.consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
self.safety_rollback();
unsafe {
EVENTS[n_ev] = (EVENT_ROLLBACK_TRIGGERED, self.meta_level as f32);
n_ev += 1;
}
}
// ── Advance to next parameter ────────────────────────
self.advance_sweep();
self.reset_accumulators();
self.phase = OptPhase::Baseline;
// ── Emit meta level periodically ─────────────────────
if self.sweep_idx == 0 && n_ev < 4 {
unsafe {
EVENTS[n_ev] = (EVENT_META_LEVEL, self.meta_level as f32);
n_ev += 1;
}
}
}
}
}
unsafe { &EVENTS[..n_ev] }
}
/// Compute the performance score from accumulated feedback.
fn compute_score(&self) -> f32 {
if self.total_events == 0 {
return 0.0;
}
let total = self.total_events as f32;
let tp_rate = self.true_positives as f32 / total;
let fp_rate = self.false_positives as f32 / total;
tp_rate - 2.0 * fp_rate
}
/// Apply a perturbation to the current parameter.
fn apply_perturbation(&mut self) {
let p = &mut self.params[self.current_param];
p.prev_value = p.value;
let delta = p.step_size * self.perturb_direction as f32;
p.value += delta;
p.clamp();
// Alternate perturbation direction each iteration.
self.perturb_direction = if self.perturb_direction > 0 { -1 } else { 1 };
}
/// Advance to the next parameter in the sweep.
fn advance_sweep(&mut self) {
self.sweep_idx += 1;
if self.sweep_idx >= NUM_PARAMS {
self.sweep_idx = 0;
self.meta_level = self.meta_level.saturating_add(1);
// Take a new rollback snapshot after a successful sweep.
self.snapshot_params();
}
self.current_param = self.sweep_idx;
}
/// Reset evaluation accumulators for the next window.
fn reset_accumulators(&mut self) {
self.true_positives = 0;
self.false_positives = 0;
self.total_events = 0;
self.eval_ticks = 0;
}
/// Take a snapshot of current parameter values for rollback.
fn snapshot_params(&mut self) {
for i in 0..NUM_PARAMS {
self.rollback_snapshot[i] = self.params[i].value;
}
}
/// Safety rollback: restore all parameters to the last known-good snapshot.
fn safety_rollback(&mut self) {
for i in 0..NUM_PARAMS {
self.params[i].value = self.rollback_snapshot[i];
self.params[i].prev_value = self.rollback_snapshot[i];
}
self.consecutive_failures = 0;
// Reset sweep to start fresh.
self.sweep_idx = 0;
self.current_param = 0;
}
/// Total number of optimization iterations completed.
pub fn iteration_count(&self) -> u32 {
self.iteration_count
}
/// Total number of successful parameter adaptations.
pub fn success_count(&self) -> u32 {
self.success_count
}
/// Current meta-level (number of complete sweeps).
pub fn meta_level(&self) -> u16 {
self.meta_level
}
/// Current consecutive failure count.
pub fn consecutive_failures(&self) -> u8 {
self.consecutive_failures
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_state() {
let ma = MetaAdapter::new();
assert_eq!(ma.iteration_count(), 0);
assert_eq!(ma.success_count(), 0);
assert_eq!(ma.meta_level(), 0);
assert_eq!(ma.consecutive_failures(), 0);
}
#[test]
fn test_default_params() {
let ma = MetaAdapter::new();
assert!((ma.get_param(0) - 0.05).abs() < 0.001); // presence_threshold
assert!((ma.get_param(1) - 0.10).abs() < 0.001); // motion_threshold
assert!((ma.get_param(2) - 0.70).abs() < 0.001); // coherence_threshold
assert!((ma.get_param(3) - 2.50).abs() < 0.001); // gesture_dtw_threshold
assert!((ma.get_param(7) - 0.30).abs() < 0.001); // intrusion_sensitivity
assert_eq!(ma.get_param(99), 0.0); // out-of-range
}
#[test]
fn test_score_computation() {
let mut ma = MetaAdapter::new();
// 8 TP, 1 FP, 1 generic event = 10 total.
for _ in 0..8 {
ma.report_true_positive();
}
ma.report_false_positive();
ma.report_event();
let score = ma.compute_score();
// tp_rate = 8/10 = 0.8, fp_rate = 1/10 = 0.1
// score = 0.8 - 2*0.1 = 0.6
assert!((score - 0.6).abs() < 0.01, "score should be ~0.6, got {}", score);
}
#[test]
fn test_score_all_false_positives() {
let mut ma = MetaAdapter::new();
for _ in 0..10 {
ma.report_false_positive();
}
let score = ma.compute_score();
// tp_rate = 0, fp_rate = 1.0 => score = -2.0
assert!(score < -1.0, "all-FP score should be very negative");
}
#[test]
fn test_score_empty() {
let ma = MetaAdapter::new();
assert_eq!(ma.compute_score(), 0.0);
}
#[test]
fn test_param_clamping() {
let mut p = TunableParam::new(0.5, 0.1, 0.9, 0.1);
p.value = 1.5;
p.clamp();
assert!((p.value - 0.9).abs() < 0.001);
p.value = -0.5;
p.clamp();
assert!((p.value - 0.1).abs() < 0.001);
}
#[test]
fn test_optimization_cycle() {
let mut ma = MetaAdapter::new();
// Run baseline phase.
for _ in 0..EVAL_WINDOW {
ma.report_true_positive();
ma.on_timer();
}
// Should now be in Evaluating phase.
assert_eq!(ma.phase, OptPhase::Evaluating);
// Run evaluation phase with good feedback.
for _ in 0..EVAL_WINDOW {
ma.report_true_positive();
ma.on_timer();
}
// Should have completed one iteration.
assert_eq!(ma.iteration_count(), 1);
}
#[test]
fn test_safety_rollback() {
let mut ma = MetaAdapter::new();
let original_val = ma.get_param(0);
// Manually trigger consecutive failures.
ma.consecutive_failures = MAX_CONSECUTIVE_FAILURES;
ma.safety_rollback();
assert_eq!(ma.consecutive_failures(), 0);
assert!((ma.get_param(0) - original_val).abs() < 0.001);
}
#[test]
fn test_full_sweep_increments_meta_level() {
let mut ma = MetaAdapter::new();
ma.sweep_idx = NUM_PARAMS - 1;
ma.advance_sweep();
assert_eq!(ma.meta_level(), 1);
assert_eq!(ma.sweep_idx, 0);
}
}