#![cfg(feature = "stde")]
use approx::assert_relative_eq;
use echidna::{BReverse, BytecodeTape, Scalar};
fn record_fn(f: impl FnOnce(&[BReverse<f64>]) -> BReverse<f64>, x: &[f64]) -> BytecodeTape<f64> {
let (tape, _) = echidna::record(f, x);
tape
}
fn sum_of_squares<T: Scalar>(x: &[T]) -> T {
x[0] * x[0] + x[1] * x[1]
}
fn cubic_mix<T: Scalar>(x: &[T]) -> T {
x[0] * x[0] * x[1] + x[1] * x[1] * x[1]
}
fn quartic<T: Scalar>(x: &[T]) -> T {
let x0 = x[0];
let y0 = x[1];
x0 * x0 * x0 * x0 + y0 * y0 * y0 * y0
}
#[test]
fn dense_stde_identity_is_laplacian() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let row0: Vec<f64> = vec![1.0, 0.0];
let row1: Vec<f64> = vec![0.0, 1.0];
let cholesky: Vec<&[f64]> = vec![&row0, &row1];
let z0: Vec<f64> = vec![1.0, 1.0];
let z1: Vec<f64> = vec![1.0, -1.0];
let z2: Vec<f64> = vec![-1.0, 1.0];
let z3: Vec<f64> = vec![-1.0, -1.0];
let z_vecs: Vec<&[f64]> = vec![&z0, &z1, &z2, &z3];
let dense_result = echidna::stde::dense_stde_2nd(&tape, &x, &cholesky, &z_vecs);
let (_, lap) = echidna::stde::laplacian(&tape, &x, &z_vecs);
assert_relative_eq!(dense_result.estimate, lap, epsilon = 1e-10);
}
#[test]
fn dense_stde_diagonal_scaling() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let row0: Vec<f64> = vec![2.0, 0.0];
let row1: Vec<f64> = vec![0.0, 3.0];
let cholesky: Vec<&[f64]> = vec![&row0, &row1];
let z0: Vec<f64> = vec![1.0, 1.0];
let z1: Vec<f64> = vec![1.0, -1.0];
let z2: Vec<f64> = vec![-1.0, 1.0];
let z3: Vec<f64> = vec![-1.0, -1.0];
let z_vecs: Vec<&[f64]> = vec![&z0, &z1, &z2, &z3];
let result = echidna::stde::dense_stde_2nd(&tape, &x, &cholesky, &z_vecs);
assert_relative_eq!(result.estimate, 26.0, epsilon = 1e-10);
}
#[test]
fn dense_stde_off_diagonal() {
let tape = record_fn(cubic_mix, &[1.0, 2.0, 3.0]);
let x = [1.0, 2.0, 3.0];
let row0: Vec<f64> = vec![1.0, 0.0, 0.0];
let row1: Vec<f64> = vec![0.5, 1.0, 0.0];
let row2: Vec<f64> = vec![0.0, 0.0, 1.0];
let cholesky: Vec<&[f64]> = vec![&row0, &row1, &row2];
let signs: [f64; 2] = [1.0, -1.0];
let mut vecs = Vec::new();
for &s0 in &signs {
for &s1 in &signs {
for &s2 in &signs {
vecs.push(vec![s0, s1, s2]);
}
}
}
let z_vecs: Vec<&[f64]> = vecs.iter().map(|v| v.as_slice()).collect();
let result = echidna::stde::dense_stde_2nd(&tape, &x, &cholesky, &z_vecs);
assert_relative_eq!(result.estimate, 21.0, epsilon = 1e-8);
}
#[test]
fn dense_stde_matches_parabolic() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let c0: Vec<f64> = vec![2.0, 0.0];
let c1: Vec<f64> = vec![0.0, 3.0];
let cols: Vec<&[f64]> = vec![&c0, &c1];
let (_, diffusion) = echidna::stde::parabolic_diffusion(&tape, &x, &cols);
let row0: Vec<f64> = vec![2.0, 0.0];
let row1: Vec<f64> = vec![0.0, 3.0];
let cholesky: Vec<&[f64]> = vec![&row0, &row1];
let z0: Vec<f64> = vec![1.0, 1.0];
let z1: Vec<f64> = vec![1.0, -1.0];
let z2: Vec<f64> = vec![-1.0, 1.0];
let z3: Vec<f64> = vec![-1.0, -1.0];
let z_vecs: Vec<&[f64]> = vec![&z0, &z1, &z2, &z3];
let dense_result = echidna::stde::dense_stde_2nd(&tape, &x, &cholesky, &z_vecs);
assert_relative_eq!(dense_result.estimate, 2.0 * diffusion, epsilon = 1e-10);
}
#[test]
fn dense_stde_stats_populated() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let row0: Vec<f64> = vec![1.0, 0.0];
let row1: Vec<f64> = vec![0.0, 1.0];
let cholesky: Vec<&[f64]> = vec![&row0, &row1];
let z0: Vec<f64> = vec![1.0, 1.0];
let z1: Vec<f64> = vec![1.0, -1.0];
let z_vecs: Vec<&[f64]> = vec![&z0, &z1];
let result = echidna::stde::dense_stde_2nd(&tape, &x, &cholesky, &z_vecs);
assert_eq!(result.num_samples, 2);
assert_relative_eq!(result.value, 5.0, epsilon = 1e-10);
assert_relative_eq!(result.sample_variance, 0.0, epsilon = 1e-10);
}
#[cfg(feature = "diffop")]
mod sparse_stde_tests {
use super::*;
use echidna::diffop::DiffOp;
#[test]
fn stde_sparse_full_sample_matches_exact() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let op = DiffOp::<f64>::laplacian(2);
let (_, exact) = op.eval(&tape, &x);
let dist = op.sparse_distribution();
let all_indices: Vec<usize> = (0..dist.len()).collect();
let result = echidna::stde::stde_sparse(&tape, &x, &dist, &all_indices);
assert_relative_eq!(result.estimate, exact, epsilon = 1e-6);
}
#[test]
fn stde_sparse_laplacian_convergence() {
let tape = record_fn(cubic_mix, &[1.0, 2.0, 3.0]);
let x = [1.0, 2.0, 3.0];
let op = DiffOp::<f64>::laplacian(3);
let (_, exact) = op.eval(&tape, &x);
let dist = op.sparse_distribution();
let num_samples = 1000;
let indices: Vec<usize> = (0..num_samples)
.map(|i| {
let u = ((i as u64 * 2654435761u64) % 1000) as f64 / 1000.0;
dist.sample_index(u)
})
.collect();
let result = echidna::stde::stde_sparse(&tape, &x, &dist, &indices);
let error = (result.estimate - exact).abs();
let bound = 3.0 * result.standard_error;
assert!(
error < bound || error < 1.0,
"stde_sparse estimate {} too far from exact {}: error = {}, 3*SE = {}",
result.estimate,
exact,
error,
bound,
);
}
#[test]
fn stde_sparse_diagonal_4th() {
let tape = record_fn(quartic, &[2.0, 3.0]);
let x = [2.0, 3.0];
let op = DiffOp::<f64>::diagonal(2, 4);
let (_, exact) = op.eval(&tape, &x);
assert_relative_eq!(exact, 48.0, epsilon = 1e-4);
let dist = op.sparse_distribution();
let all_indices: Vec<usize> = (0..dist.len()).collect();
let result = echidna::stde::stde_sparse(&tape, &x, &dist, &all_indices);
assert_relative_eq!(result.estimate, 48.0, epsilon = 1e-4);
}
fn sin_cos_2d<T: Scalar>(x: &[T]) -> T {
x[0].sin() * x[1].cos()
}
#[test]
fn stde_sparse_mixed_second_order() {
let tape = record_fn(sin_cos_2d, &[1.0, 2.0]);
let x = [1.0, 2.0];
let expected = -3.0 * 1.0_f64.sin() * 2.0_f64.cos();
let op = DiffOp::from_orders(
2,
&[
(1.0, &[2, 0]), (2.0, &[0, 2]), ],
);
let (_, exact) = op.eval(&tape, &x);
assert_relative_eq!(exact, expected, epsilon = 1e-6);
let dist = op.sparse_distribution();
let all_indices: Vec<usize> = (0..dist.len()).collect();
let result = echidna::stde::stde_sparse(&tape, &x, &dist, &all_indices);
assert_relative_eq!(result.estimate, expected, epsilon = 1e-6);
}
}
#[cfg(feature = "nalgebra")]
mod indefinite_stde_tests {
use super::*;
#[test]
fn indefinite_stde_matches_positive_definite() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let c = nalgebra::DMatrix::from_row_slice(2, 2, &[4.0, 1.0, 1.0, 3.0]);
let l00 = 2.0;
let l10 = 0.5;
let l11 = (3.0 - 0.25_f64).sqrt(); let row0 = vec![l00, 0.0];
let row1 = vec![l10, l11];
let cholesky: Vec<&[f64]> = vec![&row0, &row1];
let z0 = vec![1.0, 1.0];
let z1 = vec![1.0, -1.0];
let z2 = vec![-1.0, 1.0];
let z3 = vec![-1.0, -1.0];
let z_vecs: Vec<&[f64]> = vec![&z0, &z1, &z2, &z3];
let chol_result = echidna::stde::dense_stde_2nd(&tape, &x, &cholesky, &z_vecs);
let indef_result = echidna::stde::dense_stde_2nd_indefinite(&tape, &x, &c, &z_vecs, 1e-12);
assert_relative_eq!(chol_result.estimate, 14.0, epsilon = 1e-8);
assert_relative_eq!(indef_result.estimate, 14.0, epsilon = 1e-8);
}
#[test]
fn indefinite_stde_diagonal_indefinite() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let c = nalgebra::DMatrix::from_row_slice(2, 2, &[2.0, 0.0, 0.0, -3.0]);
let z0 = vec![1.0, 1.0];
let z1 = vec![1.0, -1.0];
let z2 = vec![-1.0, 1.0];
let z3 = vec![-1.0, -1.0];
let z_vecs: Vec<&[f64]> = vec![&z0, &z1, &z2, &z3];
let result = echidna::stde::dense_stde_2nd_indefinite(&tape, &x, &c, &z_vecs, 1e-12);
assert_relative_eq!(result.estimate, -2.0, epsilon = 1e-8);
}
#[test]
fn indefinite_stde_full_indefinite() {
let tape = record_fn(cubic_mix, &[1.0, 2.0, 3.0]);
let x = [1.0, 2.0, 3.0];
let c = nalgebra::DMatrix::from_row_slice(
3,
3,
&[1.0, 0.0, -1.0, 0.0, -2.0, 0.0, -1.0, 0.0, 3.0],
);
let expected = -20.0;
let signs: Vec<Vec<f64>> = vec![
vec![1.0, 1.0, 1.0],
vec![1.0, 1.0, -1.0],
vec![1.0, -1.0, 1.0],
vec![1.0, -1.0, -1.0],
vec![-1.0, 1.0, 1.0],
vec![-1.0, 1.0, -1.0],
vec![-1.0, -1.0, 1.0],
vec![-1.0, -1.0, -1.0],
];
let z_vecs: Vec<&[f64]> = signs.iter().map(|v| v.as_slice()).collect();
let result = echidna::stde::dense_stde_2nd_indefinite(&tape, &x, &c, &z_vecs, 1e-12);
assert_relative_eq!(result.estimate, expected, epsilon = 1e-6);
}
#[test]
fn indefinite_stde_all_negative() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let c = nalgebra::DMatrix::from_row_slice(2, 2, &[-2.0, 0.0, 0.0, -3.0]);
let z0 = vec![1.0, 1.0];
let z1 = vec![1.0, -1.0];
let z2 = vec![-1.0, 1.0];
let z3 = vec![-1.0, -1.0];
let z_vecs: Vec<&[f64]> = vec![&z0, &z1, &z2, &z3];
let result = echidna::stde::dense_stde_2nd_indefinite(&tape, &x, &c, &z_vecs, 1e-12);
assert_relative_eq!(result.estimate, -10.0, epsilon = 1e-8);
}
#[test]
fn indefinite_stde_zero_matrix() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let c = nalgebra::DMatrix::zeros(2, 2);
let z0 = vec![1.0, 1.0];
let z1 = vec![1.0, -1.0];
let z_vecs: Vec<&[f64]> = vec![&z0, &z1];
let result = echidna::stde::dense_stde_2nd_indefinite(&tape, &x, &c, &z_vecs, 1e-12);
assert_relative_eq!(result.estimate, 0.0, epsilon = 1e-10);
}
#[test]
fn indefinite_stde_near_zero_eigenvalue() {
let tape = record_fn(sum_of_squares, &[1.0, 2.0]);
let x = [1.0, 2.0];
let c = nalgebra::DMatrix::from_row_slice(2, 2, &[1.0, 0.0, 0.0, 1e-15]);
let z0 = vec![1.0, 1.0];
let z1 = vec![1.0, -1.0];
let z2 = vec![-1.0, 1.0];
let z3 = vec![-1.0, -1.0];
let z_vecs: Vec<&[f64]> = vec![&z0, &z1, &z2, &z3];
let result = echidna::stde::dense_stde_2nd_indefinite(&tape, &x, &c, &z_vecs, 1e-12);
assert_relative_eq!(result.estimate, 2.0, epsilon = 1e-8);
}
}