use std::sync::Arc;
use crate::multitask::{ICMKernel, LMCKernel};
use crate::tensor_kernels::RbfKernel;
use crate::types::RbfKernelConfig;
use super::icm::KroneckerICMKernel;
use super::lmc::KroneckerLMCKernel;
use super::trait_def::MultiOutputKernel;
use super::vvgp::VvgpModel;
fn make_icm_2task() -> KroneckerICMKernel {
let base = Box::new(RbfKernel::new(RbfKernelConfig::new(1.0)).expect("valid RBF gamma"));
let cov = vec![vec![2.0, 0.5], vec![0.5, 1.5]];
KroneckerICMKernel::from_base(base, cov).expect("valid ICM kernel")
}
fn make_lmc_2task() -> KroneckerLMCKernel {
let base = Box::new(RbfKernel::new(RbfKernelConfig::new(1.0)).expect("valid RBF gamma"));
let cov = vec![vec![2.0, 0.5], vec![0.5, 1.5]];
let mut lmc = LMCKernel::new(2);
lmc.add_component(base, cov).expect("valid component");
KroneckerLMCKernel::new(lmc)
}
#[test]
fn icm_block_symmetric_psd() {
let kernel = make_icm_2task();
let x = &[0.0_f64];
let block = kernel.compute_block(x, x).expect("compute_block");
assert!(
(block[[0, 1]] - block[[1, 0]]).abs() < 1e-12,
"block must be symmetric: [[{}, {}], [{}, {}]]",
block[[0, 0]],
block[[0, 1]],
block[[1, 0]],
block[[1, 1]]
);
let det = block[[0, 0]] * block[[1, 1]] - block[[0, 1]] * block[[1, 0]];
assert!(
block[[0, 0]] > 0.0,
"diagonal block[0,0]={} must be positive",
block[[0, 0]]
);
assert!(
block[[1, 1]] > 0.0,
"diagonal block[1,1]={} must be positive",
block[[1, 1]]
);
assert!(det > 0.0, "determinant={} must be positive (PSD)", det);
}
#[test]
fn icm_block_gram_shape() {
let kernel = make_icm_2task();
let inputs: Vec<Vec<f64>> = vec![vec![0.0], vec![1.0], vec![2.0]];
let gram = kernel
.block_gram_matrix(&inputs)
.expect("block_gram_matrix");
assert_eq!(gram.shape(), &[6, 6]);
}
#[test]
fn icm_block_gram_symmetric() {
let kernel = make_icm_2task();
let inputs: Vec<Vec<f64>> = vec![vec![0.0], vec![1.0], vec![2.0]];
let gram = kernel
.block_gram_matrix(&inputs)
.expect("block_gram_matrix");
let n = gram.shape()[0];
for i in 0..n {
for j in 0..n {
assert!(
(gram[[i, j]] - gram[[j, i]]).abs() < 1e-12,
"gram[{},{}]={} != gram[{},{}]={} (not symmetric)",
i,
j,
gram[[i, j]],
j,
i,
gram[[j, i]]
);
}
}
}
#[test]
fn icm_n_outputs() {
let kernel = make_icm_2task();
assert_eq!(kernel.n_outputs(), 2);
}
#[test]
fn lmc_block_matches_icm_single_component() {
let icm = make_icm_2task();
let lmc = make_lmc_2task();
let inputs_pairs: &[(&[f64], &[f64])] = &[
(&[0.0], &[0.0]),
(&[0.0], &[1.0]),
(&[1.0], &[2.5]),
(&[-1.0], &[1.0]),
];
for (x, y) in inputs_pairs {
let block_icm = icm.compute_block(x, y).expect("ICM compute_block");
let block_lmc = lmc.compute_block(x, y).expect("LMC compute_block");
for ri in 0..2 {
for ci in 0..2 {
assert!(
(block_icm[[ri, ci]] - block_lmc[[ri, ci]]).abs() < 1e-10,
"ICM[{},{}]={} != LMC[{},{}]={} for inputs {:?}, {:?}",
ri,
ci,
block_icm[[ri, ci]],
ri,
ci,
block_lmc[[ri, ci]],
x,
y
);
}
}
}
}
#[test]
fn vvgp_posterior_mean_recovers_training_targets() {
let inputs: Vec<Vec<f64>> = vec![vec![0.0], vec![1.0], vec![2.0]];
let targets: Vec<Vec<f64>> = vec![vec![1.0, -1.0], vec![0.5, 0.5], vec![-1.0, 2.0]];
let kernel = Arc::new(make_icm_2task());
let model = VvgpModel::new(kernel, 1e-6).expect("valid VvgpModel");
let fitted = model.fit(&inputs, &targets).expect("fit");
for (inp, target) in inputs.iter().zip(&targets) {
let (mean, _cov) = fitted.predict(inp).expect("predict");
assert_eq!(mean.len(), 2, "mean length must equal n_outputs");
for p_idx in 0..2 {
assert!(
(mean[p_idx] - target[p_idx]).abs() < 0.1,
"mean[{}]={} should be close to target[{}]={} (tol=0.1)",
p_idx,
mean[p_idx],
p_idx,
target[p_idx]
);
}
}
}
#[test]
fn vvgp_predict_returns_correct_shapes() {
let inputs: Vec<Vec<f64>> = vec![vec![0.0], vec![1.0]];
let targets: Vec<Vec<f64>> = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let kernel = Arc::new(make_icm_2task());
let model = VvgpModel::new(kernel, 1e-4).expect("valid VvgpModel");
let fitted = model.fit(&inputs, &targets).expect("fit");
let x_test = vec![0.5_f64];
let (mean, cov) = fitted.predict(&x_test).expect("predict");
assert_eq!(mean.len(), 2, "mean must have length p=2");
assert_eq!(cov.shape(), &[2, 2], "covariance must be p×p");
}
#[test]
fn vvgp_covariance_diagonal_non_negative() {
let inputs: Vec<Vec<f64>> = vec![vec![0.0], vec![2.0]];
let targets: Vec<Vec<f64>> = vec![vec![1.0, -1.0], vec![-1.0, 1.0]];
let kernel = Arc::new(make_icm_2task());
let model = VvgpModel::new(kernel, 1e-4).expect("valid VvgpModel");
let fitted = model.fit(&inputs, &targets).expect("fit");
let x_test = vec![1.0_f64];
let (_mean, cov) = fitted.predict(&x_test).expect("predict");
for p_idx in 0..2 {
assert!(
cov[[p_idx, p_idx]] >= -1e-10,
"posterior variance cov[{0},{0}]={1} must be non-negative",
p_idx,
cov[[p_idx, p_idx]]
);
}
}
#[test]
fn vvgp_invalid_noise_rejected() {
let kernel = Arc::new(make_icm_2task());
assert!(
VvgpModel::new(kernel, -1.0).is_err(),
"negative noise must be rejected"
);
}
#[test]
fn vvgp_mismatched_targets_rejected() {
let kernel = Arc::new(make_icm_2task());
let model = VvgpModel::new(kernel, 1e-4).expect("valid VvgpModel");
let inputs = vec![vec![0.0], vec![1.0]];
let targets = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![0.5, 0.5]];
assert!(
model.fit(&inputs, &targets).is_err(),
"mismatched target count must be rejected"
);
}
#[test]
fn kernel_names() {
let icm = make_icm_2task();
let lmc = make_lmc_2task();
assert_eq!(icm.name(), "KroneckerICM");
assert_eq!(lmc.name(), "KroneckerLMC");
}
#[test]
fn icm_from_existing_icm_kernel() {
let base = Box::new(RbfKernel::new(RbfKernelConfig::new(1.0)).expect("valid"));
let cov = vec![vec![1.0, 0.3], vec![0.3, 1.0]];
let icm_inner = ICMKernel::new(base, cov).expect("valid ICMKernel");
let kernel = KroneckerICMKernel::new(icm_inner);
assert_eq!(kernel.n_outputs(), 2);
}