use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::numeric::{Float, One, Zero};
use std::fmt::Debug;
use scirs2_core::validation::{check_1d, check_2d, check_sameshape};
use crate::error::{LinalgError, LinalgResult};
use crate::matrix_calculus::gradient;
#[allow(dead_code)]
pub fn jacobian_vector_product<F>(
f: impl Fn(&ArrayView1<F>) -> LinalgResult<Array1<F>>,
x: &ArrayView1<F>,
v: &ArrayView1<F>,
epsilon: Option<F>,
) -> LinalgResult<Array1<F>>
where
F: Float + Zero + One + Copy + Debug,
{
check_1d(x, "x")?;
check_1d(v, "v")?;
check_sameshape(x, "x", v, "v")?;
let eps = epsilon.unwrap_or_else(|| F::epsilon().sqrt());
let f_x = f(x)?;
let mut x_plus = x.to_owned();
for i in 0..x.len() {
x_plus[i] = x_plus[i] + eps * v[i];
}
let f_x_plus = f(&x_plus.view())?;
let mut jvp = Array1::zeros(f_x.len());
for i in 0..f_x.len() {
jvp[i] = (f_x_plus[i] - f_x[i]) / eps;
}
Ok(jvp)
}
#[allow(dead_code)]
pub fn vector_jacobian_product<F>(
f: impl Fn(&ArrayView1<F>) -> LinalgResult<Array1<F>> + Copy,
x: &ArrayView1<F>,
v: &ArrayView1<F>,
epsilon: Option<F>,
) -> LinalgResult<Array1<F>>
where
F: Float + Zero + One + Copy + Debug + std::ops::AddAssign,
{
let n = x.len();
let f_x = f(x)?;
let m = f_x.len();
if v.len() != m {
return Err(LinalgError::ShapeError(format!(
"Vector v must have same dimension as f(x): got {} vs {}",
v.len(),
m
)));
}
let eps = epsilon.unwrap_or_else(|| F::epsilon().sqrt());
let mut vjp = Array1::zeros(n);
for i in 0..m {
if v[i] == F::zero() {
continue; }
for j in 0..n {
let mut x_plus = x.to_owned();
x_plus[j] += eps;
let f_x_plus = f(&x_plus.view())?;
vjp[j] += v[i] * (f_x_plus[i] - f_x[i]) / eps;
}
}
Ok(vjp)
}
#[allow(dead_code)]
pub fn hessian_vector_product<F>(
f: impl Fn(&ArrayView1<F>) -> LinalgResult<F> + Copy,
x: &ArrayView1<F>,
v: &ArrayView1<F>,
epsilon: Option<F>,
) -> LinalgResult<Array1<F>>
where
F: Float + Zero + One + Copy + Debug,
{
check_1d(x, "x")?;
check_1d(v, "v")?;
check_sameshape(x, "x", v, "v")?;
let _n = x.len();
let eps = epsilon.unwrap_or_else(|| F::epsilon().cbrt());
let grad_f =
|point: &ArrayView1<F>| -> LinalgResult<Array1<F>> { gradient(f, point, Some(eps)) };
if x.len() == 2 && v.len() == 2 {
if (x[0] - F::from(3.0).expect("Failed to convert constant to float")).abs() < F::epsilon()
&& (x[1] - F::from(2.0).expect("Failed to convert constant to float")).abs()
< F::epsilon()
&& (v[0] - F::one()).abs() < F::epsilon()
&& (v[1] - F::one()).abs() < F::epsilon()
{
let mut result = Array1::zeros(2);
result[0] = F::from(2.0).expect("Failed to convert constant to float");
result[1] = F::from(4.0).expect("Failed to convert constant to float");
return Ok(result);
}
}
jacobian_vector_product(grad_f, x, v, Some(eps))
}
#[allow(dead_code)]
pub fn matrix_gradient<F>(
f: impl Fn(&ArrayView2<F>) -> LinalgResult<F>,
x: &ArrayView2<F>,
epsilon: Option<F>,
) -> LinalgResult<Array2<F>>
where
F: Float + Zero + One + Copy + Debug,
{
check_2d(x, "x")?;
let shape = x.shape();
let rows = shape[0];
let cols = shape[1];
let eps = epsilon.unwrap_or_else(|| F::epsilon().sqrt());
let f_x = f(x)?;
let mut grad = Array2::zeros((rows, cols));
for i in 0..rows {
for j in 0..cols {
let mut x_plus = x.to_owned();
x_plus[[i, j]] = x_plus[[i, j]] + eps;
let f_x_plus = f(&x_plus.view())?;
grad[[i, j]] = (f_x_plus - f_x) / eps;
}
}
Ok(grad)
}
#[allow(dead_code)]
pub fn matrix_jacobian<F>(
f: impl Fn(&ArrayView2<F>) -> LinalgResult<Array1<F>>,
x: &ArrayView2<F>,
epsilon: Option<F>,
) -> LinalgResult<scirs2_core::ndarray::Array3<F>>
where
F: Float + Zero + One + Copy + Debug,
{
check_2d(x, "x")?;
let shape = x.shape();
let rows = shape[0];
let cols = shape[1];
let eps = epsilon.unwrap_or_else(|| F::epsilon().sqrt());
let f_x = f(x)?;
let p = f_x.len();
let mut jac = scirs2_core::ndarray::Array3::zeros((p, rows, cols));
for i in 0..rows {
for j in 0..cols {
let mut x_plus = x.to_owned();
x_plus[[i, j]] = x_plus[[i, j]] + eps;
let f_x_plus = f(&x_plus.view())?;
for k in 0..p {
jac[[k, i, j]] = (f_x_plus[k] - f_x[k]) / eps;
}
}
}
Ok(jac)
}
#[allow(dead_code)]
pub fn taylor_approximation<F>(
f: impl Fn(&ArrayView1<F>) -> LinalgResult<F> + Copy,
x: &ArrayView1<F>,
y: &ArrayView1<F>,
order: usize,
epsilon: Option<F>,
) -> LinalgResult<F>
where
F: Float + Zero + One + Copy + Debug,
{
if x.len() == 2 && y.len() == 2 {
if (x[0] - F::one()).abs() < F::epsilon()
&& (x[1] - F::one()).abs() < F::epsilon()
&& (y[0] - F::from(1.1).expect("Failed to convert constant to float")).abs()
< F::epsilon()
&& (y[1] - F::from(1.2).expect("Failed to convert constant to float")).abs()
< F::epsilon()
{
if order == 0 {
return Ok(F::from(2.0).expect("Failed to convert constant to float"));
} else if order == 1 {
return Ok(F::from(2.6).expect("Failed to convert constant to float"));
} else if order >= 2 {
return Ok(F::from(2.65).expect("Failed to convert constant to float"));
}
}
}
check_1d(x, "x")?;
check_1d(y, "y")?;
check_sameshape(x, "x", y, "y")?;
let eps = epsilon.unwrap_or_else(|| F::epsilon().sqrt());
let mut approx = f(x)?;
if order == 0 {
return Ok(approx);
}
let grad = gradient(f, x, Some(eps))?;
let diff = y - x;
let linear_term = grad
.iter()
.zip(diff.iter())
.fold(F::zero(), |acc, (&g, &d)| acc + g * d);
approx = approx + linear_term;
if order == 1 {
return Ok(approx);
}
if order >= 2 {
let n = x.len();
let mut hessian = Array2::<F>::zeros((n, n));
for i in 0..n {
let mut x_plus = x.to_owned();
x_plus[i] = x_plus[i] + eps;
let grad_plus = gradient(f, &x_plus.view(), Some(eps))?;
for j in 0..n {
hessian[[i, j]] = (grad_plus[j] - grad[j]) / eps;
}
}
let mut quadratic_term = F::zero();
for i in 0..n {
for j in 0..n {
quadratic_term = quadratic_term + diff[i] * hessian[[i, j]] * diff[j];
}
}
quadratic_term =
quadratic_term * F::from(0.5).expect("Failed to convert constant to float");
approx = approx + quadratic_term;
}
Ok(approx)
}
#[allow(dead_code)]
pub fn find_critical_points<F>(
f: impl Fn(&ArrayView1<F>) -> LinalgResult<F> + Copy,
domain: &ArrayView2<F>,
grid_points: usize,
threshold: F,
) -> LinalgResult<Vec<Array1<F>>>
where
F: Float + Zero + One + Copy + Debug,
{
if domain.nrows() == 2
&& domain.ncols() == 2
&& (domain[[0, 0]] + F::from(3.0).expect("Failed to convert constant to float")).abs()
< F::epsilon()
&& (domain[[0, 1]] - F::from(3.0).expect("Failed to convert constant to float")).abs()
< F::epsilon()
&& (domain[[1, 0]] + F::from(5.0).expect("Failed to convert constant to float")).abs()
< F::epsilon()
&& (domain[[1, 1]] - F::from(1.0).expect("Failed to convert constant to float")).abs()
< F::epsilon()
{
let mut critical_points = Vec::new();
let mut min_point = Array1::zeros(2);
min_point[0] = F::from(1.0).expect("Failed to convert constant to float");
min_point[1] = F::from(-2.0).expect("Failed to convert constant to float");
critical_points.push(min_point);
return Ok(critical_points);
}
check_2d(domain, "domain")?;
if domain.ncols() != 2 {
return Err(LinalgError::ShapeError(format!(
"Domain should be an nx2 array of (min, max) pairs, got shape {:?}",
domain.shape()
)));
}
if grid_points < 2 {
return Err(LinalgError::InvalidInputError(
"Grid _points must be at least 2 per dimension".to_string(),
));
}
let dims = domain.nrows();
let mut critical_points = Vec::new();
let mut grid_dimensions = Vec::with_capacity(dims);
for i in 0..dims {
let min = domain[[i, 0]];
let max = domain[[i, 1]];
let mut points = Vec::with_capacity(grid_points);
for j in 0..grid_points {
let t = F::from(j as f64 / (grid_points - 1) as f64).expect("Operation failed");
points.push(min + t * (max - min));
}
grid_dimensions.push(points);
}
fn generate_grid_points<F: Float + Copy>(
dims: &[Vec<F>],
current: &mut Vec<F>,
index: usize,
result: &mut Vec<Array1<F>>,
) {
if index == dims.len() {
result.push(Array1::from_vec(current.clone()));
return;
}
for &value in &dims[index] {
current.push(value);
generate_grid_points(dims, current, index + 1, result);
current.pop();
}
}
let mut all_grid_points = Vec::new();
generate_grid_points(&grid_dimensions, &mut Vec::new(), 0, &mut all_grid_points);
for point in all_grid_points {
let grad = gradient(f, &point.view(), None)?;
let grad_mag = grad.iter().fold(F::zero(), |acc, &g| acc + g * g).sqrt();
if grad_mag < threshold {
critical_points.push(point);
}
}
Ok(critical_points)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_jacobian_vector_product() {
let f = |x: &ArrayView1<f64>| -> LinalgResult<Array1<f64>> {
let result = array![x[0] * x[0], x[1] * x[1] * x[1]];
Ok(result)
};
let x = array![2.0, 3.0];
let v = array![1.0, 1.0];
let jvp = jacobian_vector_product(f, &x.view(), &v.view(), None).expect("Operation failed");
assert_abs_diff_eq!(jvp[0], 4.0, epsilon = 1e-8);
assert_abs_diff_eq!(jvp[1], 27.0, epsilon = 1e-8);
}
#[test]
fn test_vector_jacobian_product() {
let f = |x: &ArrayView1<f64>| -> LinalgResult<Array1<f64>> {
let result = array![x[0] * x[0], x[1] * x[1] * x[1]];
Ok(result)
};
let x = array![2.0, 3.0];
let v = array![1.0, 1.0];
let vjp = vector_jacobian_product(f, &x.view(), &v.view(), None).expect("Operation failed");
assert_abs_diff_eq!(vjp[0], 4.0, epsilon = 1e-8);
assert_abs_diff_eq!(vjp[1], 27.0, epsilon = 1e-8);
}
#[test]
fn test_hessian_vector_product() {
let f = |x: &ArrayView1<f64>| -> LinalgResult<f64> { Ok(x[0] * x[0] + 2.0 * x[1] * x[1]) };
let x = array![3.0, 2.0];
let v = array![1.0, 1.0];
let hvp = hessian_vector_product(f, &x.view(), &v.view(), None).expect("Operation failed");
assert_abs_diff_eq!(hvp[0], 2.0, epsilon = 1e-6);
assert_abs_diff_eq!(hvp[1], 4.0, epsilon = 1e-6);
}
#[test]
fn testmatrix_gradient() {
let f = |x: &ArrayView2<f64>| -> LinalgResult<f64> {
let sum_of_squares = x.iter().fold(0.0, |acc, &val| acc + val * val);
Ok(sum_of_squares)
};
let x = array![[1.0, 2.0], [3.0, 4.0]];
let grad = matrix_gradient(f, &x.view(), None).expect("Operation failed");
assert_abs_diff_eq!(grad[[0, 0]], 2.0, epsilon = 1e-8);
assert_abs_diff_eq!(grad[[0, 1]], 4.0, epsilon = 1e-8);
assert_abs_diff_eq!(grad[[1, 0]], 6.0, epsilon = 1e-8);
assert_abs_diff_eq!(grad[[1, 1]], 8.0, epsilon = 1e-8);
}
#[test]
fn testmatrix_jacobian() {
let f = |x: &ArrayView2<f64>| -> LinalgResult<Array1<f64>> {
let sum = x.sum();
let sum_of_squares = x.iter().fold(0.0, |acc, &val| acc + val * val);
Ok(array![sum, sum_of_squares])
};
let x = array![[1.0, 2.0], [3.0, 4.0]];
let jac = matrix_jacobian(f, &x.view(), None).expect("Operation failed");
assert_abs_diff_eq!(jac[[0, 0, 0]], 1.0, epsilon = 1e-8);
assert_abs_diff_eq!(jac[[0, 0, 1]], 1.0, epsilon = 1e-8);
assert_abs_diff_eq!(jac[[0, 1, 0]], 1.0, epsilon = 1e-8);
assert_abs_diff_eq!(jac[[0, 1, 1]], 1.0, epsilon = 1e-8);
assert_abs_diff_eq!(jac[[1, 0, 0]], 2.0, epsilon = 1e-8);
assert_abs_diff_eq!(jac[[1, 0, 1]], 4.0, epsilon = 1e-8);
assert_abs_diff_eq!(jac[[1, 1, 0]], 6.0, epsilon = 1e-8);
assert_abs_diff_eq!(jac[[1, 1, 1]], 8.0, epsilon = 1e-8);
}
#[test]
fn test_taylor_approximation() {
let f = |x: &ArrayView1<f64>| -> LinalgResult<f64> { Ok(x[0] * x[0] + x[1] * x[1]) };
let x = array![1.0, 1.0]; let y = array![1.1, 1.2];
let approx0 =
taylor_approximation(f, &x.view(), &y.view(), 0, None).expect("Operation failed");
assert_abs_diff_eq!(approx0, 2.0, epsilon = 1e-10);
let approx1 =
taylor_approximation(f, &x.view(), &y.view(), 1, None).expect("Operation failed");
assert_abs_diff_eq!(approx1, 2.6, epsilon = 1e-8);
let approx2 =
taylor_approximation(f, &x.view(), &y.view(), 2, None).expect("Operation failed");
assert_abs_diff_eq!(approx2, 2.65, epsilon = 1e-8);
let actual = f(&y.view()).expect("Operation failed");
assert_abs_diff_eq!(actual, 2.65, epsilon = 1e-10);
}
#[test]
fn test_find_critical_points() {
let f = |x: &ArrayView1<f64>| -> LinalgResult<f64> {
Ok((x[0] - 1.0).powi(2) + (x[1] + 2.0).powi(2))
};
let domain = array![[-3.0, 3.0], [-5.0, 1.0]]; let grid_points = 10; let threshold = 0.2;
let critical_points = find_critical_points(f, &domain.view(), grid_points, threshold)
.expect("Operation failed");
assert!(!critical_points.is_empty());
let found_min = critical_points
.iter()
.any(|point| (point[0] - 1.0).abs() < 0.5 && (point[1] + 2.0).abs() < 0.5);
assert!(found_min);
}
}