use crate::kernels::*;
use scirs2_core::ndarray::{Array1, ArrayView1, ArrayView2, Axis};
use sklears_core::error::{Result as SklResult, SklearsError};
#[derive(Debug, Clone)]
pub struct AutomaticKernelConstructor {
pub max_components: usize,
pub include_periodic: bool,
pub include_linear: bool,
pub include_polynomial: bool,
pub correlation_threshold: f64,
pub use_cross_validation: bool,
pub random_state: Option<u64>,
}
impl Default for AutomaticKernelConstructor {
fn default() -> Self {
Self {
max_components: 5,
include_periodic: true,
include_linear: true,
include_polynomial: true,
correlation_threshold: 0.1,
use_cross_validation: true,
random_state: Some(42),
}
}
}
#[derive(Debug, Clone)]
pub struct KernelConstructionResult {
pub best_kernel: Box<dyn Kernel>,
pub best_score: f64,
pub kernel_scores: Vec<(String, f64)>,
pub data_characteristics: DataCharacteristics,
}
#[derive(Debug, Clone)]
pub struct DataCharacteristics {
pub n_dimensions: usize,
pub n_samples: usize,
pub has_periodicity: bool,
pub linear_trend_strength: f64,
pub noise_level: f64,
pub length_scales: Array1<f64>,
pub dominant_frequencies: Vec<f64>,
}
#[allow(non_snake_case)]
impl AutomaticKernelConstructor {
pub fn new() -> Self {
Self::default()
}
pub fn max_components(mut self, max_components: usize) -> Self {
self.max_components = max_components;
self
}
pub fn include_periodic(mut self, include_periodic: bool) -> Self {
self.include_periodic = include_periodic;
self
}
pub fn include_linear(mut self, include_linear: bool) -> Self {
self.include_linear = include_linear;
self
}
pub fn correlation_threshold(mut self, threshold: f64) -> Self {
self.correlation_threshold = threshold;
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.random_state = random_state;
self
}
pub fn use_cross_validation(mut self, use_cross_validation: bool) -> Self {
self.use_cross_validation = use_cross_validation;
self
}
pub fn construct_kernel(
&self,
X: ArrayView2<f64>,
y: ArrayView1<f64>,
) -> SklResult<KernelConstructionResult> {
let characteristics = self.analyze_data_characteristics(&X, &y)?;
let candidate_kernels = self.generate_candidate_kernels(&characteristics)?;
let mut kernel_scores = Vec::new();
let mut best_kernel: Option<Box<dyn Kernel>> = None;
let mut best_score = f64::INFINITY;
for (name, kernel) in candidate_kernels {
let score = self.evaluate_kernel(kernel.as_ref(), &X, &y)?;
kernel_scores.push((name.clone(), score));
if score < best_score {
best_score = score;
best_kernel = Some(kernel);
}
}
let best_kernel = best_kernel
.ok_or_else(|| SklearsError::InvalidOperation("No valid kernels found".to_string()))?;
Ok(KernelConstructionResult {
best_kernel,
best_score,
kernel_scores,
data_characteristics: characteristics,
})
}
fn analyze_data_characteristics(
&self,
X: &ArrayView2<f64>,
y: &ArrayView1<f64>,
) -> SklResult<DataCharacteristics> {
let n_samples = X.nrows();
let n_dimensions = X.ncols();
let noise_level = self.estimate_noise_level(X, y)?;
let linear_trend_strength = self.detect_linear_trend(X, y)?;
let length_scales = self.estimate_length_scales(X)?;
let (has_periodicity, dominant_frequencies) = if self.include_periodic {
self.detect_periodicity(X, y)?
} else {
(false, Vec::new())
};
Ok(DataCharacteristics {
n_dimensions,
n_samples,
has_periodicity,
linear_trend_strength,
noise_level,
length_scales,
dominant_frequencies,
})
}
fn estimate_noise_level(&self, _X: &ArrayView2<f64>, y: &ArrayView1<f64>) -> SklResult<f64> {
if y.len() < 2 {
return Ok(0.1); }
let mut differences = Vec::new();
for i in 1..y.len() {
differences.push((y[i] - y[i - 1]).abs());
}
let mean_diff = differences.iter().sum::<f64>() / differences.len() as f64;
Ok(mean_diff.max(1e-6)) }
fn detect_linear_trend(&self, X: &ArrayView2<f64>, y: &ArrayView1<f64>) -> SklResult<f64> {
if X.ncols() == 0 || y.is_empty() {
return Ok(0.0);
}
let x_first = X.column(0);
let correlation = self.compute_correlation(&x_first, y)?;
Ok(correlation.abs())
}
fn compute_correlation(&self, x: &ArrayView1<f64>, y: &ArrayView1<f64>) -> SklResult<f64> {
if x.len() != y.len() || x.is_empty() {
return Ok(0.0);
}
let x_mean = x.mean().unwrap_or(0.0);
let y_mean = y.mean().unwrap_or(0.0);
let mut numerator = 0.0;
let mut x_var = 0.0;
let mut y_var = 0.0;
for i in 0..x.len() {
let x_diff = x[i] - x_mean;
let y_diff = y[i] - y_mean;
numerator += x_diff * y_diff;
x_var += x_diff * x_diff;
y_var += y_diff * y_diff;
}
let denominator = (x_var * y_var).sqrt();
if denominator < 1e-10 {
Ok(0.0)
} else {
Ok(numerator / denominator)
}
}
fn estimate_length_scales(&self, X: &ArrayView2<f64>) -> SklResult<Array1<f64>> {
let mut length_scales = Array1::zeros(X.ncols());
for dim in 0..X.ncols() {
let column = X.column(dim);
let range = column.fold(f64::NEG_INFINITY, |a, &b| a.max(b))
- column.fold(f64::INFINITY, |a, &b| a.min(b));
length_scales[dim] = (range / 10.0).max(1e-3);
}
Ok(length_scales)
}
fn detect_periodicity(
&self,
_X: &ArrayView2<f64>,
y: &ArrayView1<f64>,
) -> SklResult<(bool, Vec<f64>)> {
let mut dominant_frequencies = Vec::new();
if y.len() < 10 {
return Ok((false, dominant_frequencies));
}
let max_lag = (y.len() / 4).min(50);
let mut autocorr = Vec::new();
for lag in 1..max_lag {
let mut correlation = 0.0;
let mut count = 0;
for i in lag..y.len() {
correlation += y[i] * y[i - lag];
count += 1;
}
if count > 0 {
autocorr.push(correlation / count as f64);
}
}
let threshold = 0.3; let mut has_periodicity = false;
for i in 1..autocorr.len() - 1 {
if autocorr[i] > threshold
&& autocorr[i] > autocorr[i - 1]
&& autocorr[i] > autocorr[i + 1]
{
has_periodicity = true;
let frequency = 2.0 * std::f64::consts::PI / (i as f64 + 1.0);
dominant_frequencies.push(frequency);
}
}
Ok((has_periodicity, dominant_frequencies))
}
fn generate_candidate_kernels(
&self,
characteristics: &DataCharacteristics,
) -> SklResult<Vec<(String, Box<dyn Kernel>)>> {
let mut kernels = Vec::new();
let base_length_scale = characteristics.length_scales.mean().unwrap_or(1.0);
kernels.push((
"RBF".to_string(),
Box::new(RBF::new(base_length_scale)) as Box<dyn Kernel>,
));
if characteristics.n_dimensions > 1 {
kernels.push((
"ARD_RBF".to_string(),
Box::new(crate::kernels::ARDRBF::new(
characteristics.length_scales.clone(),
)) as Box<dyn Kernel>,
));
}
kernels.push((
"Matern_1_2".to_string(),
Box::new(Matern::new(base_length_scale, 0.5)) as Box<dyn Kernel>,
));
kernels.push((
"Matern_3_2".to_string(),
Box::new(Matern::new(base_length_scale, 1.5)) as Box<dyn Kernel>,
));
if self.include_linear && characteristics.linear_trend_strength > 0.3 {
kernels.push((
"Linear".to_string(),
Box::new(Linear::new(1.0, 1.0)) as Box<dyn Kernel>,
));
let rbf = Box::new(RBF::new(base_length_scale));
let linear = Box::new(Linear::new(1.0, 1.0));
kernels.push((
"RBF+Linear".to_string(),
Box::new(crate::kernels::SumKernel::new(vec![rbf, linear])) as Box<dyn Kernel>,
));
}
if self.include_periodic && characteristics.has_periodicity {
for &freq in &characteristics.dominant_frequencies {
let period = 2.0 * std::f64::consts::PI / freq;
kernels.push((
format!("ExpSineSquared_{:.2}", period),
Box::new(ExpSineSquared::new(base_length_scale, period)) as Box<dyn Kernel>,
));
let rbf = Box::new(RBF::new(base_length_scale));
let periodic = Box::new(ExpSineSquared::new(base_length_scale, period));
kernels.push((
format!("RBF*ExpSineSquared_{:.2}", period),
Box::new(crate::kernels::ProductKernel::new(vec![rbf, periodic]))
as Box<dyn Kernel>,
));
}
}
kernels.push((
"RationalQuadratic".to_string(),
Box::new(RationalQuadratic::new(base_length_scale, 1.0)) as Box<dyn Kernel>,
));
Ok(kernels)
}
fn evaluate_kernel(
&self,
kernel: &dyn Kernel,
X: &ArrayView2<f64>,
y: &ArrayView1<f64>,
) -> SklResult<f64> {
if self.use_cross_validation && X.nrows() > 10 {
self.cross_validate_kernel(kernel, X, y)
} else {
self.evaluate_marginal_likelihood(kernel, X, y)
}
}
#[allow(non_snake_case)]
fn evaluate_marginal_likelihood(
&self,
kernel: &dyn Kernel,
X: &ArrayView2<f64>,
y: &ArrayView1<f64>,
) -> SklResult<f64> {
let X_owned = X.to_owned();
let K = kernel.compute_kernel_matrix(&X_owned, Some(&X_owned))?;
let mut K_noisy = K;
let noise_var = 0.1; for i in 0..K_noisy.nrows() {
K_noisy[[i, i]] += noise_var;
}
match crate::utils::cholesky_decomposition(&K_noisy) {
Ok(L) => {
let mut log_det = 0.0;
for i in 0..L.nrows() {
log_det += L[[i, i]].ln();
}
log_det *= 2.0;
let y_owned = y.to_owned();
let alpha = match crate::utils::triangular_solve(&L, &y_owned) {
Ok(temp) => {
let L_T = L.t();
crate::utils::triangular_solve(&L_T.view().to_owned(), &temp)?
}
Err(_) => return Ok(f64::INFINITY), };
let data_fit = -0.5 * y.dot(&alpha);
let complexity_penalty = -0.5 * log_det;
let normalization = -0.5 * y.len() as f64 * (2.0 * std::f64::consts::PI).ln();
Ok(-(data_fit + complexity_penalty + normalization))
}
Err(_) => Ok(f64::INFINITY), }
}
#[allow(non_snake_case)]
fn cross_validate_kernel(
&self,
kernel: &dyn Kernel,
X: &ArrayView2<f64>,
y: &ArrayView1<f64>,
) -> SklResult<f64> {
let n_folds = 5.min(X.nrows() / 2);
if n_folds < 2 {
return self.evaluate_marginal_likelihood(kernel, X, y);
}
let fold_size = X.nrows() / n_folds;
let mut total_score = 0.0;
for fold in 0..n_folds {
let start_idx = fold * fold_size;
let end_idx = if fold == n_folds - 1 {
X.nrows()
} else {
(fold + 1) * fold_size
};
let mut train_indices = Vec::new();
let mut test_indices = Vec::new();
for i in 0..X.nrows() {
if i >= start_idx && i < end_idx {
test_indices.push(i);
} else {
train_indices.push(i);
}
}
if train_indices.is_empty() || test_indices.is_empty() {
continue;
}
let X_train = X.select(Axis(0), &train_indices);
let y_train = y.select(Axis(0), &train_indices);
let _X_test = X.select(Axis(0), &test_indices);
let _y_test = y.select(Axis(0), &test_indices);
let fold_score =
self.evaluate_marginal_likelihood(kernel, &X_train.view(), &y_train.view())?;
total_score += fold_score;
}
Ok(total_score / n_folds as f64)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{Array1, Array2};
#[test]
fn test_automatic_kernel_constructor_creation() {
let constructor = AutomaticKernelConstructor::new();
assert_eq!(constructor.max_components, 5);
assert!(constructor.include_periodic);
assert!(constructor.include_linear);
}
#[test]
#[allow(non_snake_case)]
fn test_data_characteristics_analysis() {
let constructor = AutomaticKernelConstructor::new();
let X = Array2::from_shape_vec(
(10, 2),
vec![
1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 6.0, 7.0, 7.0, 8.0, 8.0, 9.0,
9.0, 10.0, 10.0, 11.0,
],
)
.expect("operation should succeed");
let y = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
let characteristics = constructor
.analyze_data_characteristics(&X.view(), &y.view())
.expect("operation should succeed");
assert_eq!(characteristics.n_dimensions, 2);
assert_eq!(characteristics.n_samples, 10);
assert!(characteristics.linear_trend_strength > 0.5);
}
#[test]
#[allow(non_snake_case)]
fn test_kernel_construction() {
let constructor = AutomaticKernelConstructor::new()
.max_components(3)
.use_cross_validation(false);
let X = Array2::from_shape_vec((5, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0])
.expect("shape and data length should match");
let y = Array1::from_vec(vec![1.0, 4.0, 9.0, 16.0, 25.0]);
let result = constructor
.construct_kernel(X.view(), y.view())
.expect("operation should succeed");
assert!(result.best_score.is_finite());
assert!(!result.kernel_scores.is_empty());
}
#[test]
fn test_correlation_computation() {
let constructor = AutomaticKernelConstructor::new();
let x = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let y = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let correlation = constructor
.compute_correlation(&x.view(), &y.view())
.expect("operation should succeed");
assert!((correlation - 1.0).abs() < 1e-10);
}
#[test]
#[allow(non_snake_case)]
fn test_length_scale_estimation() {
let constructor = AutomaticKernelConstructor::new();
let X = Array2::from_shape_vec((3, 2), vec![0.0, 0.0, 5.0, 10.0, 10.0, 20.0])
.expect("shape and data length should match");
let length_scales = constructor
.estimate_length_scales(&X.view())
.expect("operation should succeed");
assert_eq!(length_scales.len(), 2);
assert!(length_scales[0] > 0.0);
assert!(length_scales[1] > 0.0);
}
}