Skip to main content

lowess/
api.rs

1//! High-level API for LOWESS smoothing.
2//!
3//! This module provides the primary user-facing entry point for LOWESS. It
4//! implements a fluent builder pattern for configuring regression parameters
5//! and choosing an execution adapter (Batch, Streaming, or Online).
6
7// Feature-gated imports
8#[cfg(not(feature = "std"))]
9use alloc::vec::Vec;
10#[cfg(feature = "std")]
11use std::vec::Vec;
12
13// External dependencies
14use num_traits::Float;
15
16// Internal dependencies
17use crate::adapters::batch::BatchLowessBuilder;
18use crate::adapters::online::OnlineLowessBuilder;
19use crate::adapters::streaming::StreamingLowessBuilder;
20use crate::engine::executor::{CVPassFn, IntervalPassFn, SmoothPassFn};
21use crate::evaluation::cv::{CVConfig, CVKind};
22use crate::evaluation::intervals::IntervalMethod;
23use crate::primitives::backend::Backend;
24
25// Publicly re-exported types
26pub use crate::adapters::online::UpdateMode;
27pub use crate::adapters::streaming::MergeStrategy;
28pub use crate::algorithms::regression::ZeroWeightFallback;
29pub use crate::algorithms::robustness::RobustnessMethod;
30pub use crate::engine::output::LowessResult;
31pub use crate::evaluation::cv::{KFold, LOOCV};
32pub use crate::math::boundary::BoundaryPolicy;
33pub use crate::math::kernel::WeightFunction;
34pub use crate::math::scaling::ScalingMethod;
35pub use crate::primitives::errors::LowessError;
36
37// Marker types for selecting execution adapters.
38#[allow(non_snake_case)]
39pub mod Adapter {
40    pub use super::{Batch, Online, Streaming};
41}
42
43// Fluent builder for configuring LOWESS parameters and execution modes.
44#[derive(Debug, Clone)]
45pub struct LowessBuilder<T> {
46    // Smoothing fraction (0..1].
47    pub fraction: Option<T>,
48
49    // Robustness iterations.
50    pub iterations: Option<usize>,
51
52    // Threshold for skipping fitting (delta-optimization).
53    pub delta: Option<T>,
54
55    // Kernel weight function.
56    pub weight_function: Option<WeightFunction>,
57
58    // Outlier downweighting method.
59    pub robustness_method: Option<RobustnessMethod>,
60
61    // Scaling method for robust scale estimation (MAR/MAD/Mean).
62    pub scaling_method: Option<ScalingMethod>,
63
64    // interval estimation configuration.
65    pub interval_type: Option<IntervalMethod<T>>,
66
67    // Candidate bandwidths for cross-validation.
68    pub cv_fractions: Option<Vec<T>>,
69
70    // CV strategy (K-Fold/LOOCV).
71    pub(crate) cv_kind: Option<CVKind>,
72
73    // CV seed for reproducibility.
74    pub(crate) cv_seed: Option<u64>,
75
76    // Relative convergence tolerance.
77    pub auto_convergence: Option<T>,
78
79    // Enable performance/statistical diagnostics.
80    pub return_diagnostics: Option<bool>,
81
82    // Return original residuals r_i.
83    pub compute_residuals: Option<bool>,
84
85    // Return final robustness weights w_i.
86    pub return_robustness_weights: Option<bool>,
87
88    // Policy for handling data boundaries (default: Extend).
89    pub boundary_policy: Option<BoundaryPolicy>,
90
91    // Behavior when local neighborhood weights are zero (default: UseLocalMean).
92    pub zero_weight_fallback: Option<ZeroWeightFallback>,
93
94    // Merging strategy for overlapping chunks (Streaming only).
95    pub merge_strategy: Option<MergeStrategy>,
96
97    // Incremental update mode (Online only).
98    pub update_mode: Option<UpdateMode>,
99
100    // Chunk size for streaming (Streaming only).
101    pub chunk_size: Option<usize>,
102
103    // Overlap size for streaming chunks (Streaming only).
104    pub overlap: Option<usize>,
105
106    // Window capacity for sliding window (Online only).
107    pub window_capacity: Option<usize>,
108
109    // Minimum points required for a valid fit (Online only).
110    pub min_points: Option<usize>,
111
112    // ++++++++++++++++++++++++++++++++++++++
113    // +               DEV                  +
114    // ++++++++++++++++++++++++++++++++++++++
115    // Custom smooth pass function.
116    #[doc(hidden)]
117    pub custom_smooth_pass: Option<SmoothPassFn<T>>,
118
119    // Custom cross-validation pass function.
120    #[doc(hidden)]
121    pub custom_cv_pass: Option<CVPassFn<T>>,
122
123    // Custom interval estimation pass function.
124    #[doc(hidden)]
125    pub custom_interval_pass: Option<IntervalPassFn<T>>,
126
127    // Execution backend hint.
128    #[doc(hidden)]
129    pub backend: Option<Backend>,
130
131    // Parallel execution hint.
132    #[doc(hidden)]
133    pub parallel: Option<bool>,
134
135    // Tracks if any parameter was set multiple times (for validation).
136    #[doc(hidden)]
137    pub duplicate_param: Option<&'static str>,
138}
139
140impl<T: Float> Default for LowessBuilder<T> {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl<T: Float> LowessBuilder<T> {
147    // Select an execution adapter to transition to an execution builder.
148    pub fn adapter<A>(self, _adapter: A) -> A::Output
149    where
150        A: LowessAdapter<T>,
151    {
152        A::convert(self)
153    }
154
155    // Create a new builder with default settings.
156    pub fn new() -> Self {
157        Self {
158            fraction: None,
159            iterations: None,
160            delta: None,
161            weight_function: None,
162            robustness_method: None,
163            scaling_method: None,
164            interval_type: None,
165            cv_fractions: None,
166            cv_kind: None,
167            cv_seed: None,
168            auto_convergence: None,
169            return_diagnostics: None,
170            compute_residuals: None,
171            return_robustness_weights: None,
172            boundary_policy: None,
173            zero_weight_fallback: None,
174            merge_strategy: None,
175            update_mode: None,
176            chunk_size: None,
177            overlap: None,
178            window_capacity: None,
179            min_points: None,
180            custom_smooth_pass: None,
181            custom_cv_pass: None,
182            custom_interval_pass: None,
183            backend: None,
184            parallel: None,
185            duplicate_param: None,
186        }
187    }
188
189    // Set behavior for handling zero-weight neighborhoods.
190    pub fn zero_weight_fallback(mut self, policy: ZeroWeightFallback) -> Self {
191        if self.zero_weight_fallback.is_some() {
192            self.duplicate_param = Some("zero_weight_fallback");
193        }
194        self.zero_weight_fallback = Some(policy);
195        self
196    }
197
198    // Set the boundary handling policy.
199    pub fn boundary_policy(mut self, policy: BoundaryPolicy) -> Self {
200        if self.boundary_policy.is_some() {
201            self.duplicate_param = Some("boundary_policy");
202        }
203        self.boundary_policy = Some(policy);
204        self
205    }
206
207    // Set the merging strategy for overlapping chunks (Streaming only).
208    pub fn merge_strategy(mut self, strategy: MergeStrategy) -> Self {
209        if self.merge_strategy.is_some() {
210            self.duplicate_param = Some("merge_strategy");
211        }
212        self.merge_strategy = Some(strategy);
213        self
214    }
215
216    // Set the incremental update mode (Online only).
217    pub fn update_mode(mut self, mode: UpdateMode) -> Self {
218        if self.update_mode.is_some() {
219            self.duplicate_param = Some("update_mode");
220        }
221        self.update_mode = Some(mode);
222        self
223    }
224
225    // Set the chunk size for streaming (Streaming only).
226    pub fn chunk_size(mut self, size: usize) -> Self {
227        if self.chunk_size.is_some() {
228            self.duplicate_param = Some("chunk_size");
229        }
230        self.chunk_size = Some(size);
231        self
232    }
233
234    // Set the overlap size for streaming chunks (Streaming only).
235    pub fn overlap(mut self, overlap: usize) -> Self {
236        if self.overlap.is_some() {
237            self.duplicate_param = Some("overlap");
238        }
239        self.overlap = Some(overlap);
240        self
241    }
242
243    // Set the window capacity for online processing (Online only).
244    pub fn window_capacity(mut self, capacity: usize) -> Self {
245        if self.window_capacity.is_some() {
246            self.duplicate_param = Some("window_capacity");
247        }
248        self.window_capacity = Some(capacity);
249        self
250    }
251
252    // Set the minimum points required for a valid fit (Online only).
253    pub fn min_points(mut self, points: usize) -> Self {
254        if self.min_points.is_some() {
255            self.duplicate_param = Some("min_points");
256        }
257        self.min_points = Some(points);
258        self
259    }
260
261    // Set the smoothing fraction (bandwidth alpha).
262    pub fn fraction(mut self, fraction: T) -> Self {
263        if self.fraction.is_some() {
264            self.duplicate_param = Some("fraction");
265        }
266        self.fraction = Some(fraction);
267        self
268    }
269
270    // Set the number of robustness iterations (typically 0-4).
271    pub fn iterations(mut self, iterations: usize) -> Self {
272        if self.iterations.is_some() {
273            self.duplicate_param = Some("iterations");
274        }
275        self.iterations = Some(iterations);
276        self
277    }
278
279    // Set the delta parameter for interpolation-based optimization.
280    pub fn delta(mut self, delta: T) -> Self {
281        if self.delta.is_some() {
282            self.duplicate_param = Some("delta");
283        }
284        self.delta = Some(delta);
285        self
286    }
287
288    // Set the kernel weight function.
289    pub fn weight_function(mut self, wf: WeightFunction) -> Self {
290        if self.weight_function.is_some() {
291            self.duplicate_param = Some("weight_function");
292        }
293        self.weight_function = Some(wf);
294        self
295    }
296
297    // Set the robustness weighting method.
298    pub fn robustness_method(mut self, rm: RobustnessMethod) -> Self {
299        if self.robustness_method.is_some() {
300            self.duplicate_param = Some("robustness_method");
301        }
302        self.robustness_method = Some(rm);
303        self
304    }
305
306    // Set the scaling method for robust scale estimation.
307    pub fn scaling_method(mut self, sm: ScalingMethod) -> Self {
308        if self.scaling_method.is_some() {
309            self.duplicate_param = Some("scaling_method");
310        }
311        self.scaling_method = Some(sm);
312        self
313    }
314
315    // Enable standard error computation.
316    pub fn return_se(mut self) -> Self {
317        if self.interval_type.is_none() {
318            self.interval_type = Some(IntervalMethod::se());
319        }
320        self
321    }
322
323    // Enable confidence intervals at the specified level (e.g., 0.95).
324    pub fn confidence_intervals(mut self, level: T) -> Self {
325        if self.interval_type.as_ref().is_some_and(|it| it.confidence) {
326            self.duplicate_param = Some("confidence_intervals");
327        }
328        self.interval_type = Some(match self.interval_type {
329            Some(existing) if existing.prediction => IntervalMethod {
330                level,
331                confidence: true,
332                prediction: true,
333                se: true,
334            },
335            _ => IntervalMethod::confidence(level),
336        });
337        self
338    }
339
340    // Enable prediction intervals at the specified level.
341    pub fn prediction_intervals(mut self, level: T) -> Self {
342        if self.interval_type.as_ref().is_some_and(|it| it.prediction) {
343            self.duplicate_param = Some("prediction_intervals");
344        }
345        self.interval_type = Some(match self.interval_type {
346            Some(existing) if existing.confidence => IntervalMethod {
347                level,
348                confidence: true,
349                prediction: true,
350                se: true,
351            },
352            _ => IntervalMethod::prediction(level),
353        });
354        self
355    }
356
357    // Enable automatic bandwidth selection via cross-validation.
358    pub fn cross_validate(mut self, config: CVConfig<'_, T>) -> Self {
359        if self.cv_fractions.is_some() {
360            self.duplicate_param = Some("cross_validate");
361        }
362        self.cv_fractions = Some(config.fractions().to_vec());
363        self.cv_kind = Some(config.kind());
364        self.cv_seed = config.get_seed();
365        self
366    }
367
368    // Enable automatic convergence detection based on relative change.
369    pub fn auto_converge(mut self, tolerance: T) -> Self {
370        if self.auto_convergence.is_some() {
371            self.duplicate_param = Some("auto_converge");
372        }
373        self.auto_convergence = Some(tolerance);
374        self
375    }
376
377    // Include statistical diagnostics (Metric, R², etc.) in output.
378    pub fn return_diagnostics(mut self) -> Self {
379        self.return_diagnostics = Some(true);
380        self
381    }
382
383    // Include residuals in output.
384    pub fn return_residuals(mut self) -> Self {
385        self.compute_residuals = Some(true);
386        self
387    }
388
389    // Include final robustness weights in output.
390    pub fn return_robustness_weights(mut self) -> Self {
391        self.return_robustness_weights = Some(true);
392        self
393    }
394
395    // ++++++++++++++++++++++++++++++++++++++
396    // +               DEV                  +
397    // ++++++++++++++++++++++++++++++++++++++
398
399    // Set a custom smooth pass function for execution (only for dev)
400    #[doc(hidden)]
401    pub fn custom_smooth_pass(mut self, pass: SmoothPassFn<T>) -> Self {
402        self.custom_smooth_pass = Some(pass);
403        self
404    }
405
406    // Set a custom cross-validation pass function (only for dev)
407    #[doc(hidden)]
408    pub fn custom_cv_pass(mut self, pass: CVPassFn<T>) -> Self {
409        self.custom_cv_pass = Some(pass);
410        self
411    }
412
413    // Set a custom interval estimation pass function (only for dev)
414    #[doc(hidden)]
415    pub fn custom_interval_pass(mut self, pass: IntervalPassFn<T>) -> Self {
416        self.custom_interval_pass = Some(pass);
417        self
418    }
419
420    // Set the execution backend hint (only for dev)
421    #[doc(hidden)]
422    pub fn backend(mut self, backend: Backend) -> Self {
423        self.backend = Some(backend);
424        self
425    }
426
427    // Set parallel execution hint (only for dev)
428    #[doc(hidden)]
429    pub fn parallel(mut self, parallel: bool) -> Self {
430        self.parallel = Some(parallel);
431        self
432    }
433}
434
435// Trait for transitioning from a generic builder to an execution builder.
436pub trait LowessAdapter<T: Float> {
437    // The output execution builder.
438    type Output;
439
440    // Convert a generic [`LowessBuilder`] into a specialized execution builder.
441    fn convert(builder: LowessBuilder<T>) -> Self::Output;
442}
443
444// Marker for in-memory batch processing.
445#[derive(Debug, Clone, Copy)]
446pub struct Batch;
447
448impl<T: Float> LowessAdapter<T> for Batch {
449    type Output = BatchLowessBuilder<T>;
450
451    fn convert(builder: LowessBuilder<T>) -> Self::Output {
452        let mut result = BatchLowessBuilder::default();
453
454        if let Some(fraction) = builder.fraction {
455            result.fraction = fraction;
456        }
457        if let Some(iterations) = builder.iterations {
458            result.iterations = iterations;
459        }
460        if let Some(delta) = builder.delta {
461            result.delta = Some(delta);
462        }
463        if let Some(wf) = builder.weight_function {
464            result.weight_function = wf;
465        }
466        if let Some(rm) = builder.robustness_method {
467            result.robustness_method = rm;
468        }
469        if let Some(it) = builder.interval_type {
470            result.interval_type = Some(it);
471        }
472        if let Some(cvf) = builder.cv_fractions {
473            result.cv_fractions = Some(cvf);
474        }
475        if let Some(cvk) = builder.cv_kind {
476            result.cv_kind = Some(cvk);
477        }
478        result.cv_seed = builder.cv_seed;
479        if let Some(ac) = builder.auto_convergence {
480            result.auto_convergence = Some(ac);
481        }
482        if let Some(zwf) = builder.zero_weight_fallback {
483            result.zero_weight_fallback = zwf;
484        }
485        if let Some(bp) = builder.boundary_policy {
486            result.boundary_policy = bp;
487        }
488        if let Some(sm) = builder.scaling_method {
489            result.scaling_method = sm;
490        }
491
492        if let Some(rw) = builder.return_robustness_weights {
493            result.return_robustness_weights = rw;
494        }
495        if let Some(rd) = builder.return_diagnostics {
496            result.return_diagnostics = rd;
497        }
498        if let Some(cr) = builder.compute_residuals {
499            result.compute_residuals = cr;
500        }
501
502        // ++++++++++++++++++++++++++++++++++++++
503        // +               DEV                  +
504        // ++++++++++++++++++++++++++++++++++++++
505        if let Some(sp) = builder.custom_smooth_pass {
506            result.custom_smooth_pass = Some(sp);
507        }
508        if let Some(cp) = builder.custom_cv_pass {
509            result.custom_cv_pass = Some(cp);
510        }
511        if let Some(ip) = builder.custom_interval_pass {
512            result.custom_interval_pass = Some(ip);
513        }
514        if let Some(b) = builder.backend {
515            result.backend = Some(b);
516        }
517        if let Some(p) = builder.parallel {
518            result.parallel = Some(p);
519        }
520
521        result.duplicate_param = builder.duplicate_param;
522
523        result
524    }
525}
526
527// Marker for chunked streaming processing.
528#[derive(Debug, Clone, Copy)]
529pub struct Streaming;
530
531impl<T: Float> LowessAdapter<T> for Streaming {
532    type Output = StreamingLowessBuilder<T>;
533
534    fn convert(builder: LowessBuilder<T>) -> Self::Output {
535        let mut result = StreamingLowessBuilder::default();
536
537        // Override with user-provided values
538        if let Some(chunk_size) = builder.chunk_size {
539            result.chunk_size = chunk_size;
540        }
541        if let Some(overlap) = builder.overlap {
542            result.overlap = overlap;
543        }
544        if let Some(fraction) = builder.fraction {
545            result.fraction = fraction;
546        }
547        if let Some(iterations) = builder.iterations {
548            result.iterations = iterations;
549        }
550        if let Some(delta) = builder.delta {
551            result.delta = delta;
552        }
553        if let Some(wf) = builder.weight_function {
554            result.weight_function = wf;
555        }
556        if let Some(bp) = builder.boundary_policy {
557            result.boundary_policy = bp;
558        }
559        if let Some(rm) = builder.robustness_method {
560            result.robustness_method = rm;
561        }
562        if let Some(zwf) = builder.zero_weight_fallback {
563            result.zero_weight_fallback = zwf;
564        }
565        if let Some(ms) = builder.merge_strategy {
566            result.merge_strategy = ms;
567        }
568        if let Some(sm) = builder.scaling_method {
569            result.scaling_method = sm;
570        }
571
572        if let Some(rw) = builder.return_robustness_weights {
573            result.return_robustness_weights = rw;
574        }
575        if let Some(rd) = builder.return_diagnostics {
576            result.return_diagnostics = rd;
577        }
578        if let Some(cr) = builder.compute_residuals {
579            result.compute_residuals = cr;
580        }
581        if let Some(ac) = builder.auto_convergence {
582            result.auto_convergence = Some(ac);
583        }
584
585        // ++++++++++++++++++++++++++++++++++++++
586        // +               DEV                  +
587        // ++++++++++++++++++++++++++++++++++++++
588
589        if let Some(sp) = builder.custom_smooth_pass {
590            result.custom_smooth_pass = Some(sp);
591        }
592        if let Some(cp) = builder.custom_cv_pass {
593            result.custom_cv_pass = Some(cp);
594        }
595        if let Some(ip) = builder.custom_interval_pass {
596            result.custom_interval_pass = Some(ip);
597        }
598        if let Some(b) = builder.backend {
599            result.backend = Some(b);
600        }
601        if let Some(p) = builder.parallel {
602            result.parallel = Some(p);
603        }
604        result.duplicate_param = builder.duplicate_param;
605
606        result
607    }
608}
609
610// Marker for incremental online processing.
611#[derive(Debug, Clone, Copy)]
612pub struct Online;
613
614impl<T: Float> LowessAdapter<T> for Online {
615    type Output = OnlineLowessBuilder<T>;
616
617    fn convert(builder: LowessBuilder<T>) -> Self::Output {
618        let mut result = OnlineLowessBuilder::default();
619
620        // Override with user-provided values
621        if let Some(window_capacity) = builder.window_capacity {
622            result.window_capacity = window_capacity;
623        }
624        if let Some(min_points) = builder.min_points {
625            result.min_points = min_points;
626        }
627        if let Some(fraction) = builder.fraction {
628            result.fraction = fraction;
629        }
630        if let Some(iterations) = builder.iterations {
631            result.iterations = iterations;
632        }
633        if let Some(delta) = builder.delta {
634            result.delta = delta;
635        }
636        if let Some(wf) = builder.weight_function {
637            result.weight_function = wf;
638        }
639        if let Some(um) = builder.update_mode {
640            result.update_mode = um;
641        }
642        if let Some(rm) = builder.robustness_method {
643            result.robustness_method = rm;
644        }
645        if let Some(bp) = builder.boundary_policy {
646            result.boundary_policy = bp;
647        }
648        if let Some(zwf) = builder.zero_weight_fallback {
649            result.zero_weight_fallback = zwf;
650        }
651        if let Some(sm) = builder.scaling_method {
652            result.scaling_method = sm;
653        }
654
655        if let Some(cr) = builder.compute_residuals {
656            result.compute_residuals = cr;
657        }
658        if let Some(rw) = builder.return_robustness_weights {
659            result.return_robustness_weights = rw;
660        }
661        if let Some(ac) = builder.auto_convergence {
662            result.auto_convergence = Some(ac);
663        }
664
665        // ++++++++++++++++++++++++++++++++++++++
666        // +               DEV                  +
667        // ++++++++++++++++++++++++++++++++++++++
668
669        if let Some(sp) = builder.custom_smooth_pass {
670            result.custom_smooth_pass = Some(sp);
671        }
672        if let Some(cp) = builder.custom_cv_pass {
673            result.custom_cv_pass = Some(cp);
674        }
675        if let Some(ip) = builder.custom_interval_pass {
676            result.custom_interval_pass = Some(ip);
677        }
678        if let Some(b) = builder.backend {
679            result.backend = Some(b);
680        }
681        if let Some(p) = builder.parallel {
682            result.parallel = Some(p);
683        }
684        result.duplicate_param = builder.duplicate_param;
685
686        result
687    }
688}