use std::sync::Arc;
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_linalg::{cholesky, solve_triangular};
use crate::error::KernelError;
use super::trait_def::MultiOutputKernel;
type Result<T> = std::result::Result<T, KernelError>;
pub struct VvgpModel {
kernel: Arc<dyn MultiOutputKernel>,
noise: f64,
}
pub struct VvgpFitted {
pub alpha: Array1<f64>,
inputs: Vec<Vec<f64>>,
chol: Array2<f64>,
kernel: Arc<dyn MultiOutputKernel>,
noise: f64,
n_outputs: usize,
}
impl VvgpModel {
pub fn new(kernel: Arc<dyn MultiOutputKernel>, noise: f64) -> Result<Self> {
if noise < 0.0 {
return Err(KernelError::InvalidParameter {
parameter: "noise".to_string(),
value: noise.to_string(),
reason: "noise variance must be >= 0".to_string(),
});
}
Ok(Self { kernel, noise })
}
pub fn fit(&self, inputs: &[Vec<f64>], targets: &[Vec<f64>]) -> Result<VvgpFitted> {
let n = inputs.len();
let p = self.kernel.n_outputs();
if targets.len() != n {
return Err(KernelError::DimensionMismatch {
expected: vec![n],
got: vec![targets.len()],
context: "VvgpModel::fit: targets.len() must equal inputs.len()".to_string(),
});
}
for (idx, t) in targets.iter().enumerate() {
if t.len() != p {
return Err(KernelError::DimensionMismatch {
expected: vec![p],
got: vec![t.len()],
context: format!("VvgpModel::fit: targets[{}] must have length p={}", idx, p),
});
}
}
let np = n * p;
let mut k_full = self.kernel.block_gram_matrix(inputs)?;
for i in 0..np {
k_full[[i, i]] += self.noise;
}
let mut y_flat = Array1::<f64>::zeros(np);
for (n_idx, target) in targets.iter().enumerate() {
for (p_idx, &v) in target.iter().enumerate() {
y_flat[n_idx * p + p_idx] = v;
}
}
let chol = cholesky(&k_full.view(), None).map_err(|e| {
KernelError::ComputationError(format!(
"VvgpModel::fit: Cholesky failed (matrix may not be PSD): {}",
e
))
})?;
let w = solve_triangular(&chol.view(), &y_flat.view(), true, false).map_err(|e| {
KernelError::ComputationError(format!(
"VvgpModel::fit: forward triangular solve failed: {}",
e
))
})?;
let chol_t = chol.t().to_owned();
let alpha = solve_triangular(&chol_t.view(), &w.view(), false, false).map_err(|e| {
KernelError::ComputationError(format!(
"VvgpModel::fit: back triangular solve failed: {}",
e
))
})?;
Ok(VvgpFitted {
alpha,
inputs: inputs.to_vec(),
chol,
kernel: Arc::clone(&self.kernel),
noise: self.noise,
n_outputs: p,
})
}
}
impl VvgpFitted {
pub fn predict(&self, x_star: &[f64]) -> Result<(Vec<f64>, Array2<f64>)> {
let n = self.inputs.len();
let p = self.n_outputs;
let np = n * p;
let mut k_star = Array2::<f64>::zeros((p, np));
for j in 0..n {
let block = self.kernel.compute_block(x_star, &self.inputs[j])?;
for ri in 0..p {
for ci in 0..p {
k_star[[ri, j * p + ci]] = block[[ri, ci]];
}
}
}
let k_ss = self.kernel.compute_block(x_star, x_star)?;
let mean_arr = k_star.dot(&self.alpha);
let mean: Vec<f64> = mean_arr.into_raw_vec_and_offset().0;
let k_star_t = k_star.t().to_owned(); let mut v = Array2::<f64>::zeros((np, p));
for col_idx in 0..p {
let col = k_star_t.column(col_idx).to_owned();
let v_col =
solve_triangular(&self.chol.view(), &col.view(), true, false).map_err(|e| {
KernelError::ComputationError(format!(
"VvgpFitted::predict: triangular solve for column {} failed: {}",
col_idx, e
))
})?;
for row_idx in 0..np {
v[[row_idx, col_idx]] = v_col[row_idx];
}
}
let cov = k_ss - v.t().dot(&v);
Ok((mean, cov))
}
pub fn n_train(&self) -> usize {
self.inputs.len()
}
pub fn n_outputs(&self) -> usize {
self.n_outputs
}
pub fn noise(&self) -> f64 {
self.noise
}
}