use super::*;
use crate::primitives::Vector;
#[test]
fn falsify_lbfgs_001_quadratic_convergence() {
let objective = |x: &Vector<f32>| -> f32 { x[0] * x[0] };
let gradient = |x: &Vector<f32>| -> Vector<f32> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LBFGS::new(100, 1e-6, 10);
let x0 = Vector::from_vec(vec![5.0]);
let result = lbfgs.minimize(objective, gradient, x0);
assert!(
result.solution[0].abs() < 0.01,
"FALSIFIED LBFGS-001: minimizer x={}, expected ≈ 0",
result.solution[0]
);
}
#[test]
fn falsify_lbfgs_002_objective_decreases() {
let objective = |x: &Vector<f32>| -> f32 { x[0] * x[0] + x[1] * x[1] };
let gradient =
|x: &Vector<f32>| -> Vector<f32> { Vector::from_vec(vec![2.0 * x[0], 2.0 * x[1]]) };
let x0 = Vector::from_vec(vec![3.0, 4.0]);
let initial_obj = objective(&x0);
let mut lbfgs = LBFGS::new(100, 1e-6, 10);
let result = lbfgs.minimize(objective, gradient, x0);
assert!(
result.objective_value < initial_obj,
"FALSIFIED LBFGS-002: final obj {} >= initial obj {}",
result.objective_value,
initial_obj
);
}
#[test]
fn falsify_lbfgs_003_finite_result() {
let objective = |x: &Vector<f32>| -> f32 { x[0] * x[0] };
let gradient = |x: &Vector<f32>| -> Vector<f32> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LBFGS::new(50, 1e-6, 5);
let x0 = Vector::from_vec(vec![10.0]);
let result = lbfgs.minimize(objective, gradient, x0);
assert!(
result.solution[0].is_finite(),
"FALSIFIED LBFGS-003: result x is not finite"
);
assert!(
result.objective_value.is_finite(),
"FALSIFIED LBFGS-003: objective value is not finite"
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_x0_nan_f32() {
let objective = |x: &Vector<f32>| -> f32 { x[0] * x[0] };
let gradient = |x: &Vector<f32>| -> Vector<f32> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LBFGS::new(100, 1e-6, 10);
let result = lbfgs.minimize(objective, gradient, Vector::from_vec(vec![f32::NAN]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: NaN in x0 (f32) reported {:?}, expected NumericalError",
result.status
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_x0_inf_f32() {
let objective = |x: &Vector<f32>| -> f32 { x[0] * x[0] };
let gradient = |x: &Vector<f32>| -> Vector<f32> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LBFGS::new(100, 1e-6, 10);
let result = lbfgs.minimize(objective, gradient, Vector::from_vec(vec![f32::INFINITY]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: +inf in x0 (f32) reported {:?}, expected NumericalError",
result.status
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_objective_nan_f32() {
let objective = |x: &Vector<f32>| -> f32 {
if x[0] == 1.0 {
f32::NAN
} else {
x[0] * x[0]
}
};
let gradient = |x: &Vector<f32>| -> Vector<f32> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LBFGS::new(100, 1e-6, 10);
let result = lbfgs.minimize(objective, gradient, Vector::from_vec(vec![1.0]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: NaN objective at x0 (f32) reported {:?}, expected NumericalError",
result.status
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_gradient_inf_f32() {
let objective = |x: &Vector<f32>| -> f32 { x[0] * x[0] };
let gradient = |x: &Vector<f32>| -> Vector<f32> {
if x[0] == 1.0 {
Vector::from_vec(vec![f32::INFINITY])
} else {
Vector::from_vec(vec![2.0 * x[0]])
}
};
let mut lbfgs = LBFGS::new(100, 1e-6, 10);
let result = lbfgs.minimize(objective, gradient, Vector::from_vec(vec![1.0]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: +inf gradient at x0 (f32) reported {:?}, expected NumericalError",
result.status
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_linesearch_trial_f32() {
let objective = |x: &Vector<f32>| -> f32 { x[0] * x[0] };
let gradient = |x: &Vector<f32>| -> Vector<f32> {
if x[0] == 1.0 {
Vector::from_vec(vec![2.0])
} else {
Vector::from_vec(vec![f32::INFINITY])
}
};
let mut lbfgs = LBFGS::new(100, 1e-6, 10);
let result = lbfgs.minimize(objective, gradient, Vector::from_vec(vec![1.0]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: +inf gradient at a line-search trial point (f32) reported {:?}, \
expected NumericalError — a poisoned search must not be reported as a benign tiny step",
result.status
);
}
const LSQ_A: [[f64; 4]; 6] = [
[0.13, -0.27, 0.41, 0.08],
[0.22, 0.19, -0.33, 0.47],
[-0.31, 0.44, 0.12, -0.26],
[0.38, -0.11, 0.29, 0.35],
[0.07, 0.33, -0.48, 0.21],
[-0.24, 0.16, 0.37, -0.09],
];
const LSQ_B: [f64; 6] = [0.051, -0.037, 0.083, 0.019, -0.062, 0.044];
fn lsq_obj_f32(x: &Vector<f32>) -> f32 {
let mut total = 0.0f32;
for j in 0..6 {
let mut r = 0.0f32;
for i in 0..4 {
r += LSQ_A[j][i] as f32 * x[i];
}
r -= LSQ_B[j] as f32;
total += r * r;
}
total
}
fn lsq_grad_f32(x: &Vector<f32>) -> Vector<f32> {
let mut r = [0.0f32; 6];
for j in 0..6 {
let mut acc = 0.0f32;
for i in 0..4 {
acc += LSQ_A[j][i] as f32 * x[i];
}
r[j] = acc - LSQ_B[j] as f32;
}
let mut g = vec![0.0f32; 4];
for (i, gi) in g.iter_mut().enumerate() {
let mut acc = 0.0f32;
for (j, rj) in r.iter().enumerate() {
acc += LSQ_A[j][i] as f32 * rj;
}
*gi = 2.0 * acc;
}
Vector::from_vec(g)
}
fn lsq_obj_f64(x: &Vector<f64>) -> f64 {
let mut total = 0.0f64;
for j in 0..6 {
let mut r = 0.0f64;
for i in 0..4 {
r += LSQ_A[j][i] * x[i];
}
r -= LSQ_B[j];
total += r * r;
}
total
}
fn lsq_grad_f64(x: &Vector<f64>) -> Vector<f64> {
let mut r = [0.0f64; 6];
for j in 0..6 {
let mut acc = 0.0f64;
for i in 0..4 {
acc += LSQ_A[j][i] * x[i];
}
r[j] = acc - LSQ_B[j];
}
let mut g = vec![0.0f64; 4];
for (i, gi) in g.iter_mut().enumerate() {
let mut acc = 0.0f64;
for (j, rj) in r.iter().enumerate() {
acc += LSQ_A[j][i] * rj;
}
*gi = 2.0 * acc;
}
Vector::from_vec(g)
}
#[test]
fn falsify_lbfgs_001_f64() {
let objective = |x: &Vector<f64>| -> f64 { x[0] * x[0] };
let gradient = |x: &Vector<f64>| -> Vector<f64> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LbfgsF64::new(200, 1e-10, 10);
let result = lbfgs.minimize(objective, gradient, &Vector::from_vec(vec![5.0]));
assert_eq!(
result.status,
ConvergenceStatus::Converged,
"FALSIFIED LBFGS-001-f64: status {:?}, expected Converged",
result.status
);
assert!(
result.gradient_norm < 1e-10,
"FALSIFIED LBFGS-001-f64: gradient_norm {} not below 1e-10",
result.gradient_norm
);
assert!(
result.solution[0].abs() < 1e-10,
"FALSIFIED LBFGS-001-f64: minimizer x={}, expected ≈ 0",
result.solution[0]
);
}
#[test]
fn falsify_lbfgs_001_f64_tolerance_1e6_cannot_distinguish_widths() {
let mut o32 = LBFGS::new(500, 1e-6, 10);
let r32 = o32.minimize(lsq_obj_f32, lsq_grad_f32, Vector::from_vec(vec![0.0f32; 4]));
let mut o64 = LbfgsF64::new(500, 1e-6, 10);
let r64 = o64.minimize(
lsq_obj_f64,
lsq_grad_f64,
&Vector::from_vec(vec![0.0f64; 4]),
);
assert_eq!(
r32.status,
ConvergenceStatus::Converged,
"VACUITY WITNESS broken: f32 no longer reaches 1e-6 (status {:?}, grad_norm {})",
r32.status,
r32.gradient_norm
);
assert_eq!(
r64.status,
ConvergenceStatus::Converged,
"VACUITY WITNESS broken: f64 no longer reaches 1e-6 (status {:?}, grad_norm {})",
r64.status,
r64.gradient_norm
);
}
#[test]
fn falsify_lbfgs_001_f64_1e10_width_is_real() {
let mut o32 = LBFGS::new(500, 1e-10, 10);
let r32 = o32.minimize(lsq_obj_f32, lsq_grad_f32, Vector::from_vec(vec![0.0f32; 4]));
let mut o64 = LbfgsF64::new(500, 1e-10, 10);
let r64 = o64.minimize(
lsq_obj_f64,
lsq_grad_f64,
&Vector::from_vec(vec![0.0f64; 4]),
);
assert!(
f64::from(r32.gradient_norm) > 1e-10,
"FALSIFIED LBFGS-001-f64: the f32 path reached gradient_norm {} <= 1e-10, so this test \
no longer demonstrates that the f64 width buys anything",
r32.gradient_norm
);
assert_ne!(
r32.status,
ConvergenceStatus::Converged,
"FALSIFIED LBFGS-001-f64: f32 reported Converged at tol 1e-10"
);
assert_eq!(
r64.status,
ConvergenceStatus::Converged,
"FALSIFIED LBFGS-001-f64: f64 status {:?} at tol 1e-10, expected Converged",
r64.status
);
assert!(
r64.gradient_norm < 1e-10,
"FALSIFIED LBFGS-001-f64: f64 gradient_norm {} not below 1e-10",
r64.gradient_norm
);
}
#[test]
fn falsify_lbfgs_002_f64() {
use std::cell::RefCell;
let objective = |x: &Vector<f64>| -> f64 { x[0] * x[0] + x[1] * x[1] };
let gradient =
|x: &Vector<f64>| -> Vector<f64> { Vector::from_vec(vec![2.0 * x[0], 2.0 * x[1]]) };
let x0 = Vector::from_vec(vec![3.0, 4.0]);
let initial_obj = objective(&x0);
let calls: RefCell<Vec<f64>> = RefCell::new(Vec::new());
let recorded = |x: &Vector<f64>| {
let value = objective(x);
calls.borrow_mut().push(value);
value
};
let mut lbfgs = LbfgsF64::new(200, 1e-10, 10);
let result = lbfgs.minimize(&recorded, gradient, &x0);
let calls = calls.into_inner();
let mut accepted = Vec::new();
let mut best = f64::INFINITY;
for value in &calls {
if *value < best {
best = *value;
accepted.push(*value);
}
}
assert!(
accepted.len() >= 2,
"FALSIFIED LBFGS-002-f64: only {} accepted step(s); the solver made no progress",
accepted.len()
);
for pair in accepted.windows(2) {
assert!(
pair[1] < pair[0],
"FALSIFIED LBFGS-002-f64: accepted objective did not strictly decrease: {} -> {}",
pair[0],
pair[1]
);
}
assert!(
result.objective_value < initial_obj,
"FALSIFIED LBFGS-002-f64: final obj {} >= initial obj {}",
result.objective_value,
initial_obj
);
let recomputed = objective(&result.solution);
assert!(
(recomputed - result.objective_value).abs() <= f64::EPSILON * recomputed.abs().max(1.0),
"FALSIFIED LBFGS-002-f64: reported objective_value {} does not match f(solution) {}",
result.objective_value,
recomputed
);
}
#[test]
fn falsify_lbfgs_003_f64() {
let objective = |x: &Vector<f64>| -> f64 { x[0] * x[0] + 1e10 * x[1] * x[1] };
let gradient =
|x: &Vector<f64>| -> Vector<f64> { Vector::from_vec(vec![2.0 * x[0], 2e10 * x[1]]) };
let mut lbfgs = LbfgsF64::new(100, 1e-10, 5);
let result = lbfgs.minimize(objective, gradient, &Vector::from_vec(vec![1.0, 1.0]));
assert!(
result.solution[0].is_finite() && result.solution[1].is_finite(),
"FALSIFIED LBFGS-003-f64: solution is not finite: [{}, {}]",
result.solution[0],
result.solution[1]
);
assert!(
result.objective_value.is_finite(),
"FALSIFIED LBFGS-003-f64: objective value is not finite"
);
assert!(
result.gradient_norm.is_finite(),
"FALSIFIED LBFGS-003-f64: gradient norm is not finite"
);
assert_ne!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003-f64: a FINITE pathological input was reported as NumericalError"
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_x0_nan_f64() {
let objective = |x: &Vector<f64>| -> f64 { x[0] * x[0] };
let gradient = |x: &Vector<f64>| -> Vector<f64> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LbfgsF64::new(100, 1e-10, 10);
let result = lbfgs.minimize(objective, gradient, &Vector::from_vec(vec![f64::NAN]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: NaN in x0 (f64) reported {:?}, expected NumericalError",
result.status
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_x0_inf_f64() {
let objective = |x: &Vector<f64>| -> f64 { x[0] * x[0] };
let gradient = |x: &Vector<f64>| -> Vector<f64> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LbfgsF64::new(100, 1e-10, 10);
let result = lbfgs.minimize(objective, gradient, &Vector::from_vec(vec![f64::INFINITY]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: +inf in x0 (f64) reported {:?}, expected NumericalError",
result.status
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_objective_nan_f64() {
let objective = |x: &Vector<f64>| -> f64 {
if x[0] == 1.0 {
f64::NAN
} else {
x[0] * x[0]
}
};
let gradient = |x: &Vector<f64>| -> Vector<f64> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LbfgsF64::new(100, 1e-10, 10);
let result = lbfgs.minimize(objective, gradient, &Vector::from_vec(vec![1.0]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: NaN objective at x0 (f64) reported {:?}, expected NumericalError",
result.status
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_gradient_inf_f64() {
let objective = |x: &Vector<f64>| -> f64 { x[0] * x[0] };
let gradient = |x: &Vector<f64>| -> Vector<f64> {
if x[0] == 1.0 {
Vector::from_vec(vec![f64::INFINITY])
} else {
Vector::from_vec(vec![2.0 * x[0]])
}
};
let mut lbfgs = LbfgsF64::new(100, 1e-10, 10);
let result = lbfgs.minimize(objective, gradient, &Vector::from_vec(vec![1.0]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: +inf gradient at x0 (f64) reported {:?}, expected NumericalError",
result.status
);
}
#[test]
fn falsify_lbfgs_003_nonfinite_linesearch_trial_f64() {
let objective = |x: &Vector<f64>| -> f64 { x[0] * x[0] };
let gradient = |x: &Vector<f64>| -> Vector<f64> {
if x[0] == 1.0 {
Vector::from_vec(vec![2.0])
} else {
Vector::from_vec(vec![f64::INFINITY])
}
};
let mut lbfgs = LbfgsF64::new(100, 1e-10, 10);
let result = lbfgs.minimize(objective, gradient, &Vector::from_vec(vec![1.0]));
assert_eq!(
result.status,
ConvergenceStatus::NumericalError,
"FALSIFIED LBFGS-003: +inf gradient at a line-search trial point (f64) reported {:?}, \
expected NumericalError — a poisoned search must not be reported as a benign tiny step",
result.status
);
}
mod lbfgs_proptest_falsify {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(30))]
#[test]
fn falsify_lbfgs_001_prop_quadratic_convergence(
x0_val in -50.0f32..50.0,
) {
let objective = |x: &Vector<f32>| -> f32 { x[0] * x[0] };
let gradient = |x: &Vector<f32>| -> Vector<f32> { Vector::from_vec(vec![2.0 * x[0]]) };
let mut lbfgs = LBFGS::new(100, 1e-6, 10);
let x0 = Vector::from_vec(vec![x0_val]);
let result = lbfgs.minimize(objective, gradient, x0);
prop_assert!(
result.solution[0].abs() < 1.0,
"FALSIFIED LBFGS-001-prop: x={} for start={}",
result.solution[0], x0_val
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(30))]
#[test]
fn falsify_lbfgs_002_prop_objective_decreases(
x0_val in -20.0f32..20.0,
y0_val in -20.0f32..20.0,
) {
let objective = |x: &Vector<f32>| -> f32 { x[0] * x[0] + x[1] * x[1] };
let gradient = |x: &Vector<f32>| -> Vector<f32> {
Vector::from_vec(vec![2.0 * x[0], 2.0 * x[1]])
};
let x0 = Vector::from_vec(vec![x0_val, y0_val]);
let initial_obj = objective(&x0);
let mut lbfgs = LBFGS::new(100, 1e-6, 10);
let result = lbfgs.minimize(objective, gradient, x0);
if initial_obj > 1e-10 {
prop_assert!(
result.objective_value < initial_obj,
"FALSIFIED LBFGS-002-prop: final {} >= initial {} for start=({}, {})",
result.objective_value, initial_obj, x0_val, y0_val
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(30))]
#[test]
fn falsify_lbfgs_001_f64_prop_psd_quadratic_convergence(
a in 0.5f64..5.0,
b in 0.5f64..5.0,
x0_val in -10.0f64..10.0,
y0_val in -10.0f64..10.0,
) {
let objective = |x: &Vector<f64>| -> f64 { a * x[0] * x[0] + b * x[1] * x[1] };
let gradient = |x: &Vector<f64>| -> Vector<f64> {
Vector::from_vec(vec![2.0 * a * x[0], 2.0 * b * x[1]])
};
let mut lbfgs = LbfgsF64::new(500, 1e-10, 10);
let result = lbfgs.minimize(
objective,
gradient,
&Vector::from_vec(vec![x0_val, y0_val]),
);
prop_assert_eq!(
result.status,
ConvergenceStatus::Converged,
"FALSIFIED LBFGS-001-f64-prop: status {:?} (grad_norm {}) for a={}, b={}, start=({}, {})",
result.status, result.gradient_norm, a, b, x0_val, y0_val
);
prop_assert!(
result.gradient_norm < 1e-10,
"FALSIFIED LBFGS-001-f64-prop: gradient_norm {} not below 1e-10",
result.gradient_norm
);
}
}
}