use crate::error::AutogradError;
use crate::Result as AgResult;
const H: f64 = 1e-4;
pub fn mixed_partial<F>(f: F, x: &[f64], axes: &[usize]) -> AgResult<f64>
where
F: Fn(&[f64]) -> f64,
{
let n = x.len();
for &ax in axes {
if ax >= n {
return Err(AutogradError::invalid_argument(format!(
"mixed_partial: axis {ax} out of bounds for input of length {n}"
)));
}
}
if axes.is_empty() {
return Ok(f(x));
}
Ok(mixed_partial_impl(&f, x, axes))
}
fn mixed_partial_impl(f: &dyn Fn(&[f64]) -> f64, x: &[f64], axes: &[usize]) -> f64 {
if axes.is_empty() {
return f(x);
}
let ax = axes[0];
let remaining = &axes[1..];
let mut xp = x.to_vec();
let mut xm = x.to_vec();
xp[ax] = x[ax] + H;
xm[ax] = x[ax] - H;
let fp = mixed_partial_impl(f, &xp, remaining);
let fm = mixed_partial_impl(f, &xm, remaining);
(fp - fm) / (2.0 * H)
}
pub fn laplacian<F>(f: F, x: &[f64]) -> AgResult<f64>
where
F: Fn(&[f64]) -> f64,
{
if x.is_empty() {
return Err(AutogradError::invalid_argument(
"laplacian: input must be non-empty".to_string(),
));
}
let fx = f(x);
let h2 = H * H;
let n = x.len();
let mut sum = 0.0f64;
let mut xp = x.to_vec();
let mut xm = x.to_vec();
for i in 0..n {
xp[i] = x[i] + H;
xm[i] = x[i] - H;
sum += (f(&xp) + f(&xm) - 2.0 * fx) / h2;
xp[i] = x[i];
xm[i] = x[i];
}
Ok(sum)
}
pub fn laplacian_stochastic<F>(f: F, x: &[f64], m: usize, seed: u64) -> AgResult<f64>
where
F: Fn(&[f64]) -> f64,
{
if x.is_empty() {
return Err(AutogradError::invalid_argument(
"laplacian_stochastic: input must be non-empty".to_string(),
));
}
if m == 0 {
return Err(AutogradError::invalid_argument(
"laplacian_stochastic: m must be positive".to_string(),
));
}
let n = x.len();
let h2 = H * H;
let mut total = 0.0f64;
let mut rng_state = seed.wrapping_add(1);
let lcg_next = |s: &mut u64| -> u64 {
*s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
*s
};
for _ in 0..m {
let v: Vec<f64> = (0..n)
.map(|_| if lcg_next(&mut rng_state) & 1 == 0 { 1.0 } else { -1.0 })
.collect();
let xp: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi + H * vi).collect();
let xm: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi - H * vi).collect();
let fx = f(x);
let estimate = (f(&xp) + f(&xm) - 2.0 * fx) / h2;
total += estimate;
}
Ok(total / m as f64)
}
pub fn hessian_diagonal<F>(f: F, x: &[f64]) -> AgResult<Vec<f64>>
where
F: Fn(&[f64]) -> f64,
{
if x.is_empty() {
return Err(AutogradError::invalid_argument(
"hessian_diagonal: input must be non-empty".to_string(),
));
}
let n = x.len();
let fx = f(x);
let h2 = H * H;
let mut diag = vec![0.0f64; n];
let mut xp = x.to_vec();
let mut xm = x.to_vec();
for i in 0..n {
xp[i] = x[i] + H;
xm[i] = x[i] - H;
diag[i] = (f(&xp) + f(&xm) - 2.0 * fx) / h2;
xp[i] = x[i];
xm[i] = x[i];
}
Ok(diag)
}
pub fn hvp<F>(f: F, x: &[f64], v: &[f64]) -> AgResult<Vec<f64>>
where
F: Fn(&[f64]) -> f64,
{
let n = x.len();
if n == 0 {
return Err(AutogradError::invalid_argument(
"hvp: input must be non-empty".to_string(),
));
}
if v.len() != n {
return Err(AutogradError::invalid_argument(format!(
"hvp: v has length {} but x has length {n}",
v.len()
)));
}
let grad_fd = |pt: &[f64]| -> Vec<f64> {
let mut g = vec![0.0f64; n];
let mut pp = pt.to_vec();
let mut pm = pt.to_vec();
for i in 0..n {
pp[i] = pt[i] + H;
pm[i] = pt[i] - H;
g[i] = (f(&pp) - f(&pm)) / (2.0 * H);
pp[i] = pt[i];
pm[i] = pt[i];
}
g
};
let xp: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi + H * vi).collect();
let xm: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi - H * vi).collect();
let gp = grad_fd(&xp);
let gm = grad_fd(&xm);
let hv: Vec<f64> = gp
.iter()
.zip(gm.iter())
.map(|(&a, &b)| (a - b) / (2.0 * H))
.collect();
Ok(hv)
}
pub fn taylor_coefficients<F>(
f: F,
x: &[f64],
v: &[f64],
order: usize,
) -> AgResult<Vec<f64>>
where
F: Fn(&[f64]) -> f64,
{
if x.is_empty() {
return Err(AutogradError::invalid_argument(
"taylor_coefficients: input must be non-empty".to_string(),
));
}
if v.len() != x.len() {
return Err(AutogradError::invalid_argument(format!(
"taylor_coefficients: v has length {} but x has length {}",
v.len(),
x.len()
)));
}
if order > 8 {
return Err(AutogradError::invalid_argument(
"taylor_coefficients: order > 8 would require too many function evaluations"
.to_string(),
));
}
let mut coeffs = vec![0.0f64; order + 1];
for k in 0..=order {
let raw = directional_derivative_recursive(&f, x, v, k);
let k_factorial: f64 = (1..=k).map(|i| i as f64).product();
coeffs[k] = raw / k_factorial;
}
Ok(coeffs)
}
fn directional_derivative_recursive(
f: &dyn Fn(&[f64]) -> f64,
x: &[f64],
v: &[f64],
k: usize,
) -> f64 {
match k {
0 => f(x),
1 => {
let xp: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi + H * vi).collect();
let xm: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi - H * vi).collect();
(f(&xp) - f(&xm)) / (2.0 * H)
}
n => {
let xp: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi + H * vi).collect();
let xm: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi - H * vi).collect();
let dp = directional_derivative_recursive(f, &xp, v, n - 1);
let dm = directional_derivative_recursive(f, &xm, v, n - 1);
(dp - dm) / (2.0 * H)
}
}
}
pub fn nth_derivative_scalar<F>(f: F, x: f64, n: usize) -> AgResult<f64>
where
F: Fn(f64) -> f64,
{
if n == 0 {
return Ok(f(x));
}
if n > 10 {
return Err(AutogradError::invalid_argument(
"nth_derivative_scalar: n > 10 is numerically unreliable".to_string(),
));
}
let h_ord = H.powf(1.0 / n as f64).max(1e-6_f64.powf(1.0 / n as f64));
let h_step = match n {
1 => 1e-5_f64,
2 => 1e-4_f64,
3 | 4 => 1e-3_f64,
_ => h_ord,
};
let h_n = h_step.powi(n as i32);
let mut sum = 0.0f64;
for k in 0..=(n as u32) {
let binom = binomial(n as u32, k);
let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
let shift = (n as f64 / 2.0 - k as f64) * h_step;
sum += sign * binom as f64 * f(x + shift);
}
Ok(sum / h_n)
}
fn binomial(n: u32, k: u32) -> u64 {
if k > n {
return 0;
}
if k == 0 || k == n {
return 1;
}
let k = k.min(n - k); let mut result = 1u64;
for i in 0..k {
result = result * (n - i) as u64 / (i + 1) as u64;
}
result
}
pub fn iterated_gradient<F>(f: F, x: &[f64], k: usize) -> AgResult<Vec<f64>>
where
F: Fn(&[f64]) -> f64 + Clone,
{
if x.is_empty() {
return Err(AutogradError::invalid_argument(
"iterated_gradient: input must be non-empty".to_string(),
));
}
if k > 3 {
return Err(AutogradError::invalid_argument(
"iterated_gradient: k > 3 would produce n^k outputs and is prohibitively expensive"
.to_string(),
));
}
match k {
0 => Ok(vec![f(x)]),
1 => {
let n = x.len();
let mut g = vec![0.0f64; n];
let mut xp = x.to_vec();
let mut xm = x.to_vec();
for i in 0..n {
xp[i] = x[i] + H;
xm[i] = x[i] - H;
g[i] = (f(&xp) - f(&xm)) / (2.0 * H);
xp[i] = x[i];
xm[i] = x[i];
}
Ok(g)
}
2 => {
let n = x.len();
let fx = f(x);
let h2 = H * H;
let mut mat = vec![0.0f64; n * n];
let mut xp = x.to_vec();
let mut xm = x.to_vec();
for i in 0..n {
xp[i] = x[i] + H;
xm[i] = x[i] - H;
mat[i * n + i] = (f(&xp) + f(&xm) - 2.0 * fx) / h2;
xp[i] = x[i];
xm[i] = x[i];
}
for i in 0..n {
for j in (i + 1)..n {
let mut xpp = x.to_vec();
let mut xpm = x.to_vec();
let mut xmp = x.to_vec();
let mut xmm = x.to_vec();
xpp[i] += H;
xpp[j] += H;
xpm[i] += H;
xpm[j] -= H;
xmp[i] -= H;
xmp[j] += H;
xmm[i] -= H;
xmm[j] -= H;
let val = (f(&xpp) - f(&xpm) - f(&xmp) + f(&xmm)) / (4.0 * h2);
mat[i * n + j] = val;
mat[j * n + i] = val;
}
}
Ok(mat)
}
3 => {
let n = x.len();
let mut tensor = vec![0.0f64; n * n * n];
let mut xp = x.to_vec();
let mut xm = x.to_vec();
for k_ax in 0..n {
xp[k_ax] = x[k_ax] + H;
xm[k_ax] = x[k_ax] - H;
let hess_p = hessian_matrix(&f, &xp);
let hess_m = hessian_matrix(&f, &xm);
xp[k_ax] = x[k_ax];
xm[k_ax] = x[k_ax];
for i in 0..n {
for j in 0..n {
tensor[i * n * n + j * n + k_ax] =
(hess_p[i][j] - hess_m[i][j]) / (2.0 * H);
}
}
}
Ok(tensor)
}
_ => unreachable!(),
}
}
fn hessian_matrix(f: &dyn Fn(&[f64]) -> f64, x: &[f64]) -> Vec<Vec<f64>> {
let n = x.len();
let fx = f(x);
let h2 = H * H;
let mut hess = vec![vec![0.0f64; n]; n];
let mut xp = x.to_vec();
let mut xm = x.to_vec();
for i in 0..n {
xp[i] = x[i] + H;
xm[i] = x[i] - H;
hess[i][i] = (f(&xp) + f(&xm) - 2.0 * fx) / h2;
xp[i] = x[i];
xm[i] = x[i];
}
for i in 0..n {
for j in (i + 1)..n {
let mut xpp = x.to_vec();
let mut xpm = x.to_vec();
let mut xmp = x.to_vec();
let mut xmm = x.to_vec();
xpp[i] += H;
xpp[j] += H;
xpm[i] += H;
xpm[j] -= H;
xmp[i] -= H;
xmp[j] += H;
xmm[i] -= H;
xmm[j] -= H;
let val = (f(&xpp) - f(&xpm) - f(&xmp) + f(&xmm)) / (4.0 * h2);
hess[i][j] = val;
hess[j][i] = val;
}
}
hess
}
pub fn jacobian_sequence<F>(
f: F,
x: &[f64],
order: usize,
) -> AgResult<Vec<Vec<f64>>>
where
F: Fn(&[f64]) -> Vec<f64>,
{
if x.is_empty() {
return Err(AutogradError::invalid_argument(
"jacobian_sequence: input must be non-empty".to_string(),
));
}
if order > 2 {
return Err(AutogradError::invalid_argument(
"jacobian_sequence: order > 2 is not supported".to_string(),
));
}
let n = x.len();
let fx = f(x);
let m = fx.len();
let mut result = Vec::with_capacity(order + 1);
result.push(fx.clone());
if order == 0 {
return Ok(result);
}
let mut j1 = vec![0.0f64; m * n];
let mut xp = x.to_vec();
let mut xm = x.to_vec();
let two_h = 2.0 * H;
for col in 0..n {
xp[col] = x[col] + H;
xm[col] = x[col] - H;
let fp = f(&xp);
let fm = f(&xm);
for row in 0..m {
j1[row * n + col] = (fp[row] - fm[row]) / two_h;
}
xp[col] = x[col];
xm[col] = x[col];
}
result.push(j1.clone());
if order == 1 {
return Ok(result);
}
let mut j2 = vec![0.0f64; m * n * n];
for j_ax in 0..n {
xp[j_ax] = x[j_ax] + H;
xm[j_ax] = x[j_ax] - H;
let mut jp = vec![0.0f64; m * n];
for col in 0..n {
let mut xpp = xp.clone();
let mut xpm = xp.clone();
xpp[col] += H;
xpm[col] -= H;
let fp2 = f(&xpp);
let fm2 = f(&xpm);
for row in 0..m {
jp[row * n + col] = (fp2[row] - fm2[row]) / two_h;
}
}
let mut jm = vec![0.0f64; m * n];
for col in 0..n {
let mut xmp = xm.clone();
let mut xmm = xm.clone();
xmp[col] += H;
xmm[col] -= H;
let fp2 = f(&xmp);
let fm2 = f(&xmm);
for row in 0..m {
jm[row * n + col] = (fp2[row] - fm2[row]) / two_h;
}
}
for row in 0..m {
for col in 0..n {
j2[row * n * n + col * n + j_ax] =
(jp[row * n + col] - jm[row * n + col]) / two_h;
}
}
xp[j_ax] = x[j_ax];
xm[j_ax] = x[j_ax];
}
result.push(j2);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mixed_partial_cubic() {
let val = mixed_partial(
|xs: &[f64]| xs[0] * xs[0] * xs[1] * xs[2],
&[1.0, 2.0, 3.0],
&[0, 1, 2],
)
.expect("mixed partial");
assert!((val - 2.0).abs() < 0.5, "∂³f = {val}");
}
#[test]
fn test_mixed_partial_second_order() {
let val = mixed_partial(
|xs: &[f64]| xs[0] * xs[1],
&[1.0, 1.0],
&[0, 1],
)
.expect("2nd order mixed");
assert!((val - 1.0).abs() < 1e-2, "∂²f/∂x∂y = {val}");
}
#[test]
fn test_mixed_partial_empty_axes() {
let val = mixed_partial(|xs: &[f64]| xs[0] * xs[0], &[3.0], &[]).expect("zero order");
assert!((val - 9.0).abs() < 1e-12);
}
#[test]
fn test_mixed_partial_oob_axis() {
assert!(mixed_partial(|xs: &[f64]| xs[0], &[1.0], &[5]).is_err());
}
#[test]
fn test_laplacian_sphere() {
let lap = laplacian(
|xs: &[f64]| xs[0] * xs[0] + xs[1] * xs[1] + xs[2] * xs[2],
&[1.0, 2.0, 3.0],
)
.expect("laplacian");
assert!((lap - 6.0).abs() < 1e-1, "Δf = {lap}");
}
#[test]
fn test_laplacian_gaussian() {
let lap = laplacian(
|xs: &[f64]| (-xs[0] * xs[0] - xs[1] * xs[1]).exp(),
&[0.0, 0.0],
)
.expect("laplacian gaussian");
assert!((lap - (-4.0)).abs() < 0.5, "Δf(0,0) = {lap}");
}
#[test]
fn test_laplacian_stochastic_sphere() {
let est = laplacian_stochastic(
|xs: &[f64]| xs.iter().map(|v| v * v).sum::<f64>(),
&[1.0, 2.0, 3.0],
500,
12345,
)
.expect("hutchinson");
assert!((est - 6.0).abs() < 2.0, "stochastic Δf = {est}");
}
#[test]
fn test_hessian_diagonal_quadratic() {
let d = hessian_diagonal(
|xs: &[f64]| 3.0 * xs[0] * xs[0] + 5.0 * xs[1] * xs[1],
&[1.0, 1.0],
)
.expect("hessian diagonal");
assert!((d[0] - 6.0).abs() < 0.5, "H[0][0] = {}", d[0]);
assert!((d[1] - 10.0).abs() < 0.5, "H[1][1] = {}", d[1]);
}
#[test]
fn test_hvp_identity_hessian() {
let v = &[1.0, 2.0];
let hv = hvp(|xs: &[f64]| xs[0] * xs[0] + xs[1] * xs[1], &[3.0, 4.0], v)
.expect("hvp");
assert!((hv[0] - 2.0).abs() < 0.5, "hv[0] = {}", hv[0]);
assert!((hv[1] - 4.0).abs() < 0.5, "hv[1] = {}", hv[1]);
}
#[test]
fn test_hvp_dimension_mismatch() {
assert!(
hvp(|xs: &[f64]| xs[0] * xs[0], &[1.0], &[1.0, 2.0]).is_err()
);
}
#[test]
fn test_taylor_coefficients_exp() {
let cs = taylor_coefficients(|xs: &[f64]| xs[0].exp(), &[0.0], &[1.0], 3)
.expect("taylor exp");
assert!((cs[0] - 1.0).abs() < 1e-6, "c0 = {}", cs[0]);
assert!((cs[1] - 1.0).abs() < 1e-2, "c1 = {}", cs[1]);
assert!((cs[2] - 0.5).abs() < 0.1, "c2 = {}", cs[2]);
}
#[test]
fn test_taylor_coefficients_too_high_order() {
assert!(
taylor_coefficients(|xs: &[f64]| xs[0], &[1.0], &[1.0], 9).is_err()
);
}
#[test]
fn test_nth_derivative_sin() {
let d1 = nth_derivative_scalar(|x: f64| x.sin(), 0.0, 1).expect("sin'");
assert!((d1 - 1.0).abs() < 1e-3, "sin'(0) = {d1}");
}
#[test]
fn test_nth_derivative_polynomial() {
let d2 = nth_derivative_scalar(|x: f64| x.powi(4), 1.0, 2).expect("x^4 d2");
assert!((d2 - 12.0).abs() < 1.0, "f''(1) = {d2}");
}
#[test]
fn test_nth_derivative_too_high() {
assert!(nth_derivative_scalar(|x: f64| x, 1.0, 11).is_err());
}
#[test]
fn test_iterated_gradient_order0() {
let g0 = iterated_gradient(|xs: &[f64]| xs[0] * xs[0], &[3.0], 0).expect("order 0");
assert!((g0[0] - 9.0).abs() < 1e-12);
}
#[test]
fn test_iterated_gradient_order1() {
let g1 = iterated_gradient(
|xs: &[f64]| xs[0] * xs[0] + xs[1] * xs[1],
&[3.0, 4.0],
1,
)
.expect("order 1");
assert!((g1[0] - 6.0).abs() < 1e-2, "∇f[0] = {}", g1[0]);
assert!((g1[1] - 8.0).abs() < 1e-2, "∇f[1] = {}", g1[1]);
}
#[test]
fn test_jacobian_sequence_order1() {
let seq = jacobian_sequence(
|xs: &[f64]| vec![xs[0] * xs[0], xs[0] * xs[1]],
&[2.0, 3.0],
1,
)
.expect("jacobian sequence");
let j = &seq[1];
assert!((j[0] - 4.0).abs() < 1e-2, "J[0][0] = {}", j[0]);
assert!((j[1] - 0.0).abs() < 1e-2, "J[0][1] = {}", j[1]);
assert!((j[2] - 3.0).abs() < 1e-2, "J[1][0] = {}", j[2]);
assert!((j[3] - 2.0).abs() < 1e-2, "J[1][1] = {}", j[3]);
}
}