use ndarray::Array2;
use stochastic_rs_distributions::special::gamma;
use crate::traits::FloatExt;
pub fn sphere_area<T: FloatExt>(d: usize) -> T {
let dh = d as f64 / 2.0;
T::from_f64_fast(2.0 * std::f64::consts::PI.powf(dh) / gamma(dh))
}
#[inline]
pub fn norm_h<T: FloatExt>(x: &[T], h: T) -> T {
let s: T = x.iter().map(|xi| *xi * *xi).fold(T::zero(), |a, b| a + b);
(s + h).sqrt()
}
pub fn grad_poisson_reg<T: FloatExt>(x: &[T], h: T) -> Vec<T> {
let d = x.len();
let ad: T = sphere_area(d);
let r = norm_h(x, h);
let rd = r.powi(d as i32);
let factor = if d >= 3 {
T::from_usize_(d - 2) / (ad * rd)
} else {
T::one() / (ad * rd)
};
x.iter().map(|&xi| xi * factor).collect()
}
pub fn kernel_k_ij_h<T: FloatExt>(x: &[T], h: T, i: usize, j: usize) -> T {
let d = x.len();
assert!(i < d && j < d, "kernel indices out of bounds");
let ad: T = sphere_area(d);
let r = norm_h(x, h);
let rd = r.powi(d as i32);
let factor = if d >= 3 {
T::from_usize_(d - 2) / ad
} else {
T::one() / ad
};
let delta = if i == j { T::one() } else { T::zero() };
factor * (delta / rd - T::from_usize_(d) * x[i] * x[j] / r.powi(d as i32 + 2))
}
pub fn g_digital_put_2d<T: FloatExt>(y: [T; 2], k: [T; 2]) -> [[T; 2]; 2] {
let a2_inv = T::one() / (T::from_f64_fast(2.0) * T::from_f64_fast(std::f64::consts::PI));
let nudge = T::from_f64_fast(1e-10);
let y1 = y[0];
let y2 = y[1];
let k1 = k[0];
let k2 = k[1];
let y1mk1 = if (y1 - k1).abs() < nudge {
nudge
} else {
y1 - k1
};
let y2mk2 = if (y2 - k2).abs() < nudge {
nudge
} else {
y2 - k2
};
let y1s = if y1.abs() < nudge { nudge } else { y1 };
let y2s = if y2.abs() < nudge { nudge } else { y2 };
let g11 = a2_inv
* ((y2s / y1s).atan() - (y2mk2 / y1s).atan() - (y2s / y1mk1).atan() + (y2mk2 / y1mk1).atan());
let num = (y1 * y1 + y2 * y2 + nudge) * (y1mk1 * y1mk1 + y2mk2 * y2mk2 + nudge);
let den = (y1mk1 * y1mk1 + y2 * y2 + nudge) * (y1 * y1 + y2mk2 * y2mk2 + nudge);
let g21 = (a2_inv / T::from_f64_fast(2.0)) * (num / den).ln();
[[g11, g21], [g21, -g11]]
}
pub fn g_kernel_numerical_2d<T, F>(
y: &[T; 2],
payoff: &F,
h: T,
lo: &[T; 2],
hi: &[T; 2],
n_quad: usize,
) -> Array2<T>
where
T: FloatExt,
F: Fn(&[T]) -> T,
{
let dx0 = (hi[0] - lo[0]) / T::from_usize_(n_quad);
let dx1 = (hi[1] - lo[1]) / T::from_usize_(n_quad);
let area = dx0 * dx1;
let ad: T = sphere_area(2);
let half = T::from_f64_fast(0.5);
let two = T::from_f64_fast(2.0);
let eps = T::from_f64_fast(1e-4) * (y[0].abs() + y[1].abs() + T::one());
let shifts: [[T; 2]; 5] = [
[T::zero(), T::zero()],
[eps, T::zero()],
[-eps, T::zero()],
[T::zero(), eps],
[T::zero(), -eps],
];
let mut phi = [[T::zero(); 2]; 5];
for q0 in 0..n_quad {
let x0 = lo[0] + (T::from_usize_(q0) + half) * dx0;
for q1 in 0..n_quad {
let x1 = lo[1] + (T::from_usize_(q1) + half) * dx1;
let fval = payoff(&[x0, x1]);
if fval.abs() < T::from_f64_fast(1e-15) {
continue;
}
for (s, shift) in shifts.iter().enumerate() {
let dy = [y[0] + shift[0] - x0, y[1] + shift[1] - x1];
let r = norm_h(&dy, h);
let rd = r * r; let inv = fval * area / (ad * rd);
phi[s][0] += dy[0] * inv;
phi[s][1] += dy[1] * inv;
}
}
}
let inv_2eps = T::one() / (two * eps);
let mut g = Array2::<T>::zeros((2, 2));
g[[0, 0]] = (phi[1][0] - phi[2][0]) * inv_2eps;
g[[0, 1]] = (phi[3][0] - phi[4][0]) * inv_2eps;
g[[1, 0]] = (phi[1][1] - phi[2][1]) * inv_2eps;
g[[1, 1]] = (phi[3][1] - phi[4][1]) * inv_2eps;
g
}
pub fn g_kernel_numerical_nd<T, F>(
y: &[T],
payoff: &F,
h: T,
lo: &[T],
hi: &[T],
n_quad: usize,
) -> Array2<T>
where
T: FloatExt,
F: Fn(&[T]) -> T,
{
let d = y.len();
assert!(d >= 2, "g_kernel_numerical_nd requires d >= 2");
assert_eq!(lo.len(), d, "lo must have the same dimension as y");
assert_eq!(hi.len(), d, "hi must have the same dimension as y");
assert!(n_quad > 0, "n_quad must be positive");
let half = T::from_f64_fast(0.5);
let dx: Vec<T> = (0..d)
.map(|k| (hi[k] - lo[k]) / T::from_usize_(n_quad))
.collect();
let cell_volume = dx.iter().copied().fold(T::one(), |acc, step| acc * step);
let eps = T::from_f64_fast(1e-4)
* (y.iter().map(|yi| yi.abs()).fold(T::zero(), |a, b| a + b) / T::from_usize_(d) + T::one());
let tol = T::from_f64_fast(1e-15);
let mut shifts = Vec::with_capacity(1 + 2 * d);
shifts.push(vec![T::zero(); d]);
for j in 0..d {
let mut up = vec![T::zero(); d];
up[j] = eps;
shifts.push(up);
let mut dn = vec![T::zero(); d];
dn[j] = -eps;
shifts.push(dn);
}
let mut phi = vec![vec![T::zero(); d]; shifts.len()];
let mut x = vec![T::zero(); d];
let mut multi_idx = vec![0usize; d];
loop {
for k in 0..d {
x[k] = lo[k] + (T::from_usize_(multi_idx[k]) + half) * dx[k];
}
let fval = payoff(&x);
if fval.abs() >= tol {
let weighted = fval * cell_volume;
for (shift_id, shift) in shifts.iter().enumerate() {
let diff: Vec<T> = (0..d).map(|k| y[k] + shift[k] - x[k]).collect();
let grad = grad_poisson_reg(&diff, h);
for i in 0..d {
phi[shift_id][i] += weighted * grad[i];
}
}
}
let mut exhausted = true;
for k in (0..d).rev() {
if multi_idx[k] + 1 < n_quad {
multi_idx[k] += 1;
for reset in (k + 1)..d {
multi_idx[reset] = 0;
}
exhausted = false;
break;
}
}
if exhausted {
break;
}
}
let inv_2eps = T::one() / (eps + eps);
let mut g = Array2::<T>::zeros((d, d));
for j in 0..d {
let up = 1 + 2 * j;
let dn = up + 1;
for i in 0..d {
g[[i, j]] = (phi[up][i] - phi[dn][i]) * inv_2eps;
}
}
g
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sphere_area_known_values() {
let a2: f64 = sphere_area(2);
assert!((a2 - 2.0 * std::f64::consts::PI).abs() < 1e-10);
let a3: f64 = sphere_area(3);
assert!((a3 - 4.0 * std::f64::consts::PI).abs() < 1e-10);
}
#[test]
fn g_digital_put_2d_finite() {
let g = g_digital_put_2d([100.0_f64, 100.0], [100.0, 100.0]);
for i in 0..2 {
for j in 0..2 {
assert!(g[i][j].is_finite(), "g[{i}][{j}] = {} not finite", g[i][j]);
}
}
}
#[test]
fn poisson_kernel_grad_decays() {
let g1 = grad_poisson_reg(&[1.0, 1.0], 0.01);
let g2 = grad_poisson_reg(&[10.0, 10.0], 0.01);
let n1: f64 = g1.iter().map(|x| x * x).sum::<f64>().sqrt();
let n2: f64 = g2.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!(n1 > n2, "|∇Q(1)| = {n1} should > |∇Q(10)| = {n2}");
}
#[test]
fn poisson_kernel_grad_matches_3d_newton_potential() {
let x = [1.0_f64, 2.0, 2.0];
let grad = grad_poisson_reg(&x, 0.0);
let r = (x[0] * x[0] + x[1] * x[1] + x[2] * x[2]).sqrt();
let factor = 1.0 / (4.0 * std::f64::consts::PI * r.powi(3));
for i in 0..3 {
let expected = x[i] * factor;
assert!(
(grad[i] - expected).abs() < 1e-12,
"grad[{i}] = {}, expected {expected}",
grad[i]
);
}
}
#[test]
fn g_kernel_numerical_nd_matches_2d_specialisation() {
let y = [0.7_f64, 0.4];
let lo = [0.0_f64, 0.0];
let hi = [1.5_f64, 1.5];
let h = 0.05_f64;
let payoff = |x: &[f64]| if x[0] <= 1.0 && x[1] <= 0.8 { 1.0 } else { 0.0 };
let g_2d = g_kernel_numerical_2d(&y, &payoff, h, &lo, &hi, 32);
let g_nd = g_kernel_numerical_nd(&y, &payoff, h, &lo, &hi, 32);
for i in 0..2 {
for j in 0..2 {
assert!(
(g_2d[[i, j]] - g_nd[[i, j]]).abs() < 5e-3,
"mismatch at ({i},{j}): {} vs {}",
g_2d[[i, j]],
g_nd[[i, j]]
);
}
}
}
#[test]
fn kernel_trace_identity_holds() {
let z = [2.0_f64, 3.0, 1.5];
let h = 0.01;
let d = z.len();
let trace: f64 = (0..d).map(|i| kernel_k_ij_h(&z, h, i, i)).sum();
let ad: f64 = 2.0 * std::f64::consts::PI.powf(d as f64 / 2.0) / gamma(d as f64 / 2.0);
let factor = (d - 2) as f64 / ad;
let r_h = (z.iter().map(|x| x * x).sum::<f64>() + h).sqrt();
let z_sq: f64 = z.iter().map(|x| x * x).sum();
let expected =
factor * (d as f64 / r_h.powi(d as i32) - d as f64 * z_sq / r_h.powi(d as i32 + 2));
assert!(
(trace - expected).abs() < 1e-10,
"trace={trace}, expected={expected}"
);
}
#[test]
fn kernel_is_symmetric_in_indices() {
let z = [1.0_f64, 2.0, 3.0, 4.0];
let h = 0.01;
for i in 0..z.len() {
for j in (i + 1)..z.len() {
let kij = kernel_k_ij_h(&z, h, i, j);
let kji = kernel_k_ij_h(&z, h, j, i);
assert!(
(kij - kji).abs() < 1e-14,
"K[{i},{j}]={kij} != K[{j},{i}]={kji}"
);
}
}
}
}