use super::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default, deny_unknown_fields)]
pub struct ResidualTrendSettings {
pub window: usize,
pub stable_windows: usize,
pub minimum_attempts: usize,
pub absolute_tolerance: f64,
pub relative_best_tolerance: f64,
pub relative_mean_tolerance: f64,
}
impl Default for ResidualTrendSettings {
fn default() -> Self {
Self {
window: 500,
stable_windows: 3,
minimum_attempts: 3000,
absolute_tolerance: 1e-5,
relative_best_tolerance: 0.005,
relative_mean_tolerance: 0.01,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ResidualTrendStatus {
InsufficientHistory,
StillChanging,
ResidualPlateau,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ResidualWindow {
pub first_step: usize,
pub last_step: usize,
pub mean_score: f64,
pub best_score: f64,
pub previous_best: Option<f64>,
pub previous_mean: Option<f64>,
pub best_improvement: Option<f64>,
pub mean_change: Option<f64>,
pub relative_best_improvement: Option<f64>,
pub relative_mean_change: Option<f64>,
pub flat: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ResidualTrendReport {
pub settings: ResidualTrendSettings,
pub completed: usize,
pub status: ResidualTrendStatus,
pub windows: Vec<ResidualWindow>,
}
pub fn residual_trend(
history: &[SessionStep],
settings: &ResidualTrendSettings,
) -> Result<ResidualTrendReport, RmcError> {
assess(
&history
.iter()
.map(|r| TrendSample {
step: r.step,
score: r.score,
best_score: r.best_score,
})
.collect::<Vec<_>>(),
settings,
)
}
struct TrendSample {
step: usize,
score: f64,
best_score: f64,
}
pub fn evolution_residual_trend(
history: &[EvolutionGeneration],
settings: &ResidualTrendSettings,
) -> Result<ResidualTrendReport, RmcError> {
let start = history
.iter()
.rposition(|r| r.mean_score.is_none())
.map_or(0, |i| i + 1);
let samples: Vec<_> = history[start..]
.iter()
.map(|r| TrendSample {
step: r.generation,
score: r.mean_score.unwrap(),
best_score: r.best_score,
})
.collect();
let mut report = assess(&samples, settings)?;
report.completed = history.last().map_or(0, |r| r.generation);
Ok(report)
}
fn assess(
history: &[TrendSample],
settings: &ResidualTrendSettings,
) -> Result<ResidualTrendReport, RmcError> {
require(
(2..=100_000).contains(&settings.window)
&& (1..=20).contains(&settings.stable_windows)
&& [
settings.absolute_tolerance,
settings.relative_best_tolerance,
settings.relative_mean_tolerance,
]
.iter()
.all(|v| v.is_finite() && *v >= 0.),
"invalid residual trend settings",
)?;
require(
history
.iter()
.all(|h| h.score.is_finite() && h.best_score.is_finite() && h.best_score <= h.score)
&& history.windows(2).all(|h| {
h[1].step.checked_sub(h[0].step) == Some(1) && h[1].best_score <= h[0].best_score
}),
"residual trend needs finite consecutive history with valid best scores",
)?;
let count = (history.len() / settings.window).min(settings.stable_windows + 1);
let start = history.len() - count * settings.window;
let mut windows: Vec<ResidualWindow> = Vec::new();
for index in 0..count {
let from = start + index * settings.window;
let to = from + settings.window;
let samples = &history[from..to];
let mean = samples
.iter()
.map(|s| s.score / settings.window as f64)
.sum::<f64>();
let best = samples.last().unwrap().best_score;
let previous_best = from.checked_sub(1).map(|i| history[i].best_score);
let previous_mean = windows.last().map(|w| w.mean_score);
let improvement = previous_best.map(|v| v - best);
let change = previous_mean.map(|v| mean - v);
require(
mean.is_finite()
&& improvement.is_none_or(f64::is_finite)
&& change.is_none_or(f64::is_finite),
"residual trend overflow",
)?;
windows.push(ResidualWindow {
first_step: samples[0].step,
last_step: samples.last().unwrap().step,
mean_score: mean,
best_score: best,
previous_best,
previous_mean,
best_improvement: improvement,
mean_change: change,
relative_best_improvement: previous_best
.filter(|v| *v != 0.)
.map(|v| (v - best) / v.abs()),
relative_mean_change: previous_mean
.filter(|v| *v != 0.)
.map(|v| (mean - v) / v.abs()),
flat: previous_best.zip(previous_mean).map(|(b, m)| {
b - best <= settings.absolute_tolerance + settings.relative_best_tolerance * b.abs()
&& (mean - m).abs()
<= settings.absolute_tolerance + settings.relative_mean_tolerance * m.abs()
}),
});
}
let completed = history.last().map_or(0, |h| h.step);
let status = if completed < settings.minimum_attempts || count < settings.stable_windows + 1 {
ResidualTrendStatus::InsufficientHistory
} else if windows.iter().skip(1).all(|w| w.flat == Some(true)) {
ResidualTrendStatus::ResidualPlateau
} else {
ResidualTrendStatus::StillChanging
};
Ok(ResidualTrendReport {
settings: settings.clone(),
completed,
status,
windows,
})
}