use echidna_optim::convergence::norm;
use echidna_optim::linalg::lu_solve;
use echidna_optim::objective::Objective;
use echidna_optim::result::TerminationReason;
use echidna_optim::{lbfgs, newton, trust_region, LbfgsConfig, NewtonConfig, TrustRegionConfig};
#[test]
fn m30_lu_solve_rejects_ill_conditioned_scale_mismatch() {
let a = vec![vec![1.0e-8, 0.0], vec![0.0, 1.0e10]];
let b = vec![1.0, 1.0];
assert!(
lu_solve(&a, &b).is_none(),
"matrix with condition number ~1e18 must be flagged singular under \
matrix-norm-scaled tolerance"
);
}
#[test]
fn m30_lu_solve_singular_large_scale() {
let a = vec![
vec![1.0e10, 2.0e10, 3.0e10],
vec![2.0e10, 4.0e10, 6.0e10],
vec![1.0e10, 1.0e10, 1.0e10],
];
let b = vec![1.0e10, 2.0e10, 3.0e10];
assert!(lu_solve(&a, &b).is_none());
}
#[test]
fn m30_lu_solve_small_scale_still_solvable() {
let a = vec![vec![1.0e-12, 0.0], vec![0.0, 1.0e-12]];
let b = vec![1.0e-12, 2.0e-12];
let x = lu_solve(&a, &b).expect("well-conditioned matrix");
assert!((x[0] - 1.0f64).abs() < 1e-6);
assert!((x[1] - 2.0f64).abs() < 1e-6);
}
struct IllScaled;
impl Objective<f64> for IllScaled {
fn dim(&self) -> usize {
2
}
fn eval_grad(&mut self, x: &[f64]) -> (f64, Vec<f64>) {
let f = x[0] * x[0] + 1e12 * x[1] * x[1];
let g = vec![2.0 * x[0], 2.0e12 * x[1]];
(f, g)
}
}
#[test]
fn m32_m33_lbfgs_converges_on_ill_scaled_quadratic() {
let mut obj = IllScaled;
let mut cfg = LbfgsConfig::<f64>::default();
cfg.convergence.max_iter = 200;
cfg.convergence.grad_tol = 1e-6;
let result = lbfgs(&mut obj, &[1.0, 1.0e-3], &cfg);
assert!(
matches!(
result.termination,
TerminationReason::GradientNorm
| TerminationReason::StepSize
| TerminationReason::FunctionChange
),
"unexpected termination: {:?}",
result.termination
);
assert!(result.gradient_norm < 1e-3);
}
struct UphillNewton;
impl Objective<f64> for UphillNewton {
fn dim(&self) -> usize {
2
}
fn eval_grad(&mut self, x: &[f64]) -> (f64, Vec<f64>) {
let f = 0.5 * x[0] * x[0] + x[1];
let g = vec![x[0], 1.0];
(f, g)
}
fn eval_hessian(&mut self, x: &[f64]) -> (f64, Vec<f64>, Vec<Vec<f64>>) {
let (f, g) = self.eval_grad(x);
(f, g, vec![vec![1.0, 0.0], vec![0.0, -1.0]])
}
}
#[test]
fn m34_newton_descent_fallback_makes_progress() {
let mut obj = UphillNewton;
let mut cfg = NewtonConfig::<f64>::default();
cfg.convergence.max_iter = 20;
let f_initial = 0.5 * 0.0_f64 * 0.0 + 0.0; let result = newton(&mut obj, &[0.0, 0.0], &cfg);
assert_ne!(
result.termination,
TerminationReason::LineSearchFailed,
"fallback must prevent LineSearchFailed at iter 0 (uphill Newton)"
);
assert!(
result.value < f_initial,
"fallback should have decreased f below {}, got {}",
f_initial,
result.value
);
}
#[test]
fn m36_norm_kahan_tight_for_long_vector() {
let v: Vec<f64> = (0..10_000).map(|_| 1.0).collect();
let n = norm(&v);
assert!(
(n - 100.0).abs() < 1e-12,
"norm of 10k ones not near 100: got {}",
n
);
}
#[test]
fn m36_norm_short_vector_still_works() {
let v = vec![3.0_f64, 4.0];
let n = norm(&v);
assert!((n - 5.0).abs() < 1e-15);
}
struct FiniteButNanHvp;
impl Objective<f64> for FiniteButNanHvp {
fn dim(&self) -> usize {
1
}
fn eval_grad(&mut self, x: &[f64]) -> (f64, Vec<f64>) {
(0.5 * x[0] * x[0], vec![x[0]])
}
fn hvp(&mut self, x: &[f64], _v: &[f64]) -> (Vec<f64>, Vec<f64>) {
(vec![x[0]], vec![f64::NAN])
}
}
#[test]
fn m47_trust_region_detects_nan_hvp_mid_iteration() {
let mut obj = FiniteButNanHvp;
let mut cfg = TrustRegionConfig::<f64>::default();
cfg.convergence.max_iter = 50;
let result = trust_region(&mut obj, &[1.0], &cfg);
assert_eq!(
result.termination,
TerminationReason::NumericalError,
"NaN HVP must yield NumericalError, not {:?}",
result.termination
);
}
struct NanFunc;
impl Objective<f64> for NanFunc {
fn dim(&self) -> usize {
1
}
fn eval_grad(&mut self, _x: &[f64]) -> (f64, Vec<f64>) {
(f64::NAN, vec![f64::NAN])
}
fn eval_hessian(&mut self, _x: &[f64]) -> (f64, Vec<f64>, Vec<Vec<f64>>) {
(f64::NAN, vec![f64::NAN], vec![vec![1.0]])
}
}
#[test]
fn m47_trust_region_detects_nan_grad_at_start() {
let mut obj = NanFunc;
let cfg = TrustRegionConfig::<f64>::default();
let result = trust_region(&mut obj, &[1.0], &cfg);
assert_eq!(result.termination, TerminationReason::NumericalError);
}