#[cfg(feature = "cpu")]
use rayon::prelude::*;
use num_traits::Float;
use std::fmt::Debug;
use std::vec::Vec;
use loess_rs::internals::algorithms::regression::SolverLinalg;
use loess_rs::internals::engine::executor::{LoessConfig, LoessExecutor};
use loess_rs::internals::evaluation::cv::CVKind;
use loess_rs::internals::math::distance::DistanceLinalg;
use loess_rs::internals::math::linalg::FloatLinalg;
use loess_rs::internals::math::neighborhood::KDTree;
use loess_rs::internals::primitives::window::Window;
#[cfg(feature = "cpu")]
pub fn cv_pass_parallel<T>(
x: &[T],
y: &[T],
fractions: &[T],
cv_kind: CVKind,
config: &LoessConfig<T>,
) -> (T, Vec<T>)
where
T: FloatLinalg + DistanceLinalg + SolverLinalg + Float + Debug + Send + Sync + 'static,
{
let scores: Vec<T> = fractions
.par_iter()
.map(|&frac| evaluate_fraction_cv(x, y, frac, cv_kind, config))
.collect();
let best_idx = scores
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
.unwrap_or(0);
let best_fraction = fractions
.get(best_idx)
.copied()
.unwrap_or_else(|| T::from(0.67).unwrap());
(best_fraction, scores)
}
fn evaluate_fraction_cv<T>(
x: &[T],
y: &[T],
fraction: T,
cv_kind: CVKind,
config: &LoessConfig<T>,
) -> T
where
T: FloatLinalg + DistanceLinalg + SolverLinalg + Float + Debug + Send + Sync + 'static,
{
let dims = config.dimensions;
let n = y.len();
let mut cv_config = config.clone();
cv_config.fraction = Some(fraction);
cv_config.cv_fractions = None; cv_config.return_variance = None; cv_config.return_variance = None;
match cv_kind {
CVKind::LOOCV => {
let _window_size = Window::calculate_span(n, fraction);
let result = LoessExecutor::run_with_config(x, y, cv_config.clone());
let mut sse = T::zero();
for (i, &y_val) in y.iter().enumerate().take(n) {
let residual = y_val - result.smoothed[i];
sse = sse + residual * residual;
}
(sse / T::from(n).unwrap()).sqrt()
}
CVKind::KFold(k) => {
if k < 2 {
return T::infinity();
}
let fold_size = n / k;
if fold_size < 2 {
return T::infinity();
}
let mut fold_rmses = Vec::with_capacity(k);
let executor = LoessExecutor::from_config(&cv_config);
let window_size = Window::calculate_span(n - fold_size, fraction);
for fold in 0..k {
let test_start = fold * fold_size;
let test_end = if fold == k - 1 {
n
} else {
(fold + 1) * fold_size
};
let test_size = test_end - test_start;
let train_n = n - test_size;
let mut train_x = Vec::with_capacity(train_n * dims);
let mut train_y = Vec::with_capacity(train_n);
let mut test_x = Vec::with_capacity(test_size * dims);
for i in 0..n {
if i < test_start || i >= test_end {
for d in 0..dims {
train_x.push(x[i * dims + d]);
}
train_y.push(y[i]);
} else {
for d in 0..dims {
test_x.push(x[i * dims + d]);
}
}
}
if train_y.len() < 3 {
continue;
}
let mut fold_sse = T::zero();
let mut fold_count = 0usize;
if dims == 1 {
let train_result =
LoessExecutor::run_with_config(&train_x, &train_y, cv_config.clone());
let mut train_data: Vec<(T, T)> = train_x
.iter()
.zip(train_result.smoothed.iter())
.map(|(&xi, &yi)| (xi, yi))
.collect();
train_data
.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let (sorted_tx, sorted_smooth): (Vec<T>, Vec<T>) =
train_data.into_iter().unzip();
let mut preds = vec![T::zero(); test_x.len()];
CVKind::interpolate_prediction_batch(
&sorted_tx,
&sorted_smooth,
&test_x,
&mut preds,
);
for (i, &pred) in preds.iter().enumerate() {
let actual_y = y[test_start + i];
let residual = actual_y - pred;
fold_sse = fold_sse + residual * residual;
fold_count += 1;
}
} else {
let mut scales = vec![T::one(); dims];
if train_n > 0 {
let mut mins = vec![train_x[0]; dims];
let mut maxs = vec![train_x[0]; dims];
for i in 0..train_n {
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::epsilon() {
scales[d] = T::one() / range;
}
}
}
let kdtree = KDTree::new(&train_x, dims);
let robustness_weights = vec![T::one(); train_n];
let predictions = executor.predict(
&train_x,
&train_y,
&robustness_weights,
&test_x,
window_size,
&scales,
&kdtree,
);
for (i, &pred) in predictions.iter().enumerate() {
let actual_y = y[test_start + i];
let residual = actual_y - pred;
fold_sse = fold_sse + residual * residual;
fold_count += 1;
}
}
if fold_count > 0 {
fold_rmses.push((fold_sse / T::from(fold_count).unwrap()).sqrt());
}
}
if fold_rmses.is_empty() {
T::infinity()
} else {
let sum: T = fold_rmses.iter().copied().fold(T::zero(), |a, b| a + b);
sum / T::from(fold_rmses.len()).unwrap()
}
}
}
}
#[cfg(not(feature = "cpu"))]
pub fn cv_pass_parallel<T>(
_x: &[T],
_y: &[T],
fractions: &[T],
_cv_kind: CVKind,
_config: &LoessConfig<T>,
) -> (T, Vec<T>)
where
T: Float + Send + Sync,
{
let best = fractions
.first()
.copied()
.unwrap_or_else(|| T::from(0.67).unwrap());
let scores = vec![T::infinity(); fractions.len()];
(best, scores)
}