converge_analytics/packs/forecasting/
solver.rs1use super::types::*;
2use converge_pack::gate::GateResult as Result;
3use converge_pack::gate::{ProblemSpec, ReplayEnvelope, SolverReport};
4
5pub struct ExponentialSmoothingSolver;
6
7impl ExponentialSmoothingSolver {
8 pub fn solve(
9 &self,
10 input: &ForecastingInput,
11 spec: &ProblemSpec,
12 ) -> Result<(ForecastingOutput, SolverReport)> {
13 let alpha = input.alpha;
14 let values = &input.values;
15 let n = values.len();
16
17 let mut level = values[0];
19 let mut residuals = Vec::with_capacity(n - 1);
20
21 for &v in &values[1..] {
22 let forecast = level;
23 residuals.push(v - forecast);
24 level = alpha * v + (1.0 - alpha) * level;
25 }
26
27 let residual_std = if residuals.is_empty() {
29 0.0
30 } else {
31 let mean_r = residuals.iter().sum::<f64>() / residuals.len() as f64;
32 let var = residuals.iter().map(|r| (r - mean_r).powi(2)).sum::<f64>()
33 / residuals.len() as f64;
34 var.sqrt()
35 };
36
37 let predictions: Vec<ForecastPoint> = (1..=input.horizon)
39 .map(|step| {
40 let width = 1.96 * residual_std * (step as f64).sqrt();
41 ForecastPoint {
42 step,
43 value: level,
44 lower: level - width,
45 upper: level + width,
46 }
47 })
48 .collect();
49
50 let output = ForecastingOutput {
51 predictions,
52 residual_std,
53 horizon: input.horizon,
54 };
55
56 let replay = ReplayEnvelope::minimal(spec.seed());
57 let confidence = if residual_std > 0.0 {
58 (1.0 / (1.0 + residual_std)).clamp(0.3, 0.95)
59 } else {
60 0.95
61 };
62 let report = SolverReport::optimal("exponential-smoothing-v1", confidence, replay);
63
64 Ok((output, report))
65 }
66}