#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FiniteDiffMethod {
Forward,
Backward,
Central,
FivePoint,
}
pub fn numerical_gradient(
f: &impl Fn(&[f64]) -> f64,
x: &[f64],
method: FiniteDiffMethod,
eps: f64,
) -> Vec<f64> {
let n = x.len();
let mut grad = vec![0.0_f64; n];
let mut xp = x.to_vec();
for i in 0..n {
let orig = xp[i];
grad[i] = match method {
FiniteDiffMethod::Forward => {
xp[i] = orig + eps;
let fp = f(&xp);
xp[i] = orig;
let f0 = f(&xp);
(fp - f0) / eps
}
FiniteDiffMethod::Backward => {
xp[i] = orig;
let f0 = f(&xp);
xp[i] = orig - eps;
let fm = f(&xp);
xp[i] = orig;
(f0 - fm) / eps
}
FiniteDiffMethod::Central => {
xp[i] = orig + eps;
let fp = f(&xp);
xp[i] = orig - eps;
let fm = f(&xp);
xp[i] = orig;
(fp - fm) / (2.0 * eps)
}
FiniteDiffMethod::FivePoint => {
xp[i] = orig + 2.0 * eps;
let fp2 = f(&xp);
xp[i] = orig + eps;
let fp1 = f(&xp);
xp[i] = orig - eps;
let fm1 = f(&xp);
xp[i] = orig - 2.0 * eps;
let fm2 = f(&xp);
xp[i] = orig;
(-fp2 + 8.0 * fp1 - 8.0 * fm1 + fm2) / (12.0 * eps)
}
};
xp[i] = orig; }
grad
}
pub fn numerical_jacobian(
f: &impl Fn(&[f64]) -> Vec<f64>,
x: &[f64],
method: FiniteDiffMethod,
eps: f64,
) -> Vec<Vec<f64>> {
let n = x.len();
let f0 = f(x);
let m = f0.len();
let mut jac = vec![vec![0.0_f64; n]; m];
let mut xp = x.to_vec();
for j in 0..n {
let orig = xp[j];
match method {
FiniteDiffMethod::Forward => {
xp[j] = orig + eps;
let fp = f(&xp);
xp[j] = orig;
for i in 0..m {
jac[i][j] = (fp[i] - f0[i]) / eps;
}
}
FiniteDiffMethod::Backward => {
xp[j] = orig - eps;
let fm = f(&xp);
xp[j] = orig;
for i in 0..m {
jac[i][j] = (f0[i] - fm[i]) / eps;
}
}
FiniteDiffMethod::Central => {
xp[j] = orig + eps;
let fp = f(&xp);
xp[j] = orig - eps;
let fm = f(&xp);
xp[j] = orig;
for i in 0..m {
jac[i][j] = (fp[i] - fm[i]) / (2.0 * eps);
}
}
FiniteDiffMethod::FivePoint => {
xp[j] = orig + 2.0 * eps;
let fp2 = f(&xp);
xp[j] = orig + eps;
let fp1 = f(&xp);
xp[j] = orig - eps;
let fm1 = f(&xp);
xp[j] = orig - 2.0 * eps;
let fm2 = f(&xp);
xp[j] = orig;
for i in 0..m {
jac[i][j] =
(-fp2[i] + 8.0 * fp1[i] - 8.0 * fm1[i] + fm2[i]) / (12.0 * eps);
}
}
}
xp[j] = orig; }
jac
}
pub fn numerical_hessian(f: &impl Fn(&[f64]) -> f64, x: &[f64], eps: f64) -> Vec<Vec<f64>> {
let n = x.len();
let f0 = f(x);
let mut hess = vec![vec![0.0_f64; n]; n];
let mut xp = x.to_vec();
for i in 0..n {
let orig_i = xp[i];
xp[i] = orig_i + eps;
let fp = f(&xp);
xp[i] = orig_i - eps;
let fm = f(&xp);
xp[i] = orig_i;
hess[i][i] = (fp - 2.0 * f0 + fm) / (eps * eps);
for j in (i + 1)..n {
let orig_j = xp[j];
xp[i] = orig_i + eps;
xp[j] = orig_j + eps;
let fpp = f(&xp);
xp[i] = orig_i + eps;
xp[j] = orig_j - eps;
let fpm = f(&xp);
xp[i] = orig_i - eps;
xp[j] = orig_j + eps;
let fmp = f(&xp);
xp[i] = orig_i - eps;
xp[j] = orig_j - eps;
let fmm = f(&xp);
xp[i] = orig_i;
xp[j] = orig_j;
let hij = (fpp - fpm - fmp + fmm) / (4.0 * eps * eps);
hess[i][j] = hij;
hess[j][i] = hij; }
}
hess
}
pub fn check_gradient(
f: &impl Fn(&[f64]) -> f64,
grad: &impl Fn(&[f64]) -> Vec<f64>,
x: &[f64],
eps: f64,
rtol: f64,
atol: f64,
) -> bool {
let analytic = grad(x);
let numeric = numerical_gradient(f, x, FiniteDiffMethod::Central, eps);
if analytic.len() != numeric.len() {
return false;
}
analytic.iter().zip(numeric.iter()).all(|(&a, &n)| {
let diff = (a - n).abs();
let scale = a.abs().max(n.abs());
diff <= atol + rtol * scale
})
}
pub fn richardson_gradient(
f: &impl Fn(&[f64]) -> f64,
x: &[f64],
n_steps: usize,
eps0: f64,
) -> Vec<f64> {
let n_steps = n_steps.max(1);
let n = x.len();
let mut grad = vec![0.0_f64; n];
let mut xp = x.to_vec();
for k in 0..n {
let orig = xp[k];
let mut d = Vec::with_capacity(n_steps);
let mut h = eps0;
for _ in 0..n_steps {
xp[k] = orig + h;
let fp = f(&xp);
xp[k] = orig - h;
let fm = f(&xp);
xp[k] = orig;
d.push((fp - fm) / (2.0 * h));
h *= 0.5;
}
let mut power = 4.0_f64; for _level in 1..n_steps {
let len = d.len() - 1;
for i in 0..len {
d[i] = (power * d[i + 1] - d[i]) / (power - 1.0);
}
d.pop();
power *= 4.0; }
grad[k] = d.first().copied().unwrap_or(0.0);
xp[k] = orig; }
grad
}
pub fn complex_step_gradient(
f_complex: &impl Fn(&[f64], &[f64]) -> (f64, f64),
x_real: &[f64],
eps: f64,
) -> Vec<f64> {
let n = x_real.len();
let mut grad = vec![0.0_f64; n];
let mut im_inputs = vec![0.0_f64; n];
for k in 0..n {
im_inputs[k] = eps;
let (_, im_out) = f_complex(x_real, &im_inputs);
grad[k] = im_out / eps;
im_inputs[k] = 0.0; }
grad
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_numerical_gradient_forward_quadratic() {
let f = |xs: &[f64]| xs[0] * xs[0] + xs[1] * xs[1];
let g = numerical_gradient(&f, &[3.0, 4.0], FiniteDiffMethod::Forward, 1e-6);
assert!((g[0] - 6.0).abs() < 1e-3, "g[0]={}", g[0]);
assert!((g[1] - 8.0).abs() < 1e-3, "g[1]={}", g[1]);
}
#[test]
fn test_numerical_gradient_backward_quadratic() {
let f = |xs: &[f64]| xs[0] * xs[0] + xs[1] * xs[1];
let g = numerical_gradient(&f, &[3.0, 4.0], FiniteDiffMethod::Backward, 1e-6);
assert!((g[0] - 6.0).abs() < 1e-3, "g[0]={}", g[0]);
assert!((g[1] - 8.0).abs() < 1e-3, "g[1]={}", g[1]);
}
#[test]
fn test_numerical_gradient_central_quadratic() {
let f = |xs: &[f64]| xs[0] * xs[0] + xs[1] * xs[1];
let g = numerical_gradient(&f, &[3.0, 4.0], FiniteDiffMethod::Central, 1e-6);
assert!((g[0] - 6.0).abs() < 1e-5, "g[0]={}", g[0]);
assert!((g[1] - 8.0).abs() < 1e-5, "g[1]={}", g[1]);
}
#[test]
fn test_numerical_gradient_five_point_trig() {
let f = |xs: &[f64]| xs[0].sin();
let g = numerical_gradient(&f, &[1.0], FiniteDiffMethod::FivePoint, 1e-5);
assert!((g[0] - 1.0_f64.cos()).abs() < 1e-9, "g[0]={}", g[0]);
}
#[test]
fn test_numerical_jacobian_linear() {
let f = |xs: &[f64]| vec![xs[0] + xs[1], 2.0 * xs[0] - xs[1]];
let j = numerical_jacobian(&f, &[1.0, 2.0], FiniteDiffMethod::Central, 1e-6);
assert!((j[0][0] - 1.0).abs() < 1e-5);
assert!((j[0][1] - 1.0).abs() < 1e-5);
assert!((j[1][0] - 2.0).abs() < 1e-5);
assert!((j[1][1] + 1.0).abs() < 1e-5);
}
#[test]
fn test_numerical_jacobian_nonlinear() {
let f = |xs: &[f64]| vec![xs[0] + xs[1], xs[0] * xs[1]];
let j = numerical_jacobian(&f, &[2.0, 3.0], FiniteDiffMethod::Central, 1e-6);
assert!((j[1][0] - 3.0).abs() < 1e-4, "j[1][0]={}", j[1][0]);
assert!((j[1][1] - 2.0).abs() < 1e-4, "j[1][1]={}", j[1][1]);
}
#[test]
fn test_numerical_hessian_quadratic() {
let f = |xs: &[f64]| xs[0].powi(2) + xs[0] * xs[1] + 2.0 * xs[1].powi(2);
let h = numerical_hessian(&f, &[1.0, 1.0], 1e-4);
assert!((h[0][0] - 2.0).abs() < 1e-3, "H[0,0]={}", h[0][0]);
assert!((h[0][1] - 1.0).abs() < 1e-3, "H[0,1]={}", h[0][1]);
assert!((h[1][0] - 1.0).abs() < 1e-3, "H[1,0]={}", h[1][0]);
assert!((h[1][1] - 4.0).abs() < 1e-3, "H[1,1]={}", h[1][1]);
}
#[test]
fn test_numerical_hessian_symmetric() {
let f = |xs: &[f64]| xs[0].powi(3) * xs[1] + xs[1].powi(3);
let h = numerical_hessian(&f, &[1.0, 2.0], 1e-4);
assert!(
(h[0][1] - h[1][0]).abs() < 1e-3,
"H not symmetric: {} vs {}",
h[0][1],
h[1][0]
);
}
#[test]
fn test_check_gradient_correct() {
let f = |xs: &[f64]| xs[0].sin() + xs[1].cos();
let grad_f = |xs: &[f64]| vec![xs[0].cos(), -xs[1].sin()];
assert!(check_gradient(&f, &grad_f, &[0.5, 1.2], 1e-6, 1e-4, 1e-6));
}
#[test]
fn test_check_gradient_wrong_fails() {
let f = |xs: &[f64]| xs[0].sin();
let correct_grad = |xs: &[f64]| vec![xs[0].cos()];
let wrong_grad = |xs: &[f64]| vec![xs[0].sin()]; let correct = check_gradient(&f, &correct_grad, &[0.5], 1e-6, 1e-3, 1e-6);
assert!(correct, "correct gradient should pass check");
let wrong = check_gradient(&f, &wrong_grad, &[0.5], 1e-6, 1e-4, 1e-6);
assert!(!wrong, "wrong gradient should fail check");
}
#[test]
fn test_richardson_gradient_sin() {
let f = |xs: &[f64]| xs[0].sin();
let g = richardson_gradient(&f, &[1.0], 5, 0.1);
let expected = 1.0_f64.cos();
assert!((g[0] - expected).abs() < 1e-11, "g[0]={}", g[0]);
}
#[test]
fn test_richardson_gradient_poly() {
let f = |xs: &[f64]| xs[0].powi(5);
let g = richardson_gradient(&f, &[2.0], 6, 0.5);
assert!((g[0] - 80.0).abs() < 1e-8, "g[0]={}", g[0]);
}
#[test]
fn test_complex_step_gradient_poly() {
let f_cx = |re: &[f64], im: &[f64]| {
let re_out = re[0].powi(3) + re[1].powi(2);
let im_out = 3.0 * re[0].powi(2) * im[0] + 2.0 * re[1] * im[1];
(re_out, im_out)
};
let x = vec![2.0_f64, 3.0];
let g = complex_step_gradient(&f_cx, &x, 1e-20);
assert!((g[0] - 12.0).abs() < 1e-10, "df/dx0={}", g[0]);
assert!((g[1] - 6.0).abs() < 1e-10, "df/dx1={}", g[1]);
}
#[test]
fn test_complex_step_gradient_exp() {
let f_cx = |re: &[f64], im: &[f64]| {
let e = re[0].exp();
(e, e * im[0])
};
let g = complex_step_gradient(&f_cx, &[1.0], 1e-20);
assert!((g[0] - 1.0_f64.exp()).abs() < 1e-12, "g[0]={}", g[0]);
}
}