#[derive(Clone, Copy, Debug)]
pub struct RiddersConfig {
pub initial_step: f64,
pub shrink: f64,
pub rungs: usize,
}
impl Default for RiddersConfig {
fn default() -> Self {
Self {
initial_step: 1.0e-2,
shrink: 2.0,
rungs: 12,
}
}
}
#[derive(Clone, Debug)]
pub struct FdDerivative {
pub value: f64,
pub uncertainty: f64,
pub step: f64,
pub order: usize,
pub ladder: Vec<(f64, f64)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FdVerdict {
Agree,
Disagree,
Unresolved,
}
impl FdDerivative {
pub fn self_band(&self, rel_tol: f64, abs_floor: f64) -> f64 {
rel_tol * self.value.abs().max(abs_floor)
}
pub fn band(&self, analytic: f64, rel_tol: f64, abs_floor: f64) -> f64 {
rel_tol * self.value.abs().max(analytic.abs()).max(abs_floor)
}
pub fn resolved(&self, rel_tol: f64, abs_floor: f64) -> bool {
self.uncertainty.is_finite() && self.uncertainty <= self.self_band(rel_tol, abs_floor)
}
pub fn agreement_bound(&self, analytic: f64, rel_tol: f64, abs_floor: f64) -> f64 {
self.band(analytic, rel_tol, abs_floor) + self.uncertainty
}
pub fn judge(&self, analytic: f64, rel_tol: f64, abs_floor: f64) -> FdVerdict {
if !self.value.is_finite() || !self.resolved(rel_tol, abs_floor) {
return FdVerdict::Unresolved;
}
if (analytic - self.value).abs() > self.agreement_bound(analytic, rel_tol, abs_floor) {
FdVerdict::Disagree
} else {
FdVerdict::Agree
}
}
pub fn ladder_report(&self) -> String {
self.ladder
.iter()
.map(|(h, d)| format!("h={h:.2e} D={d:+.10e}"))
.collect::<Vec<_>>()
.join(" ")
}
}
pub fn ridders_derivative<F>(mut f: F, config: RiddersConfig) -> FdDerivative
where
F: FnMut(f64) -> f64,
{
ridders_from_stencil(|h| (f(h) - f(-h)) / (2.0 * h), config)
}
pub fn ridders_from_stencil<F>(mut stencil: F, config: RiddersConfig) -> FdDerivative
where
F: FnMut(f64) -> f64,
{
assert!(
config.initial_step > 0.0 && config.initial_step.is_finite(),
"ridders_derivative: initial_step must be finite and positive"
);
assert!(
config.shrink > 1.0 && config.shrink.is_finite(),
"ridders_derivative: shrink must exceed 1"
);
assert!(
config.rungs >= 4,
"ridders_derivative: need at least 4 rungs"
);
let mut tableau: Vec<Vec<f64>> = Vec::with_capacity(config.rungs);
let mut ladder: Vec<(f64, f64)> = Vec::with_capacity(config.rungs);
let mut best = FdDerivative {
value: f64::NAN,
uncertainty: f64::INFINITY,
step: f64::NAN,
order: 0,
ladder: Vec::new(),
};
let ratio_sq = config.shrink * config.shrink;
let mut h = config.initial_step;
for i in 0..config.rungs {
let d = stencil(h);
ladder.push((h, d));
let mut row = vec![d];
if i > 0 {
let mut factor = ratio_sq;
for j in 1..=i {
let left = row[j - 1];
let up = tableau[i - 1][j - 1];
let extrapolant = (factor * left - up) / (factor - 1.0);
row.push(extrapolant);
let plateau = if j + 2 <= i {
Some((tableau[i - 1][j], tableau[i - 2][j]))
} else {
None
};
if let Some((previous, before_that)) = plateau {
let error = (extrapolant - left)
.abs()
.max((extrapolant - up).abs())
.max((extrapolant - previous).abs())
.max((extrapolant - before_that).abs());
if extrapolant.is_finite() && error < best.uncertainty {
best.value = extrapolant;
best.uncertainty = error;
best.step = h;
best.order = 2 * (j + 1);
}
}
factor *= ratio_sq;
}
}
tableau.push(row);
h /= config.shrink;
}
best.ladder = ladder;
best
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ridders_matches_closed_form_on_a_benign_objective() {
let f = |t: f64| (2.0 + t).exp() * (1.0 + 0.3 * t).ln();
let exact = {
let e2: f64 = 2.0_f64.exp();
e2 * (1.0_f64).ln() + e2 * 0.3
};
let measured = ridders_derivative(f, RiddersConfig::default());
assert!(
(measured.value - exact).abs() <= 1e-10,
"value {:.12e} vs exact {:.12e}",
measured.value,
exact
);
assert!(
measured.resolved(1e-8, 1e-12),
"uncertainty {:.3e} should certify a smooth objective",
measured.uncertainty
);
assert_eq!(measured.judge(exact, 1e-8, 1e-12), FdVerdict::Agree);
assert!(
measured.order >= 4,
"a smooth objective should accept an extrapolated entry, got order {}",
measured.order
);
}
#[test]
fn ridders_survives_the_scale_a_fixed_step_cannot() {
const SCALE: f64 = 1.664_7e-3;
const AMPLITUDE: f64 = -0.413_86;
let f = |t: f64| AMPLITUDE * (t / SCALE).sin();
let exact = AMPLITUDE / SCALE;
const LADDER_STEP: f64 = 3.0e-4;
let fixed = (f(LADDER_STEP) - f(-LADDER_STEP)) / (2.0 * LADDER_STEP);
let fixed_rel = (fixed - exact).abs() / exact.abs();
assert!(
(fixed_rel - 5.4e-3).abs() < 1.0e-4,
"fixed-step defect should reproduce the reported 5.4e-3, got {fixed_rel:.4e}"
);
let measured = ridders_derivative(f, RiddersConfig::default());
let rel = (measured.value - exact).abs() / exact.abs();
assert!(
rel < 1e-9,
"Ridders value {:.10e} vs exact {:.10e} (rel {rel:.3e}), uncertainty {:.3e}",
measured.value,
exact,
measured.uncertainty
);
assert!(
measured.uncertainty >= (measured.value - exact).abs(),
"uncertainty {:.3e} must bound the realized error {:.3e}",
measured.uncertainty,
(measured.value - exact).abs()
);
assert!(measured.resolved(1e-6, 1e-12));
assert_eq!(measured.judge(exact, 1e-6, 1e-12), FdVerdict::Agree);
assert_eq!(
measured.judge(exact * 1.01, 1e-6, 1e-12),
FdVerdict::Disagree
);
}
#[test]
fn ridders_reports_an_unresolved_component_under_evaluator_noise() {
fn jitter(t: f64) -> f64 {
let mut z = t.to_bits().wrapping_mul(0x9E37_79B9_7F4A_7C15);
z ^= z >> 31;
z = z.wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^= z >> 27;
(z >> 11) as f64 / (1u64 << 53) as f64 - 0.5
}
const SLOPE: f64 = 1.0e-7;
const NOISE: f64 = 1.0e-9;
let noisy = |t: f64| SLOPE * t + NOISE * jitter(t);
let measured = ridders_derivative(noisy, RiddersConfig::default());
assert_eq!(
measured.judge(SLOPE, 1e-3, 1e-9),
FdVerdict::Unresolved,
"a noise-dominated component must not be certified: value={:.3e} uncertainty={:.3e}",
measured.value,
measured.uncertainty
);
let clean = ridders_derivative(|t| SLOPE * t, RiddersConfig::default());
assert_eq!(
clean.judge(SLOPE, 1e-3, 1e-9),
FdVerdict::Agree,
"the noise-free objective must certify: uncertainty={:.3e}",
clean.uncertainty
);
assert!(
(clean.value - SLOPE).abs() <= 1e-16,
"clean value {:.6e} vs slope {SLOPE:.6e}",
clean.value
);
}
#[test]
fn ridders_refuses_a_scale_finer_than_its_finest_rung() {
const SCALE: f64 = 1.0e-7;
let f = |t: f64| (t / SCALE).sin();
let exact = 1.0 / SCALE;
let measured = ridders_derivative(f, RiddersConfig::default());
assert_eq!(
measured.judge(exact, 1e-3, 1e-6),
FdVerdict::Unresolved,
"value={:.3e} uncertainty={:.3e} vs exact={exact:.3e}",
measured.value,
measured.uncertainty
);
let reaching = ridders_derivative(
f,
RiddersConfig {
initial_step: 1.0e-7,
shrink: 2.0,
rungs: 12,
},
);
assert_eq!(reaching.judge(exact, 1e-3, 1e-6), FdVerdict::Agree);
}
#[test]
fn ridders_certifies_a_one_sided_stencil() {
let f = |t: f64| (0.7 + t).exp() * (1.0 + t).sqrt();
let exact = {
let e = 0.7_f64.exp();
e * 1.0 + e * 0.5
};
let measured = ridders_from_stencil(
|h| (-3.0 * f(0.0) + 4.0 * f(h) - f(2.0 * h)) / (2.0 * h),
RiddersConfig::default(),
);
assert_eq!(measured.judge(exact, 1e-6, 1e-12), FdVerdict::Agree);
assert!(
(measured.value - exact).abs() < 1e-8,
"one-sided value {:.12e} vs exact {exact:.12e} (unc {:.3e})",
measured.value,
measured.uncertainty
);
}
#[test]
fn ridders_refuses_the_measured_noise_ladder_that_minted_a_false_disagree() {
const MEASURED: [f64; 12] = [
2.2711765979e-5,
8.8927099284e-5,
-6.9222892307e-6,
-8.8662108055e-5,
1.7342896399e-4,
-4.0490124320e-4,
-7.1388753895e-4,
-3.2858329178e-3,
-7.7696204244e-3,
-9.1939324193e-3,
9.8518321465e-3,
1.6926779062e-2,
];
let mut rung = 0usize;
let measured = ridders_from_stencil(
|_| {
let value = MEASURED[rung];
rung += 1;
value
},
RiddersConfig::default(),
);
assert_eq!(rung, MEASURED.len(), "the whole ladder must be consumed");
const ANALYTIC: f64 = 1.399_835e-7;
assert_eq!(
measured.judge(ANALYTIC, 5e-3, 1e-3),
FdVerdict::Unresolved,
"value={:.6e} uncertainty={:.3e} step={:.2e} order={}",
measured.value,
measured.uncertainty,
measured.step,
measured.order
);
}
#[test]
fn ridders_keeps_its_order_on_a_converged_ladder() {
let f = |t: f64| (1.3 + t).sin() * (0.4 + t).exp();
let exact = {
let (s, c) = 1.3_f64.sin_cos();
let e = 0.4_f64.exp();
c * e + s * e
};
let measured = ridders_derivative(f, RiddersConfig::default());
assert!(
measured.order >= 6,
"a clean ladder should still reach order >= 6, got {}",
measured.order
);
assert!(
(measured.value - exact).abs() < 1e-11,
"value {:.14e} vs exact {exact:.14e} (unc {:.3e}, order {})",
measured.value,
measured.uncertainty,
measured.order
);
assert_eq!(measured.judge(exact, 1e-9, 1e-14), FdVerdict::Agree);
}
#[test]
fn ridders_reports_its_ladder_coarsest_first() {
let measured = ridders_derivative(
|t| 3.0 * t + t * t,
RiddersConfig {
initial_step: 1.0e-2,
shrink: 4.0,
rungs: 5,
},
);
assert_eq!(measured.ladder.len(), 5);
for pair in measured.ladder.windows(2) {
assert!(
pair[0].0 > pair[1].0,
"ladder must shrink: {:.2e} then {:.2e}",
pair[0].0,
pair[1].0
);
}
assert!((measured.ladder[0].0 - 1.0e-2).abs() < 1e-18);
assert!(measured.ladder_report().starts_with("h=1.00e-2 D="));
}
}