#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::vec;
#[cfg(feature = "std")]
use std::vec::Vec;
use core::cmp::Ordering::Equal;
use core::fmt::Debug;
use num_traits::Float;
use crate::algorithms::interpolation::InterpolationSurface;
use crate::algorithms::regression::{
PolynomialDegree, RegressionContext, SolverLinalg, ZeroWeightFallback,
};
use crate::algorithms::robustness::RobustnessMethod;
use crate::evaluation::cv::CVKind;
use crate::evaluation::intervals::IntervalMethod;
use crate::math::boundary::BoundaryPolicy;
use crate::math::distance::{DistanceLinalg, DistanceMetric};
use crate::math::kernel::WeightFunction;
use crate::math::linalg::FloatLinalg;
use crate::math::neighborhood::{KDTree, Neighborhood, NodeDistance, PointDistance};
use crate::math::scaling::ScalingMethod;
use crate::primitives::backend::Backend;
use crate::primitives::buffer::{
CachedNeighborhood, FittingBuffer, LoessBuffer, NeighborhoodSearchBuffer,
};
use crate::primitives::window::Window;
pub struct LoessDistanceCalculator<'a, T: FloatLinalg + DistanceLinalg + SolverLinalg> {
pub metric: DistanceMetric<T>,
pub scales: &'a [T],
}
impl<'a, T: FloatLinalg + DistanceLinalg + SolverLinalg> PointDistance<T>
for LoessDistanceCalculator<'a, T>
{
fn split_distance(&self, dim: usize, split_val: T, query_val: T) -> T {
let diff = (query_val - split_val).abs();
match &self.metric {
DistanceMetric::Normalized => diff * self.scales[dim],
DistanceMetric::Euclidean => diff,
DistanceMetric::Manhattan => diff,
DistanceMetric::Chebyshev => diff,
DistanceMetric::Minkowski(_) => diff,
DistanceMetric::Weighted(w) => diff * w[dim].sqrt(),
}
}
fn distance_squared(&self, a: &[T], b: &[T]) -> T {
match &self.metric {
DistanceMetric::Normalized => DistanceMetric::normalized_squared(a, b, self.scales),
DistanceMetric::Euclidean => DistanceMetric::euclidean_squared(a, b),
DistanceMetric::Weighted(w) => DistanceMetric::weighted_squared(a, b, w),
DistanceMetric::Manhattan => DistanceMetric::manhattan_squared(a, b),
DistanceMetric::Chebyshev => DistanceMetric::chebyshev_squared(a, b),
DistanceMetric::Minkowski(p) => DistanceMetric::minkowski_squared(a, b, *p),
}
}
fn split_distance_squared(&self, dim: usize, split_val: T, query_val: T) -> T {
let diff = query_val - split_val;
match &self.metric {
DistanceMetric::Normalized => {
let d = diff * self.scales[dim];
d * d
}
DistanceMetric::Euclidean => diff * diff,
DistanceMetric::Weighted(w) => diff * diff * w[dim],
_ => {
let d = self.split_distance(dim, split_val, query_val);
d * d
}
}
}
fn post_process_distance(&self, d: T) -> T {
d.sqrt()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SurfaceMode {
#[default]
Interpolation,
Direct,
}
#[doc(hidden)]
pub type SmoothPassFn<T> = fn(
&[T], &[T], &[T], &[T], usize, usize, bool, &[T], &mut [T], WeightFunction, ZeroWeightFallback, PolynomialDegree, &DistanceMetric<T>, &[T], );
#[doc(hidden)]
pub type CVPassFn<T> = fn(
&[T], &[T], &[T], CVKind, &LoessConfig<T>, ) -> (T, Vec<T>);
#[doc(hidden)]
pub type IntervalPassFn<T> = fn(
&[T], &[T], &[T], &[T], &[T], usize, usize, &[T], WeightFunction, &IntervalMethod<T>, PolynomialDegree, &DistanceMetric<T>, &[T], ) -> Vec<T>;
#[doc(hidden)]
pub type FitPassFn<T> = fn(
&[T], &[T], &LoessConfig<T>, ) -> (
Vec<T>, // smoothed
Option<Vec<T>>, // std_errors
usize, // iterations
Vec<T>, // robustness_weights
);
#[doc(hidden)]
pub type VertexPassFn<T> = fn(
&[T], &[T], usize, &[T], usize, bool, &[T], &mut [T], Option<&[CachedNeighborhood<T>]>, &mut Vec<CachedNeighborhood<T>>, WeightFunction,
ZeroWeightFallback,
PolynomialDegree,
&DistanceMetric<T>,
&[T], bool, );
#[doc(hidden)]
pub type KDTreeBuilderFn<T> = fn(points: &[T], dims: usize) -> KDTree<T>;
#[derive(Debug, Clone)]
pub struct ExecutorOutput<T: FloatLinalg> {
pub smoothed: Vec<T>,
pub std_errors: Option<Vec<T>>,
pub iterations: Option<usize>,
pub used_fraction: T,
pub cv_scores: Option<Vec<T>>,
pub robustness_weights: Vec<T>,
pub leverage: Option<Vec<T>>,
}
#[derive(Debug, Clone)]
pub struct LoessConfig<T: FloatLinalg + SolverLinalg> {
pub fraction: Option<T>,
pub iterations: usize,
pub weight_function: WeightFunction,
pub zero_weight_fallback: ZeroWeightFallback,
pub robustness_method: RobustnessMethod,
pub scaling_method: ScalingMethod,
pub cv_fractions: Option<Vec<T>>,
pub cv_kind: Option<CVKind>,
pub cv_seed: Option<u64>,
pub auto_converge: Option<T>,
pub return_variance: Option<IntervalMethod<T>>,
pub boundary_policy: BoundaryPolicy,
pub polynomial_degree: PolynomialDegree,
pub dimensions: usize,
pub distance_metric: DistanceMetric<T>,
pub surface_mode: SurfaceMode,
pub interpolation_vertices: Option<usize>,
pub cell: Option<f64>,
pub boundary_degree_fallback: bool,
#[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 custom_vertex_pass: Option<VertexPassFn<T>>,
#[doc(hidden)]
pub custom_kdtree_builder: Option<KDTreeBuilderFn<T>>,
#[doc(hidden)]
pub backend: Option<Backend>,
#[doc(hidden)]
pub parallel: bool,
}
impl<T: FloatLinalg + DistanceLinalg + Debug + Send + Sync + SolverLinalg> Default
for LoessConfig<T>
{
fn default() -> Self {
Self {
fraction: None,
iterations: 3,
weight_function: WeightFunction::default(),
zero_weight_fallback: ZeroWeightFallback::default(),
robustness_method: RobustnessMethod::default(),
scaling_method: ScalingMethod::default(),
cv_fractions: None,
cv_kind: None,
cv_seed: None,
auto_converge: None,
return_variance: None,
boundary_policy: BoundaryPolicy::default(),
polynomial_degree: PolynomialDegree::default(),
dimensions: 1,
distance_metric: DistanceMetric::default(),
surface_mode: SurfaceMode::default(),
interpolation_vertices: None,
cell: None,
boundary_degree_fallback: true,
custom_smooth_pass: None,
custom_cv_pass: None,
custom_interval_pass: None,
custom_fit_pass: None,
custom_vertex_pass: None,
custom_kdtree_builder: None,
parallel: false,
backend: None,
}
}
}
#[derive(Debug, Clone)]
pub struct LoessExecutor<T: FloatLinalg + SolverLinalg> {
pub fraction: T,
pub iterations: usize,
pub weight_function: WeightFunction,
pub zero_weight_fallback: ZeroWeightFallback,
pub robustness_method: RobustnessMethod,
pub scaling_method: ScalingMethod,
pub boundary_policy: BoundaryPolicy,
pub polynomial_degree: PolynomialDegree,
pub dimensions: usize,
pub distance_metric: DistanceMetric<T>,
pub surface_mode: SurfaceMode,
pub interpolation_vertices: Option<usize>,
pub cell: Option<f64>,
pub boundary_degree_fallback: bool,
#[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 custom_vertex_pass: Option<VertexPassFn<T>>,
#[doc(hidden)]
pub custom_kdtree_builder: Option<KDTreeBuilderFn<T>>,
#[doc(hidden)]
pub backend: Option<Backend>,
#[doc(hidden)]
pub parallel: bool,
}
impl<T: FloatLinalg + DistanceLinalg + Debug + Send + Sync + 'static + SolverLinalg> Default
for LoessExecutor<T>
{
fn default() -> Self {
Self::new()
}
}
impl<T: FloatLinalg + DistanceLinalg + Debug + Send + Sync + 'static + SolverLinalg>
LoessExecutor<T>
{
pub fn new() -> Self {
Self {
fraction: T::from(0.67).unwrap_or_else(|| T::from(0.5).unwrap()),
iterations: 3,
weight_function: WeightFunction::default(),
zero_weight_fallback: ZeroWeightFallback::default(),
robustness_method: RobustnessMethod::default(),
scaling_method: ScalingMethod::default(),
boundary_policy: BoundaryPolicy::default(),
polynomial_degree: PolynomialDegree::default(),
dimensions: 1,
distance_metric: DistanceMetric::default(),
surface_mode: SurfaceMode::default(),
interpolation_vertices: None,
cell: None,
boundary_degree_fallback: true,
custom_smooth_pass: None,
custom_cv_pass: None,
custom_interval_pass: None,
custom_fit_pass: None,
custom_vertex_pass: None,
custom_kdtree_builder: None,
parallel: false,
backend: None,
}
}
pub fn from_config(config: &LoessConfig<T>) -> Self {
let default_frac = T::from(0.67).unwrap_or_else(|| T::from(0.5).unwrap());
Self::new()
.fraction(config.fraction.unwrap_or(default_frac))
.iterations(config.iterations)
.weight_function(config.weight_function)
.zero_weight_fallback(config.zero_weight_fallback)
.robustness_method(config.robustness_method)
.scaling_method(config.scaling_method)
.boundary_policy(config.boundary_policy)
.polynomial_degree(config.polynomial_degree)
.dimensions(config.dimensions)
.distance_metric(config.distance_metric.clone())
.surface_mode(config.surface_mode)
.interpolation_vertices(config.interpolation_vertices)
.cell(config.cell)
.boundary_degree_fallback(config.boundary_degree_fallback)
.custom_smooth_pass(config.custom_smooth_pass)
.custom_cv_pass(config.custom_cv_pass)
.custom_interval_pass(config.custom_interval_pass)
.custom_fit_pass(config.custom_fit_pass)
.custom_vertex_pass(config.custom_vertex_pass)
.custom_kdtree_builder(config.custom_kdtree_builder)
.parallel(config.parallel)
.backend(config.backend)
}
pub fn fraction(mut self, frac: T) -> Self {
self.fraction = frac;
self
}
pub fn iterations(mut self, niter: usize) -> Self {
self.iterations = niter;
self
}
pub fn weight_function(mut self, wf: WeightFunction) -> Self {
self.weight_function = wf;
self
}
pub fn zero_weight_fallback(mut self, flag: ZeroWeightFallback) -> Self {
self.zero_weight_fallback = flag;
self
}
pub fn robustness_method(mut self, method: RobustnessMethod) -> Self {
self.robustness_method = method;
self
}
pub fn scaling_method(mut self, method: ScalingMethod) -> Self {
self.scaling_method = method;
self
}
pub fn boundary_policy(mut self, policy: BoundaryPolicy) -> Self {
self.boundary_policy = policy;
self
}
pub fn polynomial_degree(mut self, degree: PolynomialDegree) -> Self {
self.polynomial_degree = degree;
self
}
pub fn dimensions(mut self, dims: usize) -> Self {
self.dimensions = dims;
self
}
pub fn distance_metric(mut self, metric: DistanceMetric<T>) -> Self {
self.distance_metric = metric;
self
}
pub fn surface_mode(mut self, mode: SurfaceMode) -> Self {
self.surface_mode = mode;
self
}
pub fn interpolation_vertices(mut self, vertices: Option<usize>) -> Self {
self.interpolation_vertices = vertices;
self
}
pub fn cell(mut self, cell: Option<f64>) -> Self {
self.cell = cell;
self
}
pub fn boundary_degree_fallback(mut self, enabled: bool) -> Self {
self.boundary_degree_fallback = enabled;
self
}
#[doc(hidden)]
pub fn custom_smooth_pass(mut self, smooth_pass_fn: Option<SmoothPassFn<T>>) -> Self {
self.custom_smooth_pass = smooth_pass_fn;
self
}
#[doc(hidden)]
pub fn custom_cv_pass(mut self, cv_pass_fn: Option<CVPassFn<T>>) -> Self {
self.custom_cv_pass = cv_pass_fn;
self
}
#[doc(hidden)]
pub fn custom_interval_pass(mut self, interval_pass_fn: Option<IntervalPassFn<T>>) -> Self {
self.custom_interval_pass = interval_pass_fn;
self
}
#[doc(hidden)]
pub fn custom_vertex_pass(mut self, vertex_pass_fn: Option<VertexPassFn<T>>) -> Self {
self.custom_vertex_pass = vertex_pass_fn;
self
}
#[doc(hidden)]
pub fn custom_fit_pass(mut self, fit_pass_fn: Option<FitPassFn<T>>) -> Self {
self.custom_fit_pass = fit_pass_fn;
self
}
#[doc(hidden)]
pub fn custom_kdtree_builder(mut self, kdtree_builder_fn: Option<KDTreeBuilderFn<T>>) -> Self {
self.custom_kdtree_builder = kdtree_builder_fn;
self
}
#[doc(hidden)]
pub fn parallel(mut self, parallel: bool) -> Self {
self.parallel = parallel;
self
}
#[doc(hidden)]
pub fn backend(mut self, backend: Option<Backend>) -> Self {
self.backend = backend;
self
}
pub fn run_with_config(x: &[T], y: &[T], config: LoessConfig<T>) -> ExecutorOutput<T>
where
T: Float + Debug + Send + Sync + 'static,
{
let executor = LoessExecutor::from_config(&config);
let dims = executor.dimensions;
let n = x.len() / dims;
let eff_fraction = config.fraction.unwrap_or(executor.fraction);
let window_size = Window::calculate_span(n, eff_fraction);
let n_coeffs = executor.polynomial_degree.num_coefficients_nd(dims);
let mut workspace =
LoessBuffer::<T, NodeDistance<T>, Neighborhood<T>>::new(n, dims, window_size, n_coeffs);
if let Some(ref cv_fracs) = config.cv_fractions {
let cv_kind = config.cv_kind.unwrap_or(CVKind::KFold(5));
let (best_frac, scores) = if let Some(callback) = config.custom_cv_pass {
callback(x, y, cv_fracs, cv_kind, &config)
} else {
let predictor = if dims > 1 {
Some(|train_x: &[T], train_y: &[T], test_x: &[T], f: T| {
let n_train = train_y.len();
let window_size = Window::calculate_span(n_train, f);
let mut scales = vec![T::one(); dims];
if n_train > 0 {
let mut mins = train_x[..dims].to_vec();
let mut maxs = train_x[..dims].to_vec();
for i in 1..n_train {
for d in 0..dims {
let val = train_x[i * dims + d];
if val < mins[d] {
mins[d] = val;
}
if val > maxs[d] {
maxs[d] = val;
}
}
}
for d in 0..dims {
let range = maxs[d] - mins[d];
if range > T::zero() {
scales[d] = T::one() / range;
}
}
}
let kdtree = if let Some(builder) = executor.custom_kdtree_builder {
builder(train_x, dims)
} else {
KDTree::new(train_x, dims)
};
executor.predict(
train_x,
train_y,
&vec![T::one(); n_train],
test_x,
window_size,
&scales,
&kdtree,
)
})
} else {
None
};
let LoessBuffer {
ref mut cv_buffer, ..
} = workspace;
cv_kind.run(
x,
y,
dims,
cv_fracs,
config.cv_seed,
|tx, ty, f| {
executor
.run(tx, ty, Some(f), None, None, None, None)
.smoothed
},
predictor,
cv_buffer,
)
};
let mut output = executor.run(
x,
y,
Some(best_frac),
Some(config.iterations),
config.auto_converge,
config.return_variance.as_ref(),
Some(&mut workspace),
);
output.cv_scores = Some(scores);
output.used_fraction = best_frac;
output
} else {
executor.run(
x,
y,
config.fraction,
Some(config.iterations),
config.auto_converge,
config.return_variance.as_ref(),
Some(&mut workspace),
)
}
}
#[allow(clippy::too_many_arguments)]
fn run(
&self,
x: &[T],
y: &[T],
fraction: Option<T>,
max_iter: Option<usize>,
tolerance: Option<T>,
confidence_method: Option<&IntervalMethod<T>>,
workspace: Option<&mut LoessBuffer<T, NodeDistance<T>, Neighborhood<T>>>,
) -> ExecutorOutput<T>
where
T: Float + Debug + Send + Sync + 'static,
{
let dims = self.dimensions;
let n = x.len() / dims;
let eff_fraction = fraction.unwrap_or(self.fraction);
let window_size = Window::calculate_span(n, eff_fraction);
let target_iterations = max_iter.unwrap_or(self.iterations);
let n_coeffs = self.polynomial_degree.num_coefficients_nd(dims);
let (ax, ay, mapping) = self.boundary_policy.apply(x, y, dims, window_size);
let n_total = ay.len();
let is_augmented = n_total > n;
let mut new_workspace;
let workspace = if let Some(ws) = workspace {
ws.ensure_capacity(n_total, dims, window_size, n_coeffs);
ws
} else {
new_workspace = LoessBuffer::<T, NodeDistance<T>, Neighborhood<T>>::new(
n_total,
dims,
window_size,
n_coeffs,
);
&mut new_workspace
};
workspace.executor_buffer.ensure_capacity(n_total, dims);
let mins = &mut workspace.executor_buffer.mins;
let maxs = &mut workspace.executor_buffer.maxs;
mins.resize(dims, T::zero());
maxs.resize(dims, T::zero());
mins[..dims].copy_from_slice(&ax[..dims]);
maxs[..dims].copy_from_slice(&ax[..dims]);
for i in 1..n_total {
for d in 0..dims {
let val = ax[i * dims + d];
if val < mins[d] {
mins[d] = val;
}
if val > maxs[d] {
maxs[d] = val;
}
}
}
workspace.executor_buffer.scales.resize(dims, T::one());
let scales_ref = &mut workspace.executor_buffer.scales;
for d in 0..dims {
let range = maxs[d] - mins[d];
if range > T::zero() {
scales_ref[d] = T::one() / range;
} else {
scales_ref[d] = T::one();
}
}
let scales_local = workspace.executor_buffer.scales.clone();
let kdtree = if let Some(builder) = self.custom_kdtree_builder {
builder(&ax, dims)
} else {
KDTree::new(&ax, dims)
};
let dist_calc = LoessDistanceCalculator {
metric: self.distance_metric.clone(),
scales: &scales_local,
};
let max_vertices = self.interpolation_vertices.unwrap_or(usize::MAX);
let n_coeffs = self.polynomial_degree.num_coefficients_nd(dims);
workspace.ensure_capacity(n_total, dims, window_size, n_coeffs);
let mut y_smooth = vec![T::zero(); n];
workspace
.executor_buffer
.robustness_weights
.resize(n_total, T::one());
workspace.executor_buffer.residuals.resize(n, T::zero());
let mut _surface_opt: Option<InterpolationSurface<T>> = None;
if self.surface_mode == SurfaceMode::Interpolation {
let fitter = |vertex: &[T],
neighborhood: &Neighborhood<T>,
fb: &mut FittingBuffer<T>,
degree: PolynomialDegree| {
let mut context = RegressionContext::new(
&ax,
dims,
&ay,
0, Some(vertex),
neighborhood,
false, &workspace.executor_buffer.robustness_weights,
self.weight_function,
self.zero_weight_fallback,
degree,
false, Some(fb),
);
context.fit_with_coefficients()
};
let cell_fraction =
T::from(self.cell.unwrap_or(0.2)).unwrap_or_else(|| T::from(0.2).unwrap());
let surface = InterpolationSurface::build(
&ax,
&ay,
dims,
eff_fraction,
window_size,
&dist_calc,
&kdtree,
max_vertices,
fitter,
&mut workspace.search_buffer,
&mut workspace.neighborhood,
&mut workspace.fitting_buffer,
cell_fraction,
self.custom_vertex_pass,
&scales_local,
self.weight_function,
self.zero_weight_fallback,
self.polynomial_degree,
&self.distance_metric,
self.boundary_degree_fallback,
);
_surface_opt = Some(surface);
let surface = _surface_opt.as_ref().unwrap();
for (i, val) in y_smooth.iter_mut().enumerate().take(n) {
let query_offset = i * dims;
let query_point = &x[query_offset..query_offset + dims];
*val = surface.evaluate(query_point);
}
} else {
workspace.executor_buffer.neighborhood_cache.entries.clear();
if let Some(callback) = self.custom_smooth_pass {
callback(
x,
y,
&ax,
&ay,
dims,
window_size,
false, &workspace.executor_buffer.robustness_weights,
&mut y_smooth,
self.weight_function,
self.zero_weight_fallback,
self.polynomial_degree,
&self.distance_metric,
&scales_local,
);
workspace.executor_buffer.neighborhood_cache.is_valid = false;
} else {
self.smooth_pass(
&ax,
&ay,
x, y, window_size,
&workspace.executor_buffer.robustness_weights,
false,
&scales_local,
&mut y_smooth,
n,
&kdtree,
&mut workspace.search_buffer,
&mut workspace.neighborhood,
&mut workspace.fitting_buffer,
None, Some(&mut workspace.executor_buffer.neighborhood_cache.entries), None, );
workspace.executor_buffer.neighborhood_cache.is_valid = true;
}
}
let mut iterations_performed = 1;
for iter in 1..target_iterations {
iterations_performed = iter + 1;
T::batch_abs_residuals(
&y[..n],
&y_smooth[..n],
&mut workspace.executor_buffer.residuals[..n],
);
let n_res = n; let mut new_weights = vec![T::zero(); n];
workspace
.executor_buffer
.sorted_residuals
.resize(n_res, T::zero());
self.robustness_method.apply_robustness_weights(
&workspace.executor_buffer.residuals[..n_res],
&mut new_weights,
self.scaling_method,
&mut workspace.executor_buffer.sorted_residuals,
);
let tolerance_val = tolerance.unwrap_or_else(|| T::from(1e-6).unwrap());
let mut max_change = T::zero();
if is_augmented {
for (aug_idx, &orig_idx) in mapping.iter().enumerate() {
let old_w = workspace.executor_buffer.robustness_weights[aug_idx];
let new_w = new_weights[orig_idx];
workspace.executor_buffer.robustness_weights[aug_idx] = new_w;
let change = (new_w - old_w).abs();
if change > max_change {
max_change = change;
}
}
} else {
for (i, &new_w) in new_weights.iter().enumerate().take(n) {
let old_w = workspace.executor_buffer.robustness_weights[i];
workspace.executor_buffer.robustness_weights[i] = new_w;
let change = (new_w - old_w).abs();
if change > max_change {
max_change = change;
}
}
}
if max_change < tolerance_val {
break;
}
match self.surface_mode {
SurfaceMode::Interpolation => {
if let Some(ref mut surface) = _surface_opt {
let fitter =
|vertex: &[T],
neighborhood: &Neighborhood<T>,
fb: &mut FittingBuffer<T>,
degree: PolynomialDegree| {
let mut context = RegressionContext::new(
&ax,
dims,
&ay,
0, Some(vertex),
neighborhood,
true, &workspace.executor_buffer.robustness_weights,
self.weight_function,
self.zero_weight_fallback,
degree,
false, Some(fb),
);
context.fit_with_coefficients()
};
surface.refit_values(
&ax,
&ay,
fitter,
&mut workspace.neighborhood,
&mut workspace.fitting_buffer,
self.custom_vertex_pass,
self.weight_function,
self.zero_weight_fallback,
self.polynomial_degree,
&self.distance_metric,
&scales_local,
&workspace.executor_buffer.robustness_weights,
self.boundary_degree_fallback,
);
for (i, val) in y_smooth.iter_mut().enumerate().take(n) {
let query_offset = i * dims;
let query_point = &x[query_offset..query_offset + dims];
*val = surface.evaluate(query_point);
}
}
}
SurfaceMode::Direct => {
if let Some(callback) = self.custom_smooth_pass {
callback(
x,
y,
&ax,
&ay,
dims,
window_size,
true, &workspace.executor_buffer.robustness_weights,
&mut y_smooth,
self.weight_function,
self.zero_weight_fallback,
self.polynomial_degree,
&self.distance_metric,
&scales_local,
);
} else {
let cache_ref = if workspace.executor_buffer.neighborhood_cache.is_valid {
Some(
workspace
.executor_buffer
.neighborhood_cache
.entries
.as_slice(),
)
} else {
None
};
self.smooth_pass(
&ax,
&ay,
x, y, window_size,
&workspace.executor_buffer.robustness_weights,
true,
&scales_local,
&mut y_smooth,
n,
&kdtree,
&mut workspace.search_buffer,
&mut workspace.neighborhood,
&mut workspace.fitting_buffer,
None, None, cache_ref, );
}
}
}
}
let leverage_values =
if confidence_method.is_some() && self.surface_mode == SurfaceMode::Direct {
let mut leverages = Vec::with_capacity(n);
let cache_ref = if workspace.executor_buffer.neighborhood_cache.is_valid {
Some(
workspace
.executor_buffer
.neighborhood_cache
.entries
.as_slice(),
)
} else {
None
};
self.smooth_pass(
&ax,
&ay,
x, y, window_size,
&workspace.executor_buffer.robustness_weights,
true,
&scales_local,
&mut y_smooth,
n,
&kdtree,
&mut workspace.search_buffer,
&mut workspace.neighborhood,
&mut workspace.fitting_buffer,
Some(&mut leverages),
None, cache_ref, );
Some(leverages)
} else {
None
};
let se = if let Some(interval_method) = confidence_method {
if let Some(callback) = self.custom_interval_pass {
Some(callback(
x,
y,
&ax,
&ay,
&y_smooth,
dims,
window_size,
&workspace.executor_buffer.robustness_weights,
self.weight_function,
interval_method,
self.polynomial_degree,
&self.distance_metric,
&scales_local,
))
} else if let Some(ref lev) = leverage_values {
T::batch_abs_residuals(
&y[..n],
&y_smooth[..n],
&mut workspace.executor_buffer.residuals[..n],
);
let mut sorted_residuals = workspace.executor_buffer.residuals.clone();
let median_idx = n / 2;
if median_idx < sorted_residuals.len() {
sorted_residuals.select_nth_unstable_by(median_idx, |a: &T, b| {
a.partial_cmp(b).unwrap_or(Equal)
});
}
let median_residual = sorted_residuals[median_idx];
let sigma = median_residual * T::from(1.4826).unwrap();
let mut se_vec = vec![T::zero(); n];
T::batch_sqrt_scale(lev, sigma, &mut se_vec);
Some(se_vec)
} else {
T::batch_abs_residuals(
&y[..n],
&y_smooth[..n],
&mut workspace.executor_buffer.residuals[..n],
);
let mut sorted_residuals = workspace.executor_buffer.residuals.clone();
let median_idx = n / 2;
if median_idx < sorted_residuals.len() {
sorted_residuals.select_nth_unstable_by(median_idx, |a: &T, b| {
a.partial_cmp(b).unwrap_or(Equal)
});
}
let median_residual = sorted_residuals[median_idx];
let sigma = median_residual * T::from(1.4826).unwrap();
let approx_leverage = eff_fraction / T::from(n).unwrap();
let se_vec: Vec<T> = (0..n).map(|_| sigma * approx_leverage.sqrt()).collect();
Some(se_vec)
}
} else {
None
};
let final_robustness_weights = if is_augmented {
let mut rw = vec![T::one(); n];
for (i, &idx) in mapping.iter().enumerate().take(n_total) {
if idx < n {
rw[idx] = workspace.executor_buffer.robustness_weights[i];
}
}
rw
} else {
workspace.executor_buffer.robustness_weights[..n].to_vec()
};
ExecutorOutput {
smoothed: y_smooth,
std_errors: se,
iterations: Some(iterations_performed),
used_fraction: eff_fraction,
cv_scores: None,
robustness_weights: final_robustness_weights,
leverage: leverage_values,
}
}
#[allow(clippy::too_many_arguments)]
pub fn predict(
&self,
x_train: &[T],
y_train: &[T],
robustness_weights: &[T],
x_query: &[T],
window_size: usize,
scales: &[T],
kdtree: &KDTree<T>,
) -> Vec<T>
where
T: Float + Debug + Send + Sync + 'static,
{
let dims = self.dimensions;
let n_query = x_query.len() / dims;
let mut y_pred = vec![T::zero(); n_query];
let dist_calc = LoessDistanceCalculator {
metric: self.distance_metric.clone(),
scales,
};
let n_points_train = x_train.len() / dims;
let n_coeffs = self.polynomial_degree.num_coefficients_nd(dims);
let mut workspace = LoessBuffer::<T, NodeDistance<T>, Neighborhood<T>>::new(
n_points_train,
dims,
window_size,
n_coeffs,
);
for (i, pred) in y_pred.iter_mut().enumerate() {
let query_offset = i * dims;
let query_point = &x_query[query_offset..query_offset + dims];
kdtree.find_k_nearest(
query_point,
window_size,
&dist_calc,
None,
&mut workspace.search_buffer,
&mut workspace.neighborhood,
);
let neighborhood = &workspace.neighborhood;
let mut context = RegressionContext::new(
x_train,
dims,
y_train,
0, Some(query_point),
neighborhood,
true, robustness_weights,
self.weight_function,
ZeroWeightFallback::UseLocalMean, self.polynomial_degree,
false, Some(&mut workspace.fitting_buffer),
);
if let Some((val, _)) = context.fit() {
*pred = val;
} else {
*pred = T::zero();
}
}
y_pred
}
#[allow(clippy::too_many_arguments)]
fn smooth_pass(
&self,
x_context: &[T],
y_context: &[T],
x_query: &[T],
y_query: &[T],
window_size: usize,
robustness_weights: &[T],
use_robustness: bool,
scales: &[T],
y_smooth: &mut [T],
original_n: usize,
kdtree: &KDTree<T>,
search_buffer: &mut NeighborhoodSearchBuffer<NodeDistance<T>>,
neighborhood: &mut Neighborhood<T>,
fitting_buffer: &mut FittingBuffer<T>,
mut leverage_out: Option<&mut Vec<T>>,
mut populate_cache: Option<&mut Vec<CachedNeighborhood<T>>>,
cached_neighborhoods: Option<&[CachedNeighborhood<T>]>,
) where
T: Float + Debug + Send + Sync + 'static,
{
let dims = self.dimensions;
let dist_calc = LoessDistanceCalculator {
metric: self.distance_metric.clone(),
scales,
};
let compute_leverage = leverage_out.is_some();
if let Some(ref mut cache) = populate_cache.as_ref() {
let _ = cache; }
for i in 0..original_n {
let query_offset = i * dims;
let query_point = &x_query[query_offset..query_offset + dims];
if let Some(cache) = cached_neighborhoods {
let cached = &cache[i];
neighborhood.indices.clear();
neighborhood.indices.extend_from_slice(&cached.indices);
neighborhood.distances.clear();
neighborhood.distances.extend_from_slice(&cached.distances);
neighborhood.max_distance = cached.max_distance;
} else {
kdtree.find_k_nearest(
query_point,
window_size,
&dist_calc,
None,
search_buffer,
neighborhood,
);
if let Some(ref mut cache) = populate_cache {
cache.push(CachedNeighborhood {
indices: neighborhood.indices.clone(),
distances: neighborhood.distances.clone(),
max_distance: neighborhood.max_distance,
});
}
}
let neighborhood_ref = &*neighborhood;
let mut context = RegressionContext::new(
x_context,
dims,
y_context,
i,
Some(query_point),
neighborhood_ref,
use_robustness,
robustness_weights,
self.weight_function,
self.zero_weight_fallback,
self.polynomial_degree,
compute_leverage,
Some(fitting_buffer),
);
if let Some((val, lev)) = context.fit() {
y_smooth[i] = val;
if let Some(ref mut lev_vec) = leverage_out {
if lev_vec.len() <= i {
lev_vec.resize(i + 1, T::zero());
}
lev_vec[i] = lev;
}
} else {
y_smooth[i] = y_query[i];
if let Some(ref mut lev_vec) = leverage_out {
if lev_vec.len() <= i {
lev_vec.resize(i + 1, T::zero());
}
lev_vec[i] = T::zero();
}
}
}
}
}