use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExhaustionThresholds {
pub normal: f32,
pub elevated: f32,
pub exhausted: f32,
}
impl Default for ExhaustionThresholds {
fn default() -> Self {
Self {
normal: 0.7,
elevated: 0.4,
exhausted: 0.1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExhaustionLevel {
Normal,
Elevated,
Exhausted,
Emergency,
}
impl ExhaustionLevel {
pub fn from_energy(energy: f32) -> Self {
Self::from_energy_with_thresholds(energy, &ExhaustionThresholds::default())
}
pub fn from_energy_with_thresholds(energy: f32, thresholds: &ExhaustionThresholds) -> Self {
match energy {
e if e > thresholds.normal => ExhaustionLevel::Normal,
e if e > thresholds.elevated => ExhaustionLevel::Elevated,
e if e > thresholds.exhausted => ExhaustionLevel::Exhausted,
_ => ExhaustionLevel::Emergency,
}
}
pub fn allows_writes(&self) -> bool {
matches!(self, ExhaustionLevel::Normal | ExhaustionLevel::Elevated)
}
pub fn allows_operations(&self) -> bool {
!matches!(self, ExhaustionLevel::Emergency)
}
}
pub struct ExhaustionMonitor {
pub recovery_rate: f32,
pub operation_cost: f32,
}
impl Default for ExhaustionMonitor {
fn default() -> Self {
Self {
recovery_rate: 0.05,
operation_cost: 0.001,
}
}
}
impl ExhaustionMonitor {
pub fn new(recovery_rate: f32, operation_cost: f32) -> Self {
Self {
recovery_rate,
operation_cost,
}
}
pub fn calculate_cost(&self, is_write: bool, propagation_depth: usize) -> f32 {
let base = self.operation_cost;
let write_multiplier = if is_write { 2.0 } else { 1.0 };
let depth_multiplier = 1.0 + (propagation_depth as f32 * 0.1);
base * write_multiplier * depth_multiplier
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TunerConfig {
pub window_size: usize,
pub sensitivity: f32,
pub min_floor: f32,
pub hard_ceiling: f32,
pub warmup_ticks: usize,
}
impl Default for TunerConfig {
fn default() -> Self {
Self {
window_size: 100,
sensitivity: 3.0, min_floor: 0.1,
hard_ceiling: 0.9,
warmup_ticks: 50,
}
}
}
struct EMAAccumulator {
mean: f32,
variance: f32,
count: usize,
alpha: f32,
}
impl EMAAccumulator {
fn new(window_size: usize) -> Self {
let alpha = 2.0 / (window_size as f32 + 1.0);
Self {
mean: 0.0,
variance: 0.0,
count: 0,
alpha,
}
}
fn update(&mut self, x: f32) {
self.count += 1;
if self.count == 1 {
self.mean = x;
self.variance = 0.0;
return;
}
let delta = x - self.mean;
self.mean += self.alpha * delta;
self.variance = (1.0 - self.alpha) * (self.variance + self.alpha * delta * delta);
}
fn get_mean(&self) -> f32 {
self.mean
}
fn get_std_dev(&self) -> f32 {
self.variance.max(0.0).sqrt()
}
fn get_count(&self) -> usize {
self.count
}
fn reset(&mut self) {
self.mean = 0.0;
self.variance = 0.0;
self.count = 0;
}
}
#[derive(Debug, Clone)]
pub struct TunerStats {
pub mean: f32,
pub std_dev: f32,
pub sample_count: usize,
pub is_warmed_up: bool,
}
pub struct ExhaustionTuner {
config: TunerConfig,
accumulator: EMAAccumulator,
#[allow(dead_code)] fallback_threshold: f32,
}
impl ExhaustionTuner {
pub fn new(config: TunerConfig, fallback_threshold: f32) -> Self {
let accumulator = EMAAccumulator::new(config.window_size);
Self {
config,
accumulator,
fallback_threshold,
}
}
pub fn observe(&mut self, energy: f32) {
self.accumulator.update(energy);
}
pub fn get_stats(&self) -> TunerStats {
TunerStats {
mean: self.accumulator.get_mean(),
std_dev: self.accumulator.get_std_dev(),
sample_count: self.accumulator.get_count(),
is_warmed_up: self.accumulator.get_count() >= self.config.warmup_ticks,
}
}
pub fn compute_threshold(&self) -> ExhaustionThresholds {
let stats = self.get_stats();
if !stats.is_warmed_up {
return ExhaustionThresholds::default();
}
let normal = (stats.mean - self.config.sensitivity * 0.3 * stats.std_dev)
.clamp(self.config.min_floor, self.config.hard_ceiling);
let elevated = (stats.mean - self.config.sensitivity * 0.6 * stats.std_dev)
.clamp(self.config.min_floor, normal);
let exhausted = (stats.mean - self.config.sensitivity * stats.std_dev)
.clamp(self.config.min_floor, elevated);
ExhaustionThresholds {
normal,
elevated,
exhausted,
}
}
pub fn get_level(&self, energy: f32) -> ExhaustionLevel {
let thresholds = self.compute_threshold();
ExhaustionLevel::from_energy_with_thresholds(energy, &thresholds)
}
pub fn reset(&mut self) {
self.accumulator.reset();
}
}
impl Default for ExhaustionTuner {
fn default() -> Self {
Self::new(TunerConfig::default(), 0.5)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exhaustion_levels() {
assert_eq!(ExhaustionLevel::from_energy(1.0), ExhaustionLevel::Normal);
assert_eq!(ExhaustionLevel::from_energy(0.5), ExhaustionLevel::Elevated);
assert_eq!(
ExhaustionLevel::from_energy(0.2),
ExhaustionLevel::Exhausted
);
assert_eq!(
ExhaustionLevel::from_energy(0.05),
ExhaustionLevel::Emergency
);
}
#[test]
fn test_write_permissions() {
assert!(ExhaustionLevel::Normal.allows_writes());
assert!(ExhaustionLevel::Elevated.allows_writes());
assert!(!ExhaustionLevel::Exhausted.allows_writes());
assert!(!ExhaustionLevel::Emergency.allows_writes());
}
#[test]
fn test_tuner_warmup() {
let mut tuner = ExhaustionTuner::new(
TunerConfig {
warmup_ticks: 5,
..Default::default()
},
0.5,
);
assert!(!tuner.get_stats().is_warmed_up);
for _ in 0..5 {
tuner.observe(0.8);
}
assert!(tuner.get_stats().is_warmed_up);
}
#[test]
fn test_tuner_learns_pattern() {
let mut tuner = ExhaustionTuner::new(
TunerConfig {
warmup_ticks: 10,
window_size: 20,
..Default::default()
},
0.5,
);
for _ in 0..20 {
tuner.observe(0.85);
}
let stats = tuner.get_stats();
assert!(stats.mean > 0.8);
assert!(stats.std_dev < 0.1); }
}