use nalgebra::DMatrix;
pub fn gauss_hermite(n: usize) -> (Vec<f64>, Vec<f64>) {
let mut j = DMatrix::<f64>::zeros(n, n);
for k in 1..n {
let b = (k as f64).sqrt();
j[(k, k - 1)] = b;
j[(k - 1, k)] = b;
}
let eig = j.symmetric_eigen();
let nodes: Vec<f64> = (0..n).map(|i| eig.eigenvalues[i]).collect();
let weights: Vec<f64> = (0..n).map(|i| eig.eigenvectors[(0, i)].powi(2)).collect();
(nodes, weights)
}
pub fn smoothed_value_1d(f: impl Fn(f64) -> f64, theta: f64, sigma: f64, n: usize) -> f64 {
let (x, w) = gauss_hermite(n);
(0..n).map(|k| w[k] * f(theta + sigma * x[k])).sum()
}
pub fn smoothed_grad_1d(f: impl Fn(f64) -> f64, theta: f64, sigma: f64, n: usize) -> f64 {
let (x, w) = gauss_hermite(n);
(0..n).map(|k| w[k] * x[k] * f(theta + sigma * x[k])).sum::<f64>() / sigma
}
fn normal_samples(count: usize, seed: u64) -> Vec<f64> {
let mut s = seed;
let mut next = || {
s = s.wrapping_add(0x9E3779B97F4A7C15);
let mut z = s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
((z ^ (z >> 31)) as f64) / (u64::MAX as f64)
};
let mut out = Vec::with_capacity(count);
while out.len() < count {
let u1 = next().max(1e-12);
let u2 = next();
let r = (-2.0 * u1.ln()).sqrt();
out.push(r * (std::f64::consts::TAU * u2).cos());
out.push(r * (std::f64::consts::TAU * u2).sin());
}
out.truncate(count);
out
}
pub fn smoothed_grad(f: impl Fn(&[f64]) -> f64, theta: &[f64], sigma: f64, n_samples: usize) -> Vec<f64> {
let d = theta.len();
let xi = normal_samples(n_samples * d, 0xF1u64);
let mut g = vec![0.0f64; d];
let mut cand = vec![0.0f64; d];
for s in 0..n_samples {
for sign in [1.0f64, -1.0] {
for i in 0..d {
cand[i] = theta[i] + sign * sigma * xi[s * d + i];
}
let fv = f(&cand);
for i in 0..d {
g[i] += sign * xi[s * d + i] * fv;
}
}
}
let norm = 1.0 / (sigma * (2 * n_samples) as f64);
g.iter_mut().for_each(|v| *v *= norm);
g
}
#[cfg(test)]
mod verification {
use super::*;
#[test]
fn smoothed_grad_recovers_true_derivative() {
let f = |t: f64| (0.7 * t).sin() + 0.3 * t * t; for &t in &[-1.0f64, 0.0, 0.6, 1.5] {
let exact = 0.7 * (0.7 * t).cos() + 0.6 * t;
let g = smoothed_grad_1d(f, t, 1e-3, 21);
assert!((g - exact).abs() < 1e-4, "smoothed grad {g} vs exact {exact} at {t}");
}
}
fn projectile(theta: f64) -> f64 {
let (d, h, g) = (5.0, 2.0, 9.8);
let y_at_wall = d - g * d * d / (theta * theta); if y_at_wall > h {
theta * theta / g } else {
d }
}
#[test]
fn smoothed_grad_is_informative_across_make_break() {
let target = 10.0;
let j = |theta: f64| (projectile(theta) - target).powi(2);
let theta0 = 8.0;
let raw = (j(theta0 + 1e-6) - j(theta0 - 1e-6)) / 2e-6;
let smoothed = smoothed_grad_1d(j, theta0, 0.6, 41);
eprintln!("at θ={theta0} (blocked): raw grad {raw:.3e}, smoothed grad {smoothed:.3e}");
assert!(raw.abs() < 1e-6, "raw gradient should be ~0 in the flat region");
assert!(smoothed < -1.0, "smoothed gradient should point toward clearing (negative J-grad): {smoothed}");
}
#[test]
fn smoothed_descent_crosses_the_boundary() {
let target = 10.0;
let j = |theta: f64| (projectile(theta) - target).powi(2);
let mut tn = 8.0;
for _ in 0..200 {
let g = (j(tn + 1e-6) - j(tn - 1e-6)) / 2e-6;
tn -= 0.02 * g;
}
let naive_blocked = projectile(tn) < 5.5;
let mut ts = 8.0;
let mut sigma = 0.8;
for it in 0..200 {
let g = smoothed_grad_1d(j, ts, sigma, 41);
ts -= 0.05 * g;
ts = ts.clamp(6.0, 14.0);
if it % 40 == 39 {
sigma *= 0.6; }
}
let landed = projectile(ts);
eprintln!("naive final θ={tn:.3} (blocked={naive_blocked}); smoothed final θ={ts:.3}, landed {landed:.3} (target {target})");
assert!(naive_blocked, "naive descent unexpectedly escaped — weaken the test scenario");
assert!((landed - target).abs() < 0.3, "smoothed descent did not reach the target: landed {landed}");
}
}