#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CurvatureResolution {
pub eps_f: f64,
pub m4: f64,
pub curvature: f64,
pub optimal_step: f64,
pub delta_sigma_min: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CurvatureResolutionError {
TooFewRows { rows: usize },
NonFiniteInput,
InsufficientSpan { decades: f64 },
TruncationNotResolved { slope: f64 },
TruncationRegimeTooShort { agreeing_rows: usize },
PlateauNotReached { spread: f64 },
}
impl std::fmt::Display for CurvatureResolutionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooFewRows { rows } => write!(
f,
"curvature resolution needs at least 4 ladder rows, got {rows}"
),
Self::NonFiniteInput => {
write!(f, "curvature resolution: non-finite or non-positive input")
}
Self::InsufficientSpan { decades } => write!(
f,
"curvature resolution needs the steps to span at least 2 decades \
to separate truncation from evaluation noise, got {decades:.2}"
),
Self::TruncationNotResolved { slope } => write!(
f,
"curvature resolution: truncation slope {slope:.6e} is not positive, \
so M4 cannot be read from this ladder"
),
Self::TruncationRegimeTooShort { agreeing_rows } => write!(
f,
"curvature resolution: only {agreeing_rows} large-step row(s) agree on \
2*delta/alpha^2, so there is no clean truncation regime to regress"
),
Self::PlateauNotReached { spread } => write!(
f,
"curvature resolution: the small-step residuals still vary by \
{spread:.2e}x, so no evaluation-noise plateau has been reached; \
extend the ladder to smaller steps"
),
}
}
}
#[cfg(test)]
mod curvature_resolution_tests {
use super::{CurvatureResolutionError, curvature_resolution_from_ladder};
fn ladder(c: f64, m4: f64, eps_f: f64, steps: &[f64]) -> (Vec<f64>, Vec<f64>) {
let deltas = steps
.iter()
.enumerate()
.map(|(index, alpha)| {
let sign = if index % 2 == 0 { 1.0 } else { -1.0 };
0.5 * c * alpha * alpha + (m4 / 24.0) * alpha.powi(4) + sign * eps_f
})
.collect();
(steps.to_vec(), deltas)
}
#[test]
fn recovers_known_eps_f_and_m4_from_a_synthetic_ladder_2690() {
let (c, m4, eps_f) = (121.6_f64, 100.0_f64, 1.5e-8_f64);
let steps = [1.0e-1, 5.0e-2, 2.5e-2, 1.25e-2, 1.0e-3, 3.0e-4, 1.0e-4, 3.0e-5];
let (steps, deltas) = ladder(c, m4, eps_f, &steps);
let resolved = curvature_resolution_from_ladder(&steps, &deltas)
.expect("a two-regime ladder must meter");
eprintln!(
"#2690 recovered: eps_f {:.6e} (true {eps_f:.6e}), M4 {:.6e} (true {m4:.6e}), \
curvature {:.6e} (true {c:.6e}), h* {:.6e}, delta_sigma_min {:.6e}",
resolved.eps_f, resolved.m4, resolved.curvature, resolved.optimal_step,
resolved.delta_sigma_min
);
assert!(
(resolved.curvature - c).abs() <= 1.0e-3 * c,
"#2690: curvature {:.6e} must recover {c:.6e}",
resolved.curvature
);
assert!(
(resolved.m4 - m4).abs() <= 0.05 * m4,
"#2690: M4 {:.6e} must recover {m4:.6e}",
resolved.m4
);
assert!(
resolved.eps_f >= 0.5 * eps_f && resolved.eps_f <= 2.0 * eps_f,
"#2690: eps_f {:.6e} must recover {eps_f:.6e} within a factor of 2",
resolved.eps_f
);
let expected_step = (48.0 * resolved.eps_f / resolved.m4).powf(0.25);
let expected_dsigma = (2.0 / 3.0_f64.sqrt()) * (resolved.eps_f * resolved.m4).sqrt();
assert!((resolved.optimal_step - expected_step).abs() <= 1e-12 * expected_step);
assert!((resolved.delta_sigma_min - expected_dsigma).abs() <= 1e-12 * expected_dsigma);
let (_, finer) = ladder(c, m4, eps_f / 100.0, &steps);
let finer = curvature_resolution_from_ladder(&steps, &finer)
.expect("the finer ladder must also meter");
let gain = resolved.delta_sigma_min / finer.delta_sigma_min;
assert!(
(gain - 10.0).abs() <= 1.0,
"#2690: a 100x cut in eps_f must buy ~10x in resolvable curvature \
(sqrt law), measured {gain:.3}x"
);
}
#[test]
fn refuses_rather_than_fabricating_when_the_ladder_cannot_support_it_2690() {
let short = [1.0e-2, 1.0e-3, 1.0e-4];
let (s, d) = ladder(121.6, 100.0, 1.5e-8, &short);
assert!(matches!(
curvature_resolution_from_ladder(&s, &d),
Err(CurvatureResolutionError::TooFewRows { rows: 3 })
));
let narrow = [1.0e-3, 9.0e-4, 8.0e-4, 7.0e-4, 6.0e-4];
let (s, d) = ladder(121.6, 100.0, 1.5e-8, &narrow);
assert!(matches!(
curvature_resolution_from_ladder(&s, &d),
Err(CurvatureResolutionError::InsufficientSpan { .. })
));
let truncation_only = [1.0e-1, 5.0e-2, 2.5e-2, 1.25e-2, 6.25e-3, 3.125e-3];
let (s, d) = ladder(121.6, 100.0, 0.0, &truncation_only);
assert!(
matches!(
curvature_resolution_from_ladder(&s, &d),
Err(CurvatureResolutionError::PlateauNotReached { .. })
),
"#2690: a ladder with no noise floor in range must refuse, not \
report the truncation residual as an evaluation error"
);
let (s, d) = ladder(121.6, 100.0, 1.5e-8, &[1.0e-2, 1.0e-3, 0.0, 1.0e-5]);
assert!(matches!(
curvature_resolution_from_ladder(&s, &d),
Err(CurvatureResolutionError::NonFiniteInput)
));
}
}