Skip to main content

linkrs_query/optimizer/stats/feedback/
factor.rs

1//! Generic feedback-driven correction factor
2//!
3//! Corrects an estimated value (selectivity, row count, ...) using an
4//! exponentially weighted moving average (EWMA) of observed
5//! estimated-vs-actual ratios.  The correction factor starts at 1.0 and is
6//! clamped to a configurable range to avoid over-correction.
7
8/// EWMA-driven correction factor for a single estimated value.
9#[derive(Debug, Clone)]
10pub struct FeedbackDrivenFactor {
11    /// Original (un-corrected) estimate.
12    estimated: f64,
13    /// EWMA of the correction factor (actual / estimated).
14    correction_factor: f64,
15    /// Number of feedbacks received.
16    feedback_count: u64,
17    /// EWMA smoothing factor.
18    alpha: f64,
19    /// Minimum correction factor.
20    min_correction: f64,
21    /// Maximum correction factor.
22    max_correction: f64,
23    /// Cumulative estimation error (used for error statistics).
24    cumulative_estimation_error: f64,
25    /// Sum of the squared errors (used to calculate the standard deviation).
26    error_sum_squares: f64,
27}
28
29impl FeedbackDrivenFactor {
30    /// Create a factor for `estimated` with default parameters.
31    pub fn new(estimated: f64) -> Self {
32        Self {
33            estimated,
34            correction_factor: 1.0,
35            feedback_count: 0,
36            alpha: 0.3,
37            min_correction: 0.1,
38            max_correction: 10.0,
39            cumulative_estimation_error: 0.0,
40            error_sum_squares: 0.0,
41        }
42    }
43
44    /// Create a factor with custom parameters.
45    pub fn with_params(
46        estimated: f64,
47        alpha: f64,
48        min_correction: f64,
49        max_correction: f64,
50    ) -> Self {
51        Self {
52            estimated,
53            correction_factor: 1.0,
54            feedback_count: 0,
55            alpha,
56            min_correction,
57            max_correction,
58            cumulative_estimation_error: 0.0,
59            error_sum_squares: 0.0,
60        }
61    }
62
63    /// The uncorrected estimate.
64    pub fn estimated(&self) -> f64 {
65        self.estimated
66    }
67
68    /// Track a fresh uncorrected estimate while preserving the learned
69    /// correction factor and feedback history.
70    pub fn set_estimated(&mut self, estimated: f64) {
71        if estimated > 0.0 && estimated.is_finite() {
72            self.estimated = estimated;
73        }
74    }
75
76    /// The corrected value: `estimated * correction_factor`.
77    pub fn corrected(&self) -> f64 {
78        self.estimated * self.correction_factor
79    }
80
81    /// The current correction factor.
82    pub fn correction_factor(&self) -> f64 {
83        self.correction_factor
84    }
85
86    /// Number of feedbacks received.
87    pub fn feedback_count(&self) -> u64 {
88        self.feedback_count
89    }
90
91    /// Confidence in the correction, based on the feedback count.
92    ///
93    /// Sigmoid in the feedback count; close to 0.9 at 100 feedbacks.
94    pub fn estimation_confidence(&self) -> f64 {
95        let x = self.feedback_count as f64 * 0.1;
96        1.0 / (1.0 + (-x).exp())
97    }
98
99    /// Average estimation error of the correction factor.
100    pub fn avg_estimation_error(&self) -> f64 {
101        if self.feedback_count == 0 {
102            return 1.0;
103        }
104        self.cumulative_estimation_error / self.feedback_count as f64
105    }
106
107    /// Standard deviation of the correction-factor errors.
108    pub fn error_std_dev(&self) -> f64 {
109        if self.feedback_count < 2 {
110            return 0.0;
111        }
112        let n = self.feedback_count as f64;
113        let mean = self.cumulative_estimation_error / n;
114        let variance = (self.error_sum_squares / n) - (mean * mean);
115        variance.max(0.0).sqrt()
116    }
117
118    /// Update the correction factor from an observed estimated-vs-actual
119    /// ratio (`actual / estimated`), smoothed with EWMA.
120    pub fn update_with_ratio(&mut self, ratio: f64) {
121        if self.estimated <= 0.0 || !ratio.is_finite() {
122            return;
123        }
124        let error = (ratio - self.correction_factor).abs();
125        self.cumulative_estimation_error += error;
126        self.error_sum_squares += error * error;
127        self.correction_factor = (1.0 - self.alpha) * self.correction_factor + self.alpha * ratio;
128        self.correction_factor = self
129            .correction_factor
130            .clamp(self.min_correction, self.max_correction);
131        self.feedback_count += 1;
132    }
133
134    /// Reset the correction.
135    pub fn reset(&mut self) {
136        self.correction_factor = 1.0;
137        self.feedback_count = 0;
138        self.cumulative_estimation_error = 0.0;
139        self.error_sum_squares = 0.0;
140    }
141
142    /// Set the EWMA smoothing factor.
143    pub fn set_alpha(&mut self, alpha: f64) {
144        self.alpha = alpha.clamp(0.0, 1.0);
145    }
146
147    /// Set the correction range.
148    pub fn set_correction_range(&mut self, min: f64, max: f64) {
149        self.min_correction = min.max(0.01);
150        self.max_correction = max.max(self.min_correction);
151    }
152}
153
154impl Default for FeedbackDrivenFactor {
155    fn default() -> Self {
156        Self::new(1.0)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_correction_converges_to_actual() {
166        let mut factor = FeedbackDrivenFactor::new(100.0);
167        for _ in 0..200 {
168            factor.update_with_ratio(2.0); // actual is twice the estimate
169        }
170        assert!(factor.correction_factor() > 1.8);
171        assert!((factor.corrected() - 200.0).abs() < 25.0);
172    }
173
174    #[test]
175    fn test_correction_is_clamped() {
176        let mut factor = FeedbackDrivenFactor::with_params(100.0, 0.3, 0.1, 10.0);
177        for _ in 0..100 {
178            factor.update_with_ratio(100.0);
179        }
180        assert!(factor.correction_factor() <= 10.0);
181    }
182
183    #[test]
184    fn test_confidence_grows_with_feedback() {
185        let mut factor = FeedbackDrivenFactor::new(1.0);
186        assert!(factor.estimation_confidence() < 0.55);
187        for i in 0..100 {
188            factor.update_with_ratio(1.0 + i as f64 * 0.001);
189        }
190        assert!(factor.estimation_confidence() > 0.9);
191    }
192
193    #[test]
194    fn test_ignores_invalid_ratios() {
195        let mut factor = FeedbackDrivenFactor::new(10.0);
196        factor.update_with_ratio(f64::NAN);
197        factor.update_with_ratio(f64::INFINITY);
198        assert_eq!(factor.feedback_count(), 0);
199    }
200}