use std::collections::VecDeque;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TdErrorStats {
pub history: VecDeque<f32>,
pub mean_recent: f32,
pub std_dev_recent: f32,
pub convergence_ratio: f32,
pub is_monotonic_decreasing: bool,
pub monotonicity_violations: usize,
}
impl Default for TdErrorStats {
fn default() -> Self {
Self {
history: VecDeque::with_capacity(100),
mean_recent: 0.0,
std_dev_recent: 0.0,
convergence_ratio: 1.0,
is_monotonic_decreasing: false,
monotonicity_violations: 0,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct QValueDivergenceMonitor {
pub max_q_value: f32,
pub max_q_cycle: u64,
pub is_diverging: bool,
pub max_q_history: VecDeque<f32>,
}
impl Default for QValueDivergenceMonitor {
fn default() -> Self {
Self {
max_q_value: 0.0,
max_q_cycle: 0,
is_diverging: false,
max_q_history: VecDeque::with_capacity(50),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LearningCurveSmoothness {
pub reward_history: VecDeque<f32>,
pub jump_count: usize,
pub last_jump_cycle: u64,
pub is_chaotic: bool,
pub mean_delta: f32,
}
impl Default for LearningCurveSmoothness {
fn default() -> Self {
Self {
reward_history: VecDeque::with_capacity(100),
jump_count: 0,
last_jump_cycle: 0,
is_chaotic: false,
mean_delta: 0.0,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RewardScalingValidator {
pub mean_reward: f32,
pub std_dev_reward: f32,
pub has_extreme_outliers: bool,
pub outlier_count: usize,
pub is_in_documented_range: bool,
}
impl Default for RewardScalingValidator {
fn default() -> Self {
Self {
mean_reward: 0.0,
std_dev_reward: 0.0,
has_extreme_outliers: false,
outlier_count: 0,
is_in_documented_range: true,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LearningRateDecayMonitor {
pub alpha_0: f32,
pub cycle_count: u64,
pub alpha_current: f32,
pub alpha_expected: f32,
pub schedule_is_correct: bool,
}
impl LearningRateDecayMonitor {
pub fn new(alpha_0: f32) -> Self {
Self {
alpha_0,
cycle_count: 0,
alpha_current: alpha_0,
alpha_expected: alpha_0,
schedule_is_correct: true,
}
}
pub fn update(&mut self, cycle_count: u64, alpha_measured: f32) {
self.cycle_count = cycle_count;
self.alpha_current = alpha_measured;
self.alpha_expected = self.alpha_0 * 0.9999_f32.powf(cycle_count as f32);
let tolerance = self.alpha_expected * 0.02;
self.schedule_is_correct = (self.alpha_current - self.alpha_expected).abs() < tolerance;
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RlStabilityMonitor {
pub td_error_stats: TdErrorStats,
pub q_divergence: QValueDivergenceMonitor,
pub learning_curve: LearningCurveSmoothness,
pub reward_scaling: RewardScalingValidator,
pub learning_rate_decay: LearningRateDecayMonitor,
}
impl RlStabilityMonitor {
pub fn new(alpha_0: f32) -> Self {
Self {
td_error_stats: TdErrorStats::default(),
q_divergence: QValueDivergenceMonitor::default(),
learning_curve: LearningCurveSmoothness::default(),
reward_scaling: RewardScalingValidator::default(),
learning_rate_decay: LearningRateDecayMonitor::new(alpha_0),
}
}
pub fn record_td_error(&mut self, td_error: f32) {
let abs_error = td_error.abs();
if let Some(&last_error) = self.td_error_stats.history.back() {
if abs_error > last_error {
self.td_error_stats.monotonicity_violations += 1;
}
}
if self.td_error_stats.history.len() >= 100 {
self.td_error_stats.history.pop_front();
}
self.td_error_stats.history.push_back(abs_error);
self.recompute_td_error_stats();
}
pub fn record_max_q_value(&mut self, max_q: f32, cycle_count: u64) {
if max_q > self.q_divergence.max_q_value {
self.q_divergence.max_q_value = max_q;
self.q_divergence.max_q_cycle = cycle_count;
}
if self.q_divergence.max_q_history.len() >= 50 {
self.q_divergence.max_q_history.pop_front();
}
self.q_divergence.max_q_history.push_back(max_q);
if self.q_divergence.max_q_history.len() >= 10 {
let recent_start = self.q_divergence.max_q_history[0];
let recent_max = *self
.q_divergence
.max_q_history
.iter()
.max_by(|a, b| a.total_cmp(b))
.unwrap_or(&recent_start);
if recent_start > 0.0 && (recent_max - recent_start) / recent_start > 0.5 {
self.q_divergence.is_diverging = true;
}
}
}
pub fn record_reward(&mut self, cumulative_reward: f32) {
if self.learning_curve.reward_history.len() >= 100 {
self.learning_curve.reward_history.pop_front();
}
self.learning_curve
.reward_history
.push_back(cumulative_reward);
if self.learning_curve.reward_history.len() >= 2 {
let prev =
self.learning_curve.reward_history[self.learning_curve.reward_history.len() - 2];
let curr = cumulative_reward;
let delta = (curr - prev).abs();
let mut sum_deltas = 0.0;
let mut count = 0;
for i in 1..self.learning_curve.reward_history.len() {
let d = (self.learning_curve.reward_history[i]
- self.learning_curve.reward_history[i - 1])
.abs();
sum_deltas += d;
count += 1;
}
self.learning_curve.mean_delta = if count > 0 {
sum_deltas / count as f32
} else {
0.0
};
if self.learning_curve.mean_delta > 0.0 && delta > 2.0 * self.learning_curve.mean_delta
{
self.learning_curve.jump_count += 1;
}
if self.learning_curve.reward_history.len() >= 5 {
let chaos_threshold =
(self.learning_curve.reward_history.len() as f32 * 0.2).ceil() as usize;
self.learning_curve.is_chaotic = self.learning_curve.jump_count > chaos_threshold;
}
}
}
pub fn validate_reward_scaling(&mut self, reward: f32) {
const MIN_REWARD: f32 = -5.5;
const MAX_REWARD: f32 = 1.6;
if reward < MIN_REWARD || reward > MAX_REWARD {
self.reward_scaling.has_extreme_outliers = true;
self.reward_scaling.outlier_count += 1;
self.reward_scaling.is_in_documented_range = false;
}
}
pub fn is_stable(&self) -> bool {
!self.td_error_stats.is_monotonic_decreasing && !self.q_divergence.is_diverging
&& !self.learning_curve.is_chaotic
&& !self.reward_scaling.has_extreme_outliers
}
fn recompute_td_error_stats(&mut self) {
if self.td_error_stats.history.is_empty() {
return;
}
let len = self.td_error_stats.history.len();
let recent_len = std::cmp::min(10, len);
let recent_start = len.saturating_sub(recent_len);
let recent_sum: f32 = self.td_error_stats.history.iter().skip(recent_start).sum();
self.td_error_stats.mean_recent = recent_sum / recent_len as f32;
let variance = self
.td_error_stats
.history
.iter()
.skip(recent_start)
.map(|&x| (x - self.td_error_stats.mean_recent).powi(2))
.sum::<f32>()
/ recent_len as f32;
self.td_error_stats.std_dev_recent = variance.sqrt();
if len >= 20 {
let first_sum: f32 = self.td_error_stats.history.iter().take(10).sum();
let first_mean = first_sum / 10.0;
if first_mean > 0.0 {
self.td_error_stats.convergence_ratio =
self.td_error_stats.mean_recent / first_mean;
}
}
let violation_threshold = (len as f32 * 0.05).ceil() as usize;
self.td_error_stats.is_monotonic_decreasing =
self.td_error_stats.monotonicity_violations < violation_threshold;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_td_error_monotonicity_detection() {
let mut monitor = RlStabilityMonitor::new(0.1);
for i in 0..10 {
monitor.record_td_error(1.0 - i as f32 * 0.1);
}
assert!(
monitor.td_error_stats.is_monotonic_decreasing,
"Monotonically decreasing TD errors should be flagged"
);
assert!(monitor.td_error_stats.monotonicity_violations == 0);
}
#[test]
fn test_q_value_divergence_alarm() {
let mut monitor = RlStabilityMonitor::new(0.1);
monitor.record_max_q_value(1.0, 0);
for i in 1..15 {
let q = 1.0 + (i as f32 * 0.06); monitor.record_max_q_value(q, i as u64);
}
assert!(
monitor.q_divergence.is_diverging,
"Q-values growing >50% in 50-sample window should trigger divergence alarm"
);
}
#[test]
fn test_learning_rate_decay_schedule() {
let mut monitor = RlStabilityMonitor::new(0.1);
monitor.learning_rate_decay.update(0, 0.1);
assert!(monitor.learning_rate_decay.schedule_is_correct);
monitor.learning_rate_decay.update(1000, 0.0905);
assert!(monitor.learning_rate_decay.schedule_is_correct);
monitor.learning_rate_decay.update(10000, 0.0368);
assert!(monitor.learning_rate_decay.schedule_is_correct);
}
#[test]
fn test_reward_scaling_validation() {
let mut monitor = RlStabilityMonitor::new(0.1);
monitor.validate_reward_scaling(0.0);
monitor.validate_reward_scaling(1.0);
monitor.validate_reward_scaling(-1.0);
assert!(!monitor.reward_scaling.has_extreme_outliers);
monitor.validate_reward_scaling(10.0);
assert!(monitor.reward_scaling.has_extreme_outliers);
assert_eq!(monitor.reward_scaling.outlier_count, 1);
}
#[test]
fn test_learning_curve_smoothness() {
let mut monitor = RlStabilityMonitor::new(0.1);
for i in 0..30 {
monitor.record_reward(i as f32 * 0.1);
}
assert!(!monitor.learning_curve.is_chaotic);
monitor.record_reward(100.0);
assert!(monitor.learning_curve.is_chaotic || monitor.learning_curve.jump_count > 0);
}
}