#[cfg(not(feature = "std"))]
use alloc::collections::VecDeque;
use core::fmt::Debug;
use num_traits::Float;
#[cfg(feature = "std")]
use std::collections::VecDeque;
use crate::algorithms::regression::{WLSSolver, ZeroWeightFallback};
use crate::algorithms::robustness::RobustnessMethod;
use crate::engine::executor::{CVPassFn, FitPassFn, IntervalPassFn, SmoothPassFn};
use crate::engine::executor::{LowessConfig, LowessExecutor};
use crate::engine::validator::Validator;
use crate::math::boundary::BoundaryPolicy;
use crate::math::kernel::WeightFunction;
use crate::math::scaling::ScalingMethod;
use crate::primitives::backend::Backend;
use crate::primitives::buffer::{OnlineBuffer, VecExt};
use crate::primitives::errors::LowessError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UpdateMode {
Full,
#[default]
Incremental,
}
#[derive(Debug, Clone)]
pub struct OnlineLowessBuilder<T: Float> {
pub window_capacity: usize,
pub min_points: usize,
pub fraction: T,
pub delta: T,
pub iterations: usize,
pub auto_convergence: Option<T>,
pub weight_function: WeightFunction,
pub update_mode: UpdateMode,
pub robustness_method: RobustnessMethod,
pub zero_weight_fallback: ZeroWeightFallback,
pub boundary_policy: BoundaryPolicy,
pub scaling_method: ScalingMethod,
pub compute_residuals: bool,
pub return_robustness_weights: bool,
pub deferred_error: Option<LowessError>,
#[doc(hidden)]
pub custom_smooth_pass: Option<SmoothPassFn<T>>,
#[doc(hidden)]
pub custom_cv_pass: Option<CVPassFn<T>>,
#[doc(hidden)]
pub custom_interval_pass: Option<IntervalPassFn<T>>,
#[doc(hidden)]
pub custom_fit_pass: Option<FitPassFn<T>>,
#[doc(hidden)]
pub backend: Option<Backend>,
#[doc(hidden)]
pub parallel: Option<bool>,
#[doc(hidden)]
pub(crate) duplicate_param: Option<&'static str>,
}
impl<T: Float> Default for OnlineLowessBuilder<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Float> OnlineLowessBuilder<T> {
fn new() -> Self {
Self {
window_capacity: 1000,
min_points: 2,
fraction: T::from(0.2).unwrap(),
delta: T::zero(),
iterations: 1,
weight_function: WeightFunction::default(),
update_mode: UpdateMode::default(),
robustness_method: RobustnessMethod::default(),
zero_weight_fallback: ZeroWeightFallback::default(),
boundary_policy: BoundaryPolicy::default(),
scaling_method: ScalingMethod::default(),
compute_residuals: false,
return_robustness_weights: false,
auto_convergence: None,
deferred_error: None,
custom_smooth_pass: None,
custom_cv_pass: None,
custom_interval_pass: None,
custom_fit_pass: None,
backend: None,
parallel: None,
duplicate_param: None,
}
}
pub fn fraction(mut self, fraction: T) -> Self {
self.fraction = fraction;
self
}
pub fn iterations(mut self, iterations: usize) -> Self {
self.iterations = iterations;
self
}
pub fn delta(mut self, delta: T) -> Self {
self.delta = delta;
self
}
pub fn weight_function(mut self, wf: WeightFunction) -> Self {
self.weight_function = wf;
self
}
pub fn robustness_method(mut self, method: RobustnessMethod) -> Self {
self.robustness_method = method;
self
}
pub fn zero_weight_fallback(mut self, fallback: ZeroWeightFallback) -> Self {
self.zero_weight_fallback = fallback;
self
}
pub fn boundary_policy(mut self, policy: BoundaryPolicy) -> Self {
self.boundary_policy = policy;
self
}
pub fn auto_converge(mut self, tolerance: T) -> Self {
self.auto_convergence = Some(tolerance);
self
}
pub fn compute_residuals(mut self, enabled: bool) -> Self {
self.compute_residuals = enabled;
self
}
pub fn return_robustness_weights(mut self, enabled: bool) -> Self {
self.return_robustness_weights = enabled;
self
}
pub fn window_capacity(mut self, capacity: usize) -> Self {
self.window_capacity = capacity;
self
}
pub fn min_points(mut self, min: usize) -> Self {
self.min_points = min;
self
}
pub fn update_mode(mut self, mode: UpdateMode) -> Self {
self.update_mode = mode;
self
}
#[doc(hidden)]
pub fn custom_smooth_pass(mut self, pass: SmoothPassFn<T>) -> Self {
self.custom_smooth_pass = Some(pass);
self
}
#[doc(hidden)]
pub fn custom_cv_pass(mut self, pass: CVPassFn<T>) -> Self {
self.custom_cv_pass = Some(pass);
self
}
#[doc(hidden)]
pub fn custom_interval_pass(mut self, pass: IntervalPassFn<T>) -> Self {
self.custom_interval_pass = Some(pass);
self
}
#[doc(hidden)]
pub fn backend(mut self, backend: Backend) -> Self {
self.backend = Some(backend);
self
}
#[doc(hidden)]
pub fn parallel(mut self, parallel: bool) -> Self {
self.parallel = Some(parallel);
self
}
pub fn build(self) -> Result<OnlineLowess<T>, LowessError> {
if let Some(err) = self.deferred_error {
return Err(err);
}
Validator::validate_no_duplicates(self.duplicate_param)?;
Validator::validate_fraction(self.fraction)?;
Validator::validate_iterations(self.iterations)?;
Validator::validate_window_capacity(self.window_capacity, 3)?;
Validator::validate_min_points(self.min_points, self.window_capacity)?;
let capacity = self.window_capacity;
Ok(OnlineLowess {
config: self,
window_x: VecDeque::with_capacity(capacity),
window_y: VecDeque::with_capacity(capacity),
buffer: OnlineBuffer::with_capacity(capacity),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OnlineOutput<T> {
pub smoothed: T,
pub std_error: Option<T>,
pub residual: Option<T>,
pub robustness_weight: Option<T>,
}
pub struct OnlineLowess<T: Float> {
config: OnlineLowessBuilder<T>,
window_x: VecDeque<T>,
window_y: VecDeque<T>,
buffer: OnlineBuffer<T>,
}
impl<T: Float + WLSSolver + Debug + Send + Sync + 'static> OnlineLowess<T> {
pub fn add_point(&mut self, x: T, y: T) -> Result<Option<OnlineOutput<T>>, LowessError> {
Validator::validate_scalar(x, "x")?;
Validator::validate_scalar(y, "y")?;
self.window_x.push_back(x);
self.window_y.push_back(y);
if self.window_x.len() > self.config.window_capacity {
self.window_x.pop_front();
self.window_y.pop_front();
}
if self.window_x.len() < self.config.min_points {
return Ok(None);
}
self.buffer.clear(); self.buffer.scratch_x.extend(self.window_x.iter().copied());
self.buffer.scratch_y.extend(self.window_y.iter().copied());
let x_vec = &*self.buffer.scratch_x;
let y_vec = &*self.buffer.scratch_y;
if x_vec.len() == 2 {
let x0 = x_vec[0];
let x1 = x_vec[1];
let y0 = y_vec[0];
let y1 = y_vec[1];
let smoothed = if x1 != x0 {
let slope = (y1 - y0) / (x1 - x0);
y0 + slope * (x - x0)
} else {
(y0 + y1) / T::from(2.0).unwrap()
};
let residual = y - smoothed;
return Ok(Some(OnlineOutput {
smoothed,
std_error: None,
residual: Some(residual),
robustness_weight: Some(T::one()),
}));
}
let zero_flag = self.config.zero_weight_fallback.to_u8();
let (smoothed, std_err, rob_weight) = match self.config.update_mode {
UpdateMode::Incremental => {
let n = x_vec.len();
let window_size = (self.config.fraction * T::from(n).unwrap())
.ceil()
.to_usize()
.unwrap_or(n)
.max(2)
.min(n);
VecExt::assign(self.buffer.weights.as_vec_mut(), n, T::zero());
VecExt::assign(self.buffer.robustness_weights.as_vec_mut(), n, T::one());
let (smoothed_val, _) = LowessExecutor::fit_single_point(
x_vec,
y_vec,
n - 1, window_size,
false, &self.buffer.robustness_weights,
&mut self.buffer.weights,
self.config.weight_function,
self.config.zero_weight_fallback,
);
(smoothed_val, None, Some(T::one()))
}
UpdateMode::Full => {
let config = LowessConfig {
fraction: Some(self.config.fraction),
iterations: self.config.iterations,
delta: self.config.delta,
weight_function: self.config.weight_function,
robustness_method: self.config.robustness_method,
zero_weight_fallback: zero_flag,
boundary_policy: self.config.boundary_policy,
scaling_method: self.config.scaling_method,
auto_convergence: self.config.auto_convergence,
cv_fractions: None,
cv_kind: None,
return_variance: None,
cv_seed: None,
custom_smooth_pass: self.config.custom_smooth_pass,
custom_cv_pass: self.config.custom_cv_pass,
custom_interval_pass: self.config.custom_interval_pass,
custom_fit_pass: self.config.custom_fit_pass,
parallel: self.config.parallel.unwrap_or(false),
backend: self.config.backend,
delegate_boundary_handling: false,
};
let result = LowessExecutor::run_with_config(x_vec, y_vec, config.clone())?;
let smoothed_vec = result.smoothed;
let se_vec = result.std_errors;
let smoothed_val = smoothed_vec.last().copied().ok_or_else(|| {
LowessError::InvalidNumericValue("No smoothed output produced".into())
})?;
let std_err = se_vec.as_ref().and_then(|v| v.last().copied());
let rob_weight = if self.config.return_robustness_weights {
result.robustness_weights.last().copied()
} else {
None
};
(smoothed_val, std_err, rob_weight)
}
};
let residual = y - smoothed;
Ok(Some(OnlineOutput {
smoothed,
std_error: std_err,
residual: Some(residual),
robustness_weight: rob_weight,
}))
}
pub fn window_size(&self) -> usize {
self.window_x.len()
}
pub fn reset(&mut self) {
self.window_x.clear();
self.window_y.clear();
self.buffer.clear();
}
}