use num_traits::Float;
#[derive(Debug, Clone, Copy)]
pub enum DerivativeMethod {
Forward,
Backward,
Central,
Richardson,
}
pub fn derivative<T, F>(f: F, x: T, method: DerivativeMethod) -> T
where
T: Float,
F: Fn(T) -> T,
{
let h = T::from(1e-8).expect("1e-8 is representable as Float");
match method {
DerivativeMethod::Forward => {
let fx = f(x);
let fxh = f(x + h);
(fxh - fx) / h
}
DerivativeMethod::Backward => {
let fx = f(x);
let fxmh = f(x - h);
(fx - fxmh) / h
}
DerivativeMethod::Central => {
let fxh = f(x + h);
let fxmh = f(x - h);
(fxh - fxmh) / (T::from(2.0).expect("2.0 is representable as Float") * h)
}
DerivativeMethod::Richardson => richardson_derivative(f, x),
}
}
fn richardson_derivative<T, F>(f: F, x: T) -> T
where
T: Float,
F: Fn(T) -> T,
{
let h0 = T::from(0.1).expect("0.1 is representable as Float");
let con = T::from(2.0).expect("2.0 is representable as Float");
let con2 = con * con;
let ntab = 10;
let mut a = vec![vec![T::zero(); ntab]; ntab];
let mut hh = h0;
a[0][0] = (f(x + hh) - f(x - hh)) / (T::from(2.0).expect("2.0 is representable as Float") * hh);
let mut err = T::from(1e10).expect("1e10 is representable as Float");
let mut ans = a[0][0];
for i in 1..ntab {
hh = hh / con;
a[0][i] =
(f(x + hh) - f(x - hh)) / (T::from(2.0).expect("2.0 is representable as Float") * hh);
let mut fac = con2;
for j in 1..=i {
a[j][i] = (a[j - 1][i] * fac - a[j - 1][i - 1]) / (fac - T::one());
fac = fac * con2;
let errt = (a[j][i] - a[j - 1][i])
.abs()
.max((a[j][i] - a[j - 1][i - 1]).abs());
if errt < err {
err = errt;
ans = a[j][i];
}
}
if (a[i][i] - a[i - 1][i - 1]).abs()
>= err * T::from(2.0).expect("2.0 is representable as Float")
{
break;
}
}
ans
}
pub fn gradient<T, F>(f: &F, x: &[T], method: DerivativeMethod) -> Vec<T>
where
T: Float,
F: Fn(&[T]) -> T,
{
let n = x.len();
let h = T::from(1e-8).expect("1e-8 is representable as Float");
let mut grad = vec![T::zero(); n];
match method {
DerivativeMethod::Forward => {
let f0 = f(x);
for i in 0..n {
let mut x_plus = x.to_vec();
x_plus[i] = x_plus[i] + h;
grad[i] = (f(&x_plus) - f0) / h;
}
}
DerivativeMethod::Central => {
for i in 0..n {
let mut x_plus = x.to_vec();
let mut x_minus = x.to_vec();
x_plus[i] = x_plus[i] + h;
x_minus[i] = x_minus[i] - h;
grad[i] = (f(&x_plus) - f(&x_minus))
/ (T::from(2.0).expect("2.0 is representable as Float") * h);
}
}
_ => {
for i in 0..n {
let mut x_plus = x.to_vec();
let mut x_minus = x.to_vec();
x_plus[i] = x_plus[i] + h;
x_minus[i] = x_minus[i] - h;
grad[i] = (f(&x_plus) - f(&x_minus))
/ (T::from(2.0).expect("2.0 is representable as Float") * h);
}
}
}
grad
}
pub fn jacobian<T, F>(f: &F, x: &[T], method: DerivativeMethod) -> Vec<Vec<T>>
where
T: Float,
F: Fn(&[T]) -> Vec<T>,
{
let n = x.len();
let f0 = f(x);
let m = f0.len();
let h = T::from(1e-8).expect("1e-8 is representable as Float");
let mut jac = vec![vec![T::zero(); n]; m];
match method {
DerivativeMethod::Central => {
for j in 0..n {
let mut x_plus = x.to_vec();
let mut x_minus = x.to_vec();
x_plus[j] = x_plus[j] + h;
x_minus[j] = x_minus[j] - h;
let f_plus = f(&x_plus);
let f_minus = f(&x_minus);
for i in 0..m {
jac[i][j] = (f_plus[i] - f_minus[i])
/ (T::from(2.0).expect("2.0 is representable as Float") * h);
}
}
}
_ => {
for j in 0..n {
let mut x_plus = x.to_vec();
x_plus[j] = x_plus[j] + h;
let f_plus = f(&x_plus);
for i in 0..m {
jac[i][j] = (f_plus[i] - f0[i]) / h;
}
}
}
}
jac
}
pub fn hessian<T, F>(f: &F, x: &[T]) -> Vec<Vec<T>>
where
T: Float,
F: Fn(&[T]) -> T,
{
let n = x.len();
let h = T::from(1e-5).expect("1e-5 is representable as Float");
let mut hess = vec![vec![T::zero(); n]; n];
for i in 0..n {
for j in 0..=i {
let mut x_pp = x.to_vec();
let mut x_pm = x.to_vec();
let mut x_mp = x.to_vec();
let mut x_mm = x.to_vec();
x_pp[i] = x_pp[i] + h;
x_pp[j] = x_pp[j] + h;
x_pm[i] = x_pm[i] + h;
x_pm[j] = x_pm[j] - h;
x_mp[i] = x_mp[i] - h;
x_mp[j] = x_mp[j] + h;
x_mm[i] = x_mm[i] - h;
x_mm[j] = x_mm[j] - h;
let f_pp = f(&x_pp);
let f_pm = f(&x_pm);
let f_mp = f(&x_mp);
let f_mm = f(&x_mm);
hess[i][j] = (f_pp - f_pm - f_mp + f_mm)
/ (T::from(4.0).expect("4.0 is representable as Float") * h * h);
hess[j][i] = hess[i][j]; }
}
hess
}
pub fn directional_derivative<T, F>(f: &F, x: &[T], direction: &[T]) -> T
where
T: Float + std::iter::Sum,
F: Fn(&[T]) -> T,
{
let grad = gradient(f, x, DerivativeMethod::Central);
grad.iter()
.zip(direction.iter())
.map(|(&gi, &di)| gi * di)
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_derivative_polynomial() {
let f = |x: f64| x.powi(3);
let df = derivative(f, 2.0, DerivativeMethod::Central);
assert_relative_eq!(df, 12.0, epsilon = 1e-6); }
#[test]
fn test_derivative_transcendental() {
let f = |x: f64| x.sin();
let df = derivative(f, 1.0, DerivativeMethod::Central);
assert_relative_eq!(df, 1.0f64.cos(), epsilon = 1e-7);
}
#[test]
fn test_derivative_exponential() {
let f = |x: f64| x.exp();
let df = derivative(f, 2.0, DerivativeMethod::Central);
assert_relative_eq!(df, 2.0f64.exp(), epsilon = 1e-7); }
#[test]
fn test_gradient_quadratic() {
let f = |x: &[f64]| x[0] * x[0] + x[1] * x[1];
let grad = gradient(&f, &[2.0, 3.0], DerivativeMethod::Central);
assert_relative_eq!(grad[0], 4.0, epsilon = 1e-6);
assert_relative_eq!(grad[1], 6.0, epsilon = 1e-6);
}
#[test]
fn test_gradient_mixed() {
let f = |x: &[f64]| x[0] * x[0] * x[1] + x[0] * x[1] * x[1];
let grad = gradient(&f, &[2.0, 3.0], DerivativeMethod::Central);
let expected_dx = 2.0 * 2.0 * 3.0 + 3.0 * 3.0; let expected_dy = 2.0 * 2.0 + 2.0 * 2.0 * 3.0; assert_relative_eq!(grad[0], expected_dx, epsilon = 1e-5);
assert_relative_eq!(grad[1], expected_dy, epsilon = 1e-5);
}
#[test]
fn test_jacobian_linear() {
let f = |x: &[f64]| vec![x[0] + 2.0 * x[1], 3.0 * x[0] + 4.0 * x[1]];
let jac = jacobian(&f, &[1.0, 1.0], DerivativeMethod::Central);
assert_relative_eq!(jac[0][0], 1.0, epsilon = 1e-6);
assert_relative_eq!(jac[0][1], 2.0, epsilon = 1e-6);
assert_relative_eq!(jac[1][0], 3.0, epsilon = 1e-6);
assert_relative_eq!(jac[1][1], 4.0, epsilon = 1e-6);
}
#[test]
fn test_jacobian_nonlinear() {
let f = |x: &[f64]| vec![x[0] * x[0], x[0] * x[1]];
let jac = jacobian(&f, &[2.0, 3.0], DerivativeMethod::Central);
assert_relative_eq!(jac[0][0], 4.0, epsilon = 1e-6); assert_relative_eq!(jac[0][1], 0.0, epsilon = 1e-6);
assert_relative_eq!(jac[1][0], 3.0, epsilon = 1e-6); assert_relative_eq!(jac[1][1], 2.0, epsilon = 1e-6); }
#[test]
fn test_hessian_quadratic() {
let f = |x: &[f64]| x[0] * x[0] + x[1] * x[1];
let hess = hessian(&f, &[1.0, 1.0]);
assert_relative_eq!(hess[0][0], 2.0, epsilon = 1e-4);
assert_relative_eq!(hess[0][1], 0.0, epsilon = 1e-4);
assert_relative_eq!(hess[1][0], 0.0, epsilon = 1e-4);
assert_relative_eq!(hess[1][1], 2.0, epsilon = 1e-4);
}
#[test]
fn test_hessian_mixed() {
let f = |x: &[f64]| x[0] * x[0] * x[1];
let hess = hessian(&f, &[2.0, 3.0]);
assert_relative_eq!(hess[0][0], 6.0, epsilon = 1e-4); assert_relative_eq!(hess[0][1], 4.0, epsilon = 1e-4); assert_relative_eq!(hess[1][0], 4.0, epsilon = 1e-4); assert_relative_eq!(hess[1][1], 0.0, epsilon = 1e-4);
}
#[test]
fn test_directional_derivative() {
let f = |x: &[f64]| x[0] * x[0] + x[1] * x[1];
let sqrt2_inv = 1.0 / 2.0f64.sqrt();
let direction = vec![sqrt2_inv, sqrt2_inv];
let deriv = directional_derivative(&f, &[2.0, 3.0], &direction);
let expected = 10.0 / 2.0f64.sqrt();
assert_relative_eq!(deriv, expected, epsilon = 1e-6);
}
#[test]
fn test_richardson_vs_central() {
let f = |x: f64| x.powi(5);
let exact = 5.0 * 2.0f64.powi(4);
let central = derivative(f, 2.0, DerivativeMethod::Central);
let richardson = derivative(f, 2.0, DerivativeMethod::Richardson);
let err_central = (central - exact).abs();
let err_richardson = (richardson - exact).abs();
assert!(err_central < 1e-4, "Central difference should be accurate");
assert!(err_richardson < 1e-4, "Richardson should be accurate");
}
#[test]
fn test_forward_vs_central_accuracy() {
let f = |x: f64| x.sin();
let exact = 1.0f64.cos();
let forward = derivative(f, 1.0, DerivativeMethod::Forward);
let central = derivative(f, 1.0, DerivativeMethod::Central);
let err_forward = (forward - exact).abs();
let err_central = (central - exact).abs();
assert!(err_central < err_forward);
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_derivative_linear(
a in -10.0f64..10.0,
b in -10.0f64..10.0,
x in -5.0f64..5.0
) {
let f = |x: f64| a * x + b;
let df = derivative(f, x, DerivativeMethod::Central);
prop_assert!((df - a).abs() < 1e-6);
}
#[test]
fn prop_gradient_separable(
x0 in -5.0f64..5.0,
y0 in -5.0f64..5.0
) {
let f = |x: &[f64]| x[0]*x[0] + x[1]*x[1];
let grad = gradient(&f, &[x0, y0], DerivativeMethod::Central);
prop_assert!((grad[0] - 2.0 * x0).abs() < 1e-5);
prop_assert!((grad[1] - 2.0 * y0).abs() < 1e-5);
}
#[test]
fn prop_hessian_symmetry(
x in -3.0f64..3.0,
y in -3.0f64..3.0
) {
let f = |x: &[f64]| x[0].powi(3) + x[0] * x[1] * x[1] + x[1].powi(4);
let hess = hessian(&f, &[x, y]);
prop_assert!((hess[0][1] - hess[1][0]).abs() < 1e-4);
}
}
}