mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 05:59:32 +00:00
Complete implementation of all 24 vendor-integrated sensing modules across 7 categories, compiled to wasm32-unknown-unknown for ESP32-S3 WASM3 runtime deployment. All 243 unit tests pass. Signal Intelligence (6): flash attention, coherence gate, temporal compress, sparse recovery, min-cut person match, optimal transport. Adaptive Learning (4): DTW gesture learn, anomaly attractor, meta adapt, EWC++ lifelong learning. Spatial Reasoning (3): PageRank influence, micro-HNSW, spiking tracker. Temporal Analysis (3): pattern sequence, temporal logic guard, GOAP. AI Security (2): prompt shield, behavioral profiler. Quantum-Inspired (2): quantum coherence, interference search. Autonomous Systems (2): psycho-symbolic engine, self-healing mesh. Exotic (2): time crystal detector, hyperbolic space embedding. Includes vendor_common.rs shared library, security audit with 5 fixes, and security audit report. Co-Authored-By: claude-flow <ruv@ruv.net>
471 lines
16 KiB
Rust
471 lines
16 KiB
Rust
//! 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);
|
|
}
|
|
}
|