use echidna::record_multi;
use echidna_optim::{
piggyback_adjoint_solve, piggyback_forward_adjoint_solve, piggyback_tangent_solve,
PiggybackError,
};
#[test]
fn variant_adjoint_divergence_pins_to_adjoint_divergence() {
let (mut tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
let one = x / x;
let ten = one + one + one + one + one + one + one + one + one + one;
let hundred = ten * ten;
vec![hundred * z + x]
},
&[1.0_f64, 1.0],
);
let err = piggyback_adjoint_solve(&mut tape, &[1.0], &[1.0], &[1.0], 1, 200, 1e-12)
.expect_err("expected divergence");
match err {
PiggybackError::AdjointDivergence { last_norm, .. } => {
assert!(
!last_norm.is_finite(),
"AdjointDivergence via norm check must carry a non-finite last_norm (got {last_norm})"
);
}
other => panic!("expected AdjointDivergence, got {other:?}"),
}
}
#[test]
fn variant_primal_divergence_pins_to_primal_divergence() {
let (mut tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
vec![z * z + x]
},
&[0.0_f64, 1.0],
);
let err = piggyback_forward_adjoint_solve(&mut tape, &[0.0], &[1.0], &[1.0], 1, 50, 1e-12)
.expect_err("expected primal blowup");
assert!(
matches!(err, PiggybackError::PrimalDivergence { .. }),
"expected PrimalDivergence, got {err:?}"
);
}
#[test]
fn variant_iterations_exhausted_tangent_pins_shape() {
let (tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
let one = x / x;
let ten = one + one + one + one + one + one + one + one + one + one;
let hundred = ten * ten;
let ninety_nine = hundred - one;
vec![(ninety_nine / hundred) * z + x]
},
&[0.0_f64, 1.0],
);
let err = piggyback_tangent_solve(&tape, &[0.0], &[1.0], &[1.0], 1, 50, 1e-12)
.expect_err("slow contraction must not converge in 50 steps");
match err {
PiggybackError::IterationsExhaustedTangent { iteration, z_norm } => {
assert_eq!(iteration, 50, "iteration must equal max_iter on exhaustion");
assert!(
z_norm.is_finite() && z_norm > 1e-12,
"z_norm must be finite and above tol (got {z_norm})"
);
}
other => panic!("expected IterationsExhaustedTangent, got {other:?}"),
}
}
#[test]
fn variant_display_smoke_test() {
let cases = vec![
PiggybackError::PrimalDivergence {
iteration: 7,
last_norm: f64::INFINITY,
},
PiggybackError::TangentDivergence {
iteration: 3,
last_norm: 1.25e-2,
},
PiggybackError::AdjointDivergence {
iteration: 11,
last_norm: f64::NAN,
},
PiggybackError::IterationsExhaustedTangent {
iteration: 50,
z_norm: 1e-3,
},
PiggybackError::IterationsExhaustedAdjoint {
iteration: 75,
lam_norm: 2.5e-4,
},
PiggybackError::IterationsExhaustedForwardAdjoint {
iteration: 100,
z_norm: 1e-3,
lam_norm: 2.5e-4,
},
PiggybackError::DimensionMismatch {
field: "z_dot",
expected: 3,
actual: 2,
},
];
for err in &cases {
let msg = format!("{err}");
assert!(!msg.is_empty(), "empty Display for {err:?}");
assert!(msg.contains("piggyback"), "Display missing prefix: {msg}");
let _: &dyn std::error::Error = err;
}
}
#[cfg(feature = "sparse-implicit")]
#[test]
fn variant_factor_failed_pins_to_factor_failed() {
use echidna_optim::{implicit_jacobian_sparse, SparseImplicitContext, SparseImplicitError};
let (mut tape, _) = record_multi(
|v| {
let z1 = v[1];
let x = v[2];
vec![z1 - x, x - z1]
},
&[1.0_f64, 1.0, 1.0],
);
let z_star = [1.0, 1.0];
let x = [1.0];
let ctx = SparseImplicitContext::new(&tape, 2);
let err = implicit_jacobian_sparse(&mut tape, &z_star, &x, &ctx)
.expect_err("structurally singular F_z must error");
assert!(
matches!(err, SparseImplicitError::FactorSingular { .. }),
"expected FactorSingular for zero-column F_z, got {err:?}"
);
}
#[cfg(feature = "sparse-implicit")]
#[test]
fn sparse_variant_display_smoke_test() {
use echidna_optim::SparseImplicitError;
let dummy_source = || -> Box<dyn std::error::Error + Send + Sync + 'static> {
Box::new(std::io::Error::other("dummy"))
};
let cases = vec![
SparseImplicitError::StructuralSingular {
source: dummy_source(),
},
SparseImplicitError::FactorSingular {
source: dummy_source(),
},
SparseImplicitError::NumericSingular,
SparseImplicitError::ResidualExceeded {
relative_residual: 1.234e-3,
tolerance: 1.5e-8,
dimension: 42,
},
];
for err in &cases {
let msg = format!("{err}");
assert!(!msg.is_empty(), "empty Display for {err:?}");
assert!(
msg.contains("sparse_implicit"),
"Display missing prefix: {msg}"
);
let _: &dyn std::error::Error = err;
}
let residual = SparseImplicitError::ResidualExceeded {
relative_residual: 1.234e-3,
tolerance: 1.5e-8,
dimension: 42,
};
let msg = format!("{residual}");
assert!(
msg.contains("1.234e-3") || msg.contains("1.234e-03"),
"ResidualExceeded Display lost relative_residual scientific-notation: {msg}"
);
assert!(
msg.contains("1.500e-8") || msg.contains("1.500e-08"),
"ResidualExceeded Display lost tolerance scientific-notation: {msg}"
);
assert!(
msg.contains("dim = 42") || msg.contains("42"),
"ResidualExceeded Display missing dimension field: {msg}"
);
}
#[test]
fn variant_tangent_divergence_pins_to_tangent_divergence() {
#[allow(clippy::eq_op)]
let (tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
let one = x / x;
let two = one + one;
vec![(one / two) * z + x]
},
&[0.0_f64, 1.0],
);
let err = piggyback_tangent_solve(&tape, &[0.0], &[1.0], &[f64::INFINITY], 1, 50, 1e-12)
.expect_err("Inf x_dot must make tangent diverge on first step");
match err {
PiggybackError::TangentDivergence {
iteration,
last_norm,
} => {
assert_eq!(iteration, 0, "tangent divergence must fire at iteration 0");
assert!(
last_norm.is_finite(),
"primal-delta norm should be finite in the ratio-converging case (got {last_norm})"
);
}
other => panic!("expected TangentDivergence, got {other:?}"),
}
}
#[test]
fn variant_primal_divergence_carries_non_finite_last_norm() {
let (mut tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
vec![z * z + x]
},
&[0.0_f64, 1.0],
);
let err = piggyback_forward_adjoint_solve(&mut tape, &[0.0], &[1.0], &[1.0], 1, 50, 1e-12)
.expect_err("expected primal blowup");
match err {
PiggybackError::PrimalDivergence { last_norm, .. } => {
assert!(
!last_norm.is_finite(),
"PrimalDivergence via norm check must carry a non-finite last_norm (got {last_norm})"
);
}
other => panic!("expected PrimalDivergence, got {other:?}"),
}
}
#[cfg(feature = "sparse-implicit")]
#[test]
fn variant_factor_failed_exposes_faer_lu_error_via_source() {
use echidna_optim::{implicit_jacobian_sparse, SparseImplicitContext, SparseImplicitError};
let (mut tape, _) = record_multi(
|v| {
let z1 = v[1];
let x = v[2];
vec![z1 - x, x - z1]
},
&[1.0_f64, 1.0, 1.0],
);
let z_star = [1.0, 1.0];
let x = [1.0];
let ctx = SparseImplicitContext::new(&tape, 2);
let err = implicit_jacobian_sparse(&mut tape, &z_star, &x, &ctx)
.expect_err("structurally singular F_z must error");
assert!(
matches!(err, SparseImplicitError::FactorSingular { .. }),
"expected FactorSingular, got {err:?}"
);
let src = std::error::Error::source(&err).expect("FactorSingular must carry a source error");
let src_msg = format!("{src}");
assert!(
src_msg.contains("SymbolicSingular"),
"source message should surface faer's SymbolicSingular variant, got {src_msg:?}"
);
}
#[cfg(feature = "sparse-implicit")]
#[test]
fn variant_structural_failure_exposes_source_via_source_method() {
use echidna_optim::SparseImplicitError;
let err = SparseImplicitError::StructuralSingular {
source: Box::new(std::io::Error::other("synthetic triplet-builder failure")),
};
let src =
std::error::Error::source(&err).expect("StructuralSingular must carry a source error");
let src_msg = format!("{src}");
assert!(
src_msg.contains("synthetic triplet-builder failure"),
"source message should surface the boxed error's Display, got {src_msg:?}"
);
}
#[test]
fn piggyback_iterations_exhausted_display_is_clean() {
let cases = [
PiggybackError::IterationsExhaustedTangent {
iteration: 50,
z_norm: 3.4e-3,
},
PiggybackError::IterationsExhaustedAdjoint {
iteration: 75,
lam_norm: 2.5e-4,
},
PiggybackError::IterationsExhaustedForwardAdjoint {
iteration: 100,
z_norm: 3.4e-3,
lam_norm: 1.2e-2,
},
];
for err in &cases {
let msg = format!("{err}");
for leak in ["Some", "None", "Option"] {
assert!(
!msg.contains(leak),
"Display leaks Rust-internal token {leak:?}: {msg:?}"
);
}
assert!(
msg.contains("max_iter"),
"Display missing max_iter descriptor: {msg:?}"
);
}
}
#[test]
fn variant_dimension_mismatch_pins_in_implicit_tangent() {
use echidna_optim::{implicit_tangent, ImplicitError};
let (mut tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
vec![z * z - x]
},
&[2.0_f64, 4.0],
);
let err = implicit_tangent(&mut tape, &[2.0], &[4.0], &[1.0, 1.0], 1)
.expect_err("wrong x_dot length must error");
match err {
ImplicitError::DimensionMismatch {
field,
expected,
actual,
} => {
assert_eq!(field, "x_dot");
assert_eq!(expected, 1);
assert_eq!(actual, 2);
}
other => panic!("expected DimensionMismatch, got {other:?}"),
}
}
#[test]
fn variant_dimension_mismatch_pins_in_piggyback_tangent_solve() {
#[allow(clippy::eq_op)]
let (tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
let one = x / x;
let two = one + one;
vec![(one / two) * z + x]
},
&[0.0_f64, 1.0],
);
let err = piggyback_tangent_solve(&tape, &[0.0], &[1.0], &[1.0, 1.0], 1, 50, 1e-12)
.expect_err("wrong x_dot length must error");
match err {
PiggybackError::DimensionMismatch {
field,
expected,
actual,
} => {
assert_eq!(field, "x_dot");
assert_eq!(expected, 1);
assert_eq!(actual, 2);
}
other => panic!("expected DimensionMismatch, got {other:?}"),
}
}
#[cfg(feature = "sparse-implicit")]
#[test]
fn variant_dimension_mismatch_pins_in_sparse_tangent() {
use echidna_optim::{implicit_tangent_sparse, SparseImplicitContext, SparseImplicitError};
let (mut tape, _) = record_multi(
|v| {
let z = v[0];
let x = v[1];
vec![z - x]
},
&[0.0_f64, 0.0],
);
let ctx = SparseImplicitContext::new(&tape, 1);
let err = implicit_tangent_sparse(&mut tape, &[0.0, 0.0], &[0.0], &[1.0], &ctx)
.expect_err("wrong z_star length must error");
assert!(
matches!(
err,
SparseImplicitError::DimensionMismatch {
field: "z_star",
expected: 1,
actual: 2,
}
),
"expected DimensionMismatch for z_star, got {err:?}"
);
}
#[test]
fn variant_dimension_mismatch_display_has_field_name() {
use echidna_optim::ImplicitError;
let implicit_err = ImplicitError::DimensionMismatch {
field: "x_dot",
expected: 3,
actual: 5,
};
let piggyback_err = PiggybackError::DimensionMismatch {
field: "z_bar",
expected: 4,
actual: 2,
};
for (msg, prefix, field, expected, actual) in [
(
format!("{implicit_err}"),
"implicit",
"x_dot",
3usize,
5usize,
),
(format!("{piggyback_err}"), "piggyback", "z_bar", 4, 2),
] {
assert!(
msg.starts_with(&format!("{prefix}:")),
"Display missing `{prefix}:` prefix: {msg}"
);
assert!(
msg.contains(field),
"Display missing `field` name {field:?}: {msg}"
);
assert!(
msg.contains(&format!("expected {expected}")) && msg.contains(&format!("got {actual}")),
"Display missing expected/actual counts: {msg}"
);
}
}
#[cfg(feature = "sparse-implicit")]
#[test]
fn variant_dimension_mismatch_display_sparse_has_field_name() {
use echidna_optim::SparseImplicitError;
let err = SparseImplicitError::DimensionMismatch {
field: "x_dot",
expected: 3,
actual: 5,
};
let msg = format!("{err}");
assert!(
msg.starts_with("sparse_implicit:"),
"Display missing `sparse_implicit:` prefix: {msg}"
);
assert!(msg.contains("x_dot"), "Display missing `field` name: {msg}");
assert!(
msg.contains("expected 3") && msg.contains("got 5"),
"Display missing expected/actual counts: {msg}"
);
}