#![cfg(feature = "sparse-implicit")]
use echidna::record_multi;
use echidna_optim::linalg::lu_solve;
use echidna_optim::{
implicit_adjoint, implicit_adjoint_sparse, implicit_jacobian, implicit_jacobian_sparse,
implicit_tangent, implicit_tangent_sparse, SparseImplicitContext,
};
fn newton_root_find(
tape: &mut echidna::BytecodeTape<f64>,
z_init: &[f64],
x: &[f64],
num_states: usize,
) -> Vec<f64> {
let mut z = z_init.to_vec();
let max_iter = 100;
let tol = 1e-12;
for _ in 0..max_iter {
let mut inputs = z.clone();
inputs.extend_from_slice(x);
let jac = tape.jacobian(&inputs);
tape.forward(&inputs);
let residual = tape.output_values();
let norm: f64 = residual.iter().map(|r| r * r).sum::<f64>().sqrt();
if norm < tol {
return z;
}
let f_z: Vec<Vec<f64>> = jac.iter().map(|row| row[..num_states].to_vec()).collect();
let neg_res: Vec<f64> = residual.iter().map(|r| -r).collect();
let delta = lu_solve(&f_z, &neg_res).expect("Singular Jacobian in Newton root-find");
for i in 0..num_states {
z[i] += delta[i];
}
}
panic!("Newton root-finder did not converge");
}
#[test]
fn sparse_matches_dense_linear() {
let (mut tape, _) = record_multi(
|v| {
let z0 = v[0];
let z1 = v[1];
let x0 = v[2];
let x1 = v[3];
let one = x0 / x0;
let two = one + one;
let three = two + one;
vec![two * z0 + z1 + x0, z0 + three * z1 + x1]
},
&[0.0_f64, 0.0, 1.0, 1.0],
);
let z_star = [-0.4, -0.2];
let x = [1.0, 1.0];
let num_states = 2;
let ctx = SparseImplicitContext::new(&tape, num_states);
let dense = implicit_jacobian(&mut tape, &z_star, &x, num_states).unwrap();
let sparse = implicit_jacobian_sparse(&mut tape, &z_star, &x, &ctx).unwrap();
for i in 0..num_states {
for j in 0..x.len() {
assert!(
(dense[i][j] - sparse[i][j]).abs() < 1e-10,
"dense[{}][{}] = {}, sparse[{}][{}] = {}",
i,
j,
dense[i][j],
i,
j,
sparse[i][j]
);
}
}
}
#[test]
fn sparse_matches_dense_nonlinear() {
let (mut tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
vec![z * z * z - x]
},
&[2.0_f64, 8.0],
);
let z_star = [2.0];
let x = [8.0];
let ctx = SparseImplicitContext::new(&tape, 1);
let dense = implicit_jacobian(&mut tape, &z_star, &x, 1).unwrap();
let sparse = implicit_jacobian_sparse(&mut tape, &z_star, &x, &ctx).unwrap();
assert!(
(dense[0][0] - sparse[0][0]).abs() < 1e-10,
"dense = {}, sparse = {}",
dense[0][0],
sparse[0][0]
);
}
#[test]
fn sparse_tangent_matches_dense() {
let (mut tape, _) = record_multi(
|v| {
let z0 = v[0];
let z1 = v[1];
let x0 = v[2];
let x1 = v[3];
vec![z0 * z0 + z1 - x0, z0 * z1 - x1]
},
&[1.0_f64, 1.0, 2.0, 1.0],
);
let z_star = [1.0, 1.0];
let x = [2.0, 1.0];
let num_states = 2;
let ctx = SparseImplicitContext::new(&tape, num_states);
for x_dot in &[[1.0, 0.0], [0.0, 1.0]] {
let dense = implicit_tangent(&mut tape, &z_star, &x, x_dot, num_states).unwrap();
let sparse = implicit_tangent_sparse(&mut tape, &z_star, &x, x_dot, &ctx).unwrap();
for i in 0..num_states {
assert!(
(dense[i] - sparse[i]).abs() < 1e-10,
"x_dot={:?}, dense[{}] = {}, sparse[{}] = {}",
x_dot,
i,
dense[i],
i,
sparse[i]
);
}
}
}
#[test]
fn sparse_adjoint_matches_dense() {
let (mut tape, _) = record_multi(
|v| {
let z0 = v[0];
let z1 = v[1];
let x0 = v[2];
let x1 = v[3];
vec![z0 * z0 + z1 - x0, z0 * z1 - x1]
},
&[1.0_f64, 1.0, 2.0, 1.0],
);
let z_star = [1.0, 1.0];
let x = [2.0, 1.0];
let num_states = 2;
let ctx = SparseImplicitContext::new(&tape, num_states);
for z_bar in &[[1.0, 0.0], [0.0, 1.0]] {
let dense = implicit_adjoint(&mut tape, &z_star, &x, z_bar, num_states).unwrap();
let sparse = implicit_adjoint_sparse(&mut tape, &z_star, &x, z_bar, &ctx).unwrap();
for j in 0..x.len() {
assert!(
(dense[j] - sparse[j]).abs() < 1e-10,
"z_bar={:?}, dense[{}] = {}, sparse[{}] = {}",
z_bar,
j,
dense[j],
j,
sparse[j]
);
}
}
}
#[test]
fn sparse_singular_returns_none() {
let (mut tape, _) = record_multi(
|v| {
let z0 = v[0];
let z1 = v[1];
let x = v[2];
let one = x / x;
let two = one + one;
vec![z0 + z1 - x, two * z0 + two * z1 - two * x]
},
&[0.5_f64, 0.5, 1.0],
);
let z_star = [0.5, 0.5];
let x = [1.0];
let ctx = SparseImplicitContext::new(&tape, 2);
assert!(implicit_jacobian_sparse(&mut tape, &z_star, &x, &ctx).is_none());
assert!(implicit_tangent_sparse(&mut tape, &z_star, &x, &[1.0], &ctx).is_none());
assert!(implicit_adjoint_sparse(&mut tape, &z_star, &x, &[1.0, 0.0], &ctx).is_none());
}
#[test]
fn tridiagonal_system() {
let m = 20;
let (mut tape, _) = record_multi(
|v| {
let m = 20;
let z = &v[..m];
let x = &v[m..2 * m];
let mut f = Vec::with_capacity(m);
for i in 0..m {
let mut val = z[i] + z[i] - x[i];
if i > 0 {
val = val - z[i - 1];
}
if i < m - 1 {
val = val - z[i + 1];
}
f.push(val);
}
f
},
&vec![1.0_f64; 40],
);
let ctx = SparseImplicitContext::new(&tape, m);
let expected_fz_nnz = 3 * m - 2;
assert_eq!(
ctx.fz_nnz(),
expected_fz_nnz,
"F_z should be tridiagonal with nnz = {}, got {}",
expected_fz_nnz,
ctx.fz_nnz()
);
let x: Vec<f64> = vec![1.0; m];
let z_init: Vec<f64> = vec![1.0; m];
let z_star = newton_root_find(&mut tape, &z_init, &x, m);
let dense = implicit_jacobian(&mut tape, &z_star, &x, m).unwrap();
let sparse = implicit_jacobian_sparse(&mut tape, &z_star, &x, &ctx).unwrap();
for i in 0..m {
for j in 0..m {
assert!(
(dense[i][j] - sparse[i][j]).abs() < 1e-8,
"mismatch at [{},{}]: dense = {}, sparse = {}",
i,
j,
dense[i][j],
sparse[i][j]
);
}
}
}
#[test]
fn context_reuse() {
let (mut tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
vec![z * z * z - x]
},
&[2.0_f64, 8.0],
);
let ctx = SparseImplicitContext::new(&tape, 1);
let jac1 = implicit_jacobian_sparse(&mut tape, &[2.0], &[8.0], &ctx).unwrap();
let expected1 = 1.0 / 12.0;
let jac2 = implicit_jacobian_sparse(&mut tape, &[3.0], &[27.0], &ctx).unwrap();
let expected2 = 1.0 / 27.0;
assert!(
(jac1[0][0] - expected1).abs() < 1e-10,
"point 1: got {}, expected {}",
jac1[0][0],
expected1
);
assert!(
(jac2[0][0] - expected2).abs() < 1e-10,
"point 2: got {}, expected {}",
jac2[0][0],
expected2
);
}
#[test]
fn block_diagonal() {
let (mut tape, _) = record_multi(
|v| {
let z0 = v[0];
let z1 = v[1];
let z2 = v[2];
let z3 = v[3];
let x0 = v[4];
let x1 = v[5];
vec![z0 * z0 - x0, z1 * z1 - x0, z2 * z2 - x1, z3 * z3 - x1]
},
&[1.0_f64, 2.0, 3.0, 4.0, 1.0, 9.0],
);
let z_star = [1.0, 1.0, 3.0, 3.0]; let x = [1.0, 9.0];
let m = 4;
let ctx = SparseImplicitContext::new(&tape, m);
assert_eq!(
ctx.fz_nnz(),
m,
"F_z should be diagonal, nnz = {}",
ctx.fz_nnz()
);
let dense = implicit_jacobian(&mut tape, &z_star, &x, m).unwrap();
let sparse = implicit_jacobian_sparse(&mut tape, &z_star, &x, &ctx).unwrap();
for i in 0..m {
for j in 0..x.len() {
assert!(
(dense[i][j] - sparse[i][j]).abs() < 1e-10,
"mismatch at [{},{}]: dense = {}, sparse = {}",
i,
j,
dense[i][j],
sparse[i][j]
);
}
}
}
#[test]
#[should_panic(expected = "z_star length")]
fn dimension_mismatch_panics() {
let (mut tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
vec![z * z * z - x]
},
&[2.0_f64, 8.0],
);
let ctx = SparseImplicitContext::new(&tape, 1);
let _ = implicit_jacobian_sparse(&mut tape, &[1.0, 2.0], &[8.0], &ctx);
}