use super::jet::taylor_jet_2nd_with_buf;
use super::types::{DivergenceResult, EstimatorResult, WelfordAccumulator};
use crate::bytecode_tape::BytecodeTape;
use crate::dual::Dual;
use crate::Float;
fn stde_2nd_inner<F: Float>(
tape: &BytecodeTape<F>,
x: &[F],
z_vectors: &[&[F]],
matvec: impl Fn(&[F], &mut [F]),
) -> EstimatorResult<F> {
let n = tape.num_inputs();
let two = F::from(2.0).unwrap();
let mut buf = Vec::new();
let mut v = vec![F::zero(); n];
let mut value = F::zero();
let mut acc = WelfordAccumulator::new();
for z in z_vectors.iter() {
assert_eq!(z.len(), n, "z_vector length must match tape.num_inputs()");
matvec(z, &mut v);
let (c0, _, c2) = taylor_jet_2nd_with_buf(tape, x, &v, &mut buf);
value = c0;
acc.update(two * c2);
}
let (estimate, sample_variance, standard_error) = acc.finalize();
EstimatorResult {
value,
estimate,
sample_variance,
standard_error,
num_samples: z_vectors.len(),
}
}
pub fn divergence<F: Float>(
tape: &BytecodeTape<F>,
x: &[F],
directions: &[&[F]],
) -> DivergenceResult<F> {
assert!(!directions.is_empty(), "directions must not be empty");
let n = tape.num_inputs();
let m = tape.num_outputs();
assert_eq!(
m, n,
"divergence requires num_outputs ({}) == num_inputs ({})",
m, n
);
assert_eq!(x.len(), n, "x.len() must match tape.num_inputs()");
let out_indices = tape.all_output_indices();
let mut buf: Vec<Dual<F>> = Vec::new();
let mut values = vec![F::zero(); m];
let mut acc = WelfordAccumulator::new();
for (k, v) in directions.iter().enumerate() {
assert_eq!(v.len(), n, "direction length must match tape.num_inputs()");
let dual_inputs: Vec<Dual<F>> = x
.iter()
.zip(v.iter())
.map(|(&xi, &vi)| Dual::new(xi, vi))
.collect();
tape.forward_tangent(&dual_inputs, &mut buf);
if k == 0 {
for (i, &oi) in out_indices.iter().enumerate() {
values[i] = buf[oi as usize].re;
}
}
let sample: F = v
.iter()
.zip(out_indices.iter())
.fold(F::zero(), |acc, (&vi, &oi)| acc + vi * buf[oi as usize].eps);
acc.update(sample);
}
let (estimate, sample_variance, standard_error) = acc.finalize();
DivergenceResult {
values,
estimate,
sample_variance,
standard_error,
num_samples: directions.len(),
}
}
pub fn parabolic_diffusion<F: Float>(
tape: &BytecodeTape<F>,
x: &[F],
sigma_columns: &[&[F]],
) -> (F, F) {
assert!(!sigma_columns.is_empty(), "sigma_columns must not be empty");
let n = tape.num_inputs();
assert_eq!(x.len(), n, "x.len() must match tape.num_inputs()");
let half = F::from(0.5).unwrap();
let two = F::from(2.0).unwrap();
let mut buf = Vec::new();
let mut value = F::zero();
let mut sum = F::zero();
for col in sigma_columns {
assert_eq!(
col.len(),
n,
"sigma column length must match tape.num_inputs()"
);
let (c0, _, c2) = taylor_jet_2nd_with_buf(tape, x, col, &mut buf);
value = c0;
sum = sum + two * c2; }
(value, half * sum)
}
pub fn parabolic_diffusion_stochastic<F: Float>(
tape: &BytecodeTape<F>,
x: &[F],
sigma_columns: &[&[F]],
sampled_indices: &[usize],
) -> EstimatorResult<F> {
assert!(
!sampled_indices.is_empty(),
"sampled_indices must not be empty"
);
let n = tape.num_inputs();
let d = sigma_columns.len();
assert_eq!(x.len(), n, "x.len() must match tape.num_inputs()");
let two = F::from(2.0).unwrap();
let half = F::from(0.5).unwrap();
let df = F::from(d).unwrap();
let mut buf = Vec::new();
let mut value = F::zero();
let mut acc = WelfordAccumulator::new();
for &i in sampled_indices {
assert!(i < d, "sampled index {} out of bounds (d={})", i, d);
let col = sigma_columns[i];
assert_eq!(
col.len(),
n,
"sigma column length must match tape.num_inputs()"
);
let (c0, _, c2) = taylor_jet_2nd_with_buf(tape, x, col, &mut buf);
value = c0;
acc.update(two * c2); }
let (mean, sample_variance, standard_error) = acc.finalize();
let scale = df * half;
EstimatorResult {
value,
estimate: mean * scale,
sample_variance: sample_variance * scale * scale,
standard_error: standard_error * scale,
num_samples: sampled_indices.len(),
}
}
pub fn dense_stde_2nd<F: Float>(
tape: &BytecodeTape<F>,
x: &[F],
cholesky_rows: &[&[F]],
z_vectors: &[&[F]],
) -> EstimatorResult<F> {
assert!(!z_vectors.is_empty(), "z_vectors must not be empty");
let n = tape.num_inputs();
assert_eq!(x.len(), n, "x.len() must match tape.num_inputs()");
assert_eq!(
cholesky_rows.len(),
n,
"cholesky_rows.len() must match tape.num_inputs()"
);
stde_2nd_inner(tape, x, z_vectors, |z, v| {
for i in 0..n {
let row = cholesky_rows[i];
let mut sum = F::zero();
for j in 0..=i {
sum = sum + row[j] * z[j];
}
v[i] = sum;
}
})
}
#[cfg(feature = "nalgebra")]
#[must_use]
pub fn dense_stde_2nd_indefinite(
tape: &BytecodeTape<f64>,
x: &[f64],
c_matrix: &nalgebra::DMatrix<f64>,
z_vectors: &[&[f64]],
eps_factor: f64,
) -> EstimatorResult<f64> {
assert!(!z_vectors.is_empty(), "z_vectors must not be empty");
let n = tape.num_inputs();
assert_eq!(x.len(), n, "x.len() must match tape.num_inputs()");
assert_eq!(c_matrix.nrows(), n, "c_matrix rows must match num_inputs");
assert_eq!(c_matrix.ncols(), n, "c_matrix cols must match num_inputs");
let eigen = nalgebra::SymmetricEigen::new(c_matrix.clone());
let eigenvalues = &eigen.eigenvalues;
let q = &eigen.eigenvectors;
let max_abs = eigenvalues.iter().fold(0.0f64, |m, &v| m.max(v.abs()));
let eps = eps_factor * max_abs;
let mut has_positive = false;
let mut has_negative = false;
let mut sqrt_pos = vec![0.0f64; n];
let mut sqrt_neg = vec![0.0f64; n];
for i in 0..n {
let lam = eigenvalues[i];
if lam > eps {
sqrt_pos[i] = lam.sqrt();
has_positive = true;
} else if lam < -eps {
sqrt_neg[i] = (-lam).sqrt();
has_negative = true;
}
}
if !has_positive && !has_negative {
let mut buf = Vec::new();
let v = vec![0.0f64; n];
let (value, _, _) = taylor_jet_2nd_with_buf(tape, x, &v, &mut buf);
return EstimatorResult {
value,
estimate: 0.0,
sample_variance: 0.0,
standard_error: 0.0,
num_samples: z_vectors.len(),
};
}
let l_pos = if has_positive {
let mut m = nalgebra::DMatrix::zeros(n, n);
for j in 0..n {
if sqrt_pos[j] > 0.0 {
for i in 0..n {
m[(i, j)] = q[(i, j)] * sqrt_pos[j];
}
}
}
Some(m)
} else {
None
};
let l_neg = if has_negative {
let mut m = nalgebra::DMatrix::zeros(n, n);
for j in 0..n {
if sqrt_neg[j] > 0.0 {
for i in 0..n {
m[(i, j)] = q[(i, j)] * sqrt_neg[j];
}
}
}
Some(m)
} else {
None
};
if has_positive && !has_negative {
let lp = l_pos.as_ref().unwrap();
return stde_2nd_inner(tape, x, z_vectors, |z, v| {
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
sum += lp[(i, j)] * z[j];
}
v[i] = sum;
}
});
}
if !has_positive && has_negative {
let ln = l_neg.as_ref().unwrap();
let mut result = stde_2nd_inner(tape, x, z_vectors, |z, v| {
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
sum += ln[(i, j)] * z[j];
}
v[i] = sum;
}
});
result.estimate = -result.estimate;
return result;
}
let lp = l_pos.as_ref().unwrap();
let ln = l_neg.as_ref().unwrap();
let mut buf = Vec::new();
let mut v_pos = vec![0.0f64; n];
let mut v_neg = vec![0.0f64; n];
let mut value = 0.0f64;
let mut acc = WelfordAccumulator::new();
for z in z_vectors.iter() {
assert_eq!(z.len(), n, "z_vector length must match tape.num_inputs()");
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
sum += lp[(i, j)] * z[j];
}
v_pos[i] = sum;
}
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
sum += ln[(i, j)] * z[j];
}
v_neg[i] = sum;
}
let (c0, _, c2_pos) = taylor_jet_2nd_with_buf(tape, x, &v_pos, &mut buf);
let (_, _, c2_neg) = taylor_jet_2nd_with_buf(tape, x, &v_neg, &mut buf);
value = c0;
acc.update(2.0 * c2_pos - 2.0 * c2_neg);
}
let (estimate, sample_variance, standard_error) = acc.finalize();
EstimatorResult {
value,
estimate,
sample_variance,
standard_error,
num_samples: z_vectors.len(),
}
}