use crate::error::AutogradError;
use scirs2_core::ndarray::{Array1, Array2};
const H: f64 = 1e-4;
fn gradient_fd(f: &(impl Fn(&[f64]) -> f64 + ?Sized), x: &[f64]) -> Vec<f64> {
let n = x.len();
let mut g = vec![0.0f64; n];
let two_h = 2.0 * H;
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)) / two_h;
xp[i] = x[i];
xm[i] = x[i];
}
g
}
fn second_derivative_directional(
f: &impl Fn(&[f64]) -> f64,
x: &[f64],
v: &[f64],
h_step: f64,
) -> f64 {
let xp: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi + h_step * vi).collect();
let xm: Vec<f64> = x.iter().zip(v.iter()).map(|(&xi, &vi)| xi - h_step * vi).collect();
let fx = f(x);
(f(&xp) - 2.0 * fx + f(&xm)) / (h_step * h_step)
}
fn directional_derivative_nth(
f: &dyn Fn(&[f64]) -> f64,
x: &[f64],
v: &[f64],
order: usize,
) -> f64 {
match order {
0 => f(x),
1 => {
let dot: f64 = gradient_fd(f, x).iter().zip(v.iter()).map(|(&g, &vi)| g * vi).sum();
dot
}
k => {
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_nth(f, &xp, v, k - 1);
let dm = directional_derivative_nth(f, &xm, v, k - 1);
(dp - dm) / (2.0 * H)
}
}
}
pub fn taylor_mode_forward(
f: impl Fn(&[f64]) -> f64,
x: &[f64],
v: &[f64],
order: usize,
) -> Result<Vec<f64>, AutogradError> {
if x.is_empty() {
return Err(AutogradError::OperationError(
"taylor_mode_forward: x must be non-empty".to_string(),
));
}
if v.len() != x.len() {
return Err(AutogradError::ShapeMismatch(format!(
"taylor_mode_forward: x length {} != v length {}",
x.len(),
v.len()
)));
}
if order == 0 {
return Err(AutogradError::OperationError(
"taylor_mode_forward: order must be >= 1".to_string(),
));
}
let f_box: &dyn Fn(&[f64]) -> f64 = &f;
let mut coeffs = Vec::with_capacity(order + 1);
coeffs.push(f(x));
let mut factorial: f64 = 1.0;
for k in 1..=order {
factorial *= k as f64;
let dk = directional_derivative_nth(f_box, x, v, k);
coeffs.push(dk / factorial);
}
Ok(coeffs)
}
pub fn higher_order_jacobian(
f: impl Fn(&[f64]) -> Vec<f64> + 'static,
x: &[f64],
order: usize,
) -> Result<Vec<Array2<f64>>, AutogradError> {
let n = x.len();
if n == 0 {
return Err(AutogradError::OperationError(
"higher_order_jacobian: x must be non-empty".to_string(),
));
}
if order == 0 {
return Err(AutogradError::OperationError(
"higher_order_jacobian: order must be >= 1".to_string(),
));
}
let f0 = f(x);
let m = f0.len();
if m == 0 {
return Err(AutogradError::OperationError(
"higher_order_jacobian: function output must be non-empty".to_string(),
));
}
let two_h = 2.0 * H;
let j1 = {
let mut mat = Array2::<f64>::zeros((m, n));
let mut xp = x.to_vec();
let mut xm_vec = x.to_vec();
for j in 0..n {
xp[j] = x[j] + H;
xm_vec[j] = x[j] - H;
let fp = f(&xp);
let fm = f(&xm_vec);
for i in 0..m {
mat[[i, j]] = (fp[i] - fm[i]) / two_h;
}
xp[j] = x[j];
xm_vec[j] = x[j];
}
mat
};
let mut result = Vec::with_capacity(order);
result.push(j1.clone());
if order == 1 {
return Ok(result);
}
type DynFn = Box<dyn Fn(&[f64]) -> Vec<f64>>;
let f_rc: std::sync::Arc<dyn Fn(&[f64]) -> Vec<f64>> = std::sync::Arc::new(f);
fn build_jacobian_fn(
g: std::sync::Arc<dyn Fn(&[f64]) -> Vec<f64>>,
n: usize,
out_len: usize,
) -> DynFn {
Box::new(move |x: &[f64]| {
let two_h_inner = 2.0 * H;
let mut mat = vec![0.0f64; out_len * n];
let mut xp = x.to_vec();
let mut xm = x.to_vec();
for j in 0..n {
xp[j] = x[j] + H;
xm[j] = x[j] - H;
let fp = g(&xp);
let fm = g(&xm);
for i in 0..out_len {
mat[i * n + j] = (fp[i] - fm[i]) / two_h_inner;
}
xp[j] = x[j];
xm[j] = x[j];
}
mat
})
}
let mut current_fn: std::sync::Arc<dyn Fn(&[f64]) -> Vec<f64>> = f_rc;
let mut current_out_len = m;
for _level in 2..=order {
let next_fn: DynFn = build_jacobian_fn(current_fn.clone(), n, current_out_len);
let next_out_len = current_out_len * n;
let flat = next_fn(x);
let current_depth = _level - 1; let _ = next_out_len;
let mut jk = Array2::<f64>::zeros((m, n));
for i in 0..m {
for j in 0..n {
let mut idx = i;
for _ in 0..current_depth {
idx = idx * n + j;
}
jk[[i, j]] = if idx < flat.len() { flat[idx] } else { 0.0 };
}
}
result.push(jk);
current_fn = std::sync::Arc::from(next_fn);
current_out_len = current_fn(x).len();
let _ = current_out_len;
let probe = current_fn(x);
current_out_len = probe.len();
}
Ok(result)
}
pub fn mixed_partials(
f: impl Fn(&[f64]) -> f64 + 'static,
x: &[f64],
indices: &[(usize, usize)],
) -> Result<f64, AutogradError> {
let n = x.len();
if n == 0 {
return Err(AutogradError::OperationError(
"mixed_partials: x must be non-empty".to_string(),
));
}
if indices.is_empty() {
return Err(AutogradError::OperationError(
"mixed_partials: indices must be non-empty".to_string(),
));
}
for &(axis, _) in indices {
if axis >= n {
return Err(AutogradError::ShapeMismatch(format!(
"mixed_partials: axis {} out of bounds (n={})",
axis, n
)));
}
}
type ScalarFn = Box<dyn Fn(&[f64]) -> f64>;
let f_box: ScalarFn = Box::new(f);
let mut current: std::sync::Arc<dyn Fn(&[f64]) -> f64> = std::sync::Arc::from(f_box);
for &(axis, _) in indices {
let prev = current.clone();
let diff_fn: ScalarFn = Box::new(move |xs: &[f64]| {
let mut xp = xs.to_vec();
let mut xm = xs.to_vec();
xp[axis] = xs[axis] + H;
xm[axis] = xs[axis] - H;
(prev(&xp) - prev(&xm)) / (2.0 * H)
});
current = std::sync::Arc::from(diff_fn);
}
Ok(current(x))
}
pub fn laplacian(f: impl Fn(&[f64]) -> f64, x: &[f64]) -> Result<f64, AutogradError> {
let n = x.len();
if n == 0 {
return Err(AutogradError::OperationError(
"laplacian: x must be non-empty".to_string(),
));
}
let fx = f(x);
let h2 = H * H;
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;
let d2 = (f(&xp) - 2.0 * fx + f(&xm)) / h2;
sum += d2;
xp[i] = x[i];
xm[i] = x[i];
}
Ok(sum)
}
pub fn hessian_vector_product_nth_order(
f: impl Fn(&[f64]) -> f64,
x: &[f64],
v: &[f64],
n: usize,
) -> Result<Array1<f64>, AutogradError> {
let dim = x.len();
if dim == 0 {
return Err(AutogradError::OperationError(
"hessian_vector_product_nth_order: x must be non-empty".to_string(),
));
}
if v.len() != dim {
return Err(AutogradError::ShapeMismatch(format!(
"hessian_vector_product_nth_order: x length {} != v length {}",
dim,
v.len()
)));
}
if n == 0 {
return Err(AutogradError::OperationError(
"hessian_vector_product_nth_order: order n must be >= 1".to_string(),
));
}
let f_arc: std::sync::Arc<dyn Fn(&[f64]) -> f64> = std::sync::Arc::new(f);
if n == 1 {
let g = gradient_fd(&*f_arc, x);
let result: Vec<f64> = g.iter().zip(v.iter()).map(|(&gi, &vi)| gi * vi).collect();
return Ok(Array1::from(result));
}
let f_arc_1 = f_arc.clone();
let v_owned: Vec<f64> = v.to_vec();
let v_arc = std::sync::Arc::new(v_owned);
let mut h: std::sync::Arc<dyn Fn(&[f64]) -> f64> = {
let v_c = v_arc.clone();
std::sync::Arc::new(move |xs: &[f64]| {
gradient_fd(&*f_arc_1, xs)
.iter()
.zip(v_c.iter())
.map(|(&g, &vi)| g * vi)
.sum()
})
};
for _ in 2..n {
let h_prev = h.clone();
let v_c = v_arc.clone();
h = std::sync::Arc::new(move |xs: &[f64]| {
gradient_fd(&*h_prev, xs)
.iter()
.zip(v_c.iter())
.map(|(&g, &vi)| g * vi)
.sum()
});
}
let grad_h = gradient_fd(&*h, x);
Ok(Array1::from(grad_h))
}
pub fn automatic_taylor_expansion(
f: impl Fn(&[f64]) -> f64,
x0: &[f64],
n_terms: usize,
) -> Result<Vec<f64>, AutogradError> {
if x0.is_empty() {
return Err(AutogradError::OperationError(
"automatic_taylor_expansion: x0 must be non-empty".to_string(),
));
}
if n_terms == 0 {
return Err(AutogradError::OperationError(
"automatic_taylor_expansion: n_terms must be >= 1".to_string(),
));
}
let mut v = vec![0.0f64; x0.len()];
v[0] = 1.0;
let order = n_terms - 1;
if order == 0 {
return Ok(vec![f(x0)]);
}
taylor_mode_forward(f, x0, &v, order)
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-2;
#[test]
fn test_taylor_x_cubed() {
let c = taylor_mode_forward(|xs| xs[0].powi(3), &[1.0], &[1.0], 3)
.expect("taylor x^3");
assert!((c[0] - 1.0).abs() < TOL, "c0={}", c[0]);
assert!((c[1] - 3.0).abs() < TOL, "c1={}", c[1]);
assert!((c[2] - 3.0).abs() < TOL, "c2={}", c[2]);
assert!((c[3] - 1.0).abs() < TOL, "c3={}", c[3]);
}
#[test]
fn test_taylor_empty_x_error() {
let r = taylor_mode_forward(|_| 0.0, &[], &[], 1);
assert!(r.is_err());
}
#[test]
fn test_taylor_order_zero_error() {
let r = taylor_mode_forward(|xs| xs[0], &[1.0], &[1.0], 0);
assert!(r.is_err());
}
#[test]
fn test_higher_order_jacobian_level1_identity() {
let jacs = higher_order_jacobian(|xs| xs.to_vec(), &[1.0, 2.0], 1)
.expect("jacobian identity");
assert_eq!(jacs.len(), 1);
let j = &jacs[0];
assert!((j[[0, 0]] - 1.0).abs() < TOL);
assert!(j[[0, 1]].abs() < TOL);
assert!(j[[1, 0]].abs() < TOL);
assert!((j[[1, 1]] - 1.0).abs() < TOL);
}
#[test]
fn test_higher_order_jacobian_level1_quadratic() {
let jacs = higher_order_jacobian(
|xs| vec![xs[0] * xs[0], xs[1] * xs[1]],
&[1.0, 1.0],
1,
)
.expect("jacobian quadratic");
let j = &jacs[0];
assert!((j[[0, 0]] - 2.0).abs() < TOL, "J[0,0]={}", j[[0, 0]]);
assert!(j[[0, 1]].abs() < TOL, "J[0,1]={}", j[[0, 1]]);
assert!(j[[1, 0]].abs() < TOL, "J[1,0]={}", j[[1, 0]]);
assert!((j[[1, 1]] - 2.0).abs() < TOL, "J[1,1]={}", j[[1, 1]]);
}
#[test]
fn test_higher_order_jacobian_level2() {
let jacs = higher_order_jacobian(
|xs| vec![xs[0] * xs[0], xs[1] * xs[1]],
&[1.0, 1.0],
2,
)
.expect("jacobian level2");
assert_eq!(jacs.len(), 2);
let j2 = &jacs[1];
assert!((j2[[0, 0]] - 2.0).abs() < TOL, "J2[0,0]={}", j2[[0, 0]]);
assert!((j2[[1, 1]] - 2.0).abs() < TOL, "J2[1,1]={}", j2[[1, 1]]);
}
#[test]
fn test_mixed_partials_xy() {
let v = mixed_partials(|xs| xs[0] * xs[1], &[1.0, 2.0], &[(0, 0), (1, 0)])
.expect("mixed xy");
assert!((v - 1.0).abs() < TOL, "val={}", v);
}
#[test]
fn test_mixed_partials_xx() {
let v = mixed_partials(|xs| xs[0] * xs[0], &[3.0], &[(0, 0), (0, 0)])
.expect("mixed xx");
assert!((v - 2.0).abs() < TOL, "val={}", v);
}
#[test]
fn test_mixed_partials_out_of_bounds_error() {
let r = mixed_partials(|xs| xs[0], &[1.0], &[(5, 0)]);
assert!(r.is_err());
}
#[test]
fn test_laplacian_quadratic() {
let lap = laplacian(|xs| xs[0] * xs[0] + xs[1] * xs[1], &[1.0, 2.0])
.expect("laplacian");
assert!((lap - 4.0).abs() < TOL, "lap={}", lap);
}
#[test]
fn test_laplacian_harmonic() {
let lap = laplacian(
|xs| xs[0] * xs[0] - xs[1] * xs[1],
&[1.0, 1.0, 1.0],
)
.expect("laplacian harmonic");
assert!(lap.abs() < TOL, "lap={}", lap);
}
#[test]
fn test_laplacian_empty_error() {
let r = laplacian(|_| 0.0, &[]);
assert!(r.is_err());
}
#[test]
fn test_hvp_order2_quadratic() {
let hvp = hessian_vector_product_nth_order(
|xs| xs[0] * xs[0] + xs[1] * xs[1],
&[1.0, 1.0],
&[1.0, 0.0],
2,
)
.expect("hvp order 2");
assert!((hvp[0] - 2.0).abs() < TOL, "hvp[0]={}", hvp[0]);
assert!(hvp[1].abs() < TOL, "hvp[1]={}", hvp[1]);
}
#[test]
fn test_hvp_order1_gradient() {
let hvp = hessian_vector_product_nth_order(
|xs| xs[0] * xs[0],
&[1.0],
&[1.0],
1,
)
.expect("hvp order 1");
assert!((hvp[0] - 2.0).abs() < TOL, "hvp[0]={}", hvp[0]);
}
#[test]
fn test_hvp_order_zero_error() {
let r = hessian_vector_product_nth_order(|xs| xs[0], &[1.0], &[1.0], 0);
assert!(r.is_err());
}
#[test]
fn test_taylor_exp_coefficients() {
let c = automatic_taylor_expansion(|xs| xs[0].exp(), &[0.0], 4)
.expect("taylor exp");
assert!((c[0] - 1.0).abs() < TOL, "c0={}", c[0]);
assert!((c[1] - 1.0).abs() < TOL, "c1={}", c[1]);
assert!((c[2] - 0.5).abs() < TOL, "c2={}", c[2]);
assert!((c[3] - 1.0 / 6.0).abs() < 0.05, "c3={}", c[3]);
}
#[test]
fn test_taylor_polynomial() {
let c = automatic_taylor_expansion(
|xs| 2.0 + 3.0 * xs[0] + 4.0 * xs[0] * xs[0],
&[0.0],
3,
)
.expect("taylor poly");
assert!((c[0] - 2.0).abs() < TOL, "c0={}", c[0]);
assert!((c[1] - 3.0).abs() < TOL, "c1={}", c[1]);
assert!((c[2] - 4.0).abs() < TOL, "c2={}", c[2]);
}
#[test]
fn test_taylor_n_terms_zero_error() {
let r = automatic_taylor_expansion(|xs| xs[0], &[0.0], 0);
assert!(r.is_err());
}
}