#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CurvatureLaw {
FiniteDifferenceOfValues,
AnalyticWeyl,
}
impl CurvatureLaw {
pub fn label(self) -> &'static str {
match self {
Self::FiniteDifferenceOfValues => "finite-difference (sqrt(eps_f*M4))",
Self::AnalyticWeyl => "analytic (Weyl, ||dH||_2)",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CurvatureResolutionError {
EvaluationError(f64),
FourthDerivativeBound(f64),
HessianError(f64),
}
impl std::fmt::Display for CurvatureResolutionError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EvaluationError(value) => write!(
formatter,
"a finite-difference curvature resolution needs a positive finite evaluation \
error eps_f measured ON THIS FIXTURE, got {value:.6e}"
),
Self::FourthDerivativeBound(value) => write!(
formatter,
"a finite-difference curvature resolution needs a positive finite \
fourth-derivative bound M4, got {value:.6e}"
),
Self::HessianError(value) => write!(
formatter,
"an analytic curvature resolution is Weyl's ||dH||_2 and needs a non-negative \
(possibly infinite) Hessian error, got {value:.6e}"
),
}
}
}
impl std::error::Error for CurvatureResolutionError {}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MeasuredHessianError {
pub source: &'static str,
pub value: f64,
}
impl MeasuredHessianError {
pub fn new(source: &'static str, value: f64) -> Self {
Self { source, value }
}
}
impl std::fmt::Display for MeasuredHessianError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}={:.6e}", self.source, self.value)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CurvatureResolution {
law: CurvatureLaw,
resolution: f64,
optimal_step: Option<f64>,
dominant_source: Option<&'static str>,
}
impl CurvatureResolution {
pub fn finite_difference(
evaluation_error: f64,
fourth_derivative: f64,
) -> Result<Self, CurvatureResolutionError> {
if !evaluation_error.is_finite() || evaluation_error <= 0.0 {
return Err(CurvatureResolutionError::EvaluationError(evaluation_error));
}
if !fourth_derivative.is_finite() || fourth_derivative <= 0.0 {
return Err(CurvatureResolutionError::FourthDerivativeBound(
fourth_derivative,
));
}
let optimal_step = (48.0 * evaluation_error / fourth_derivative).sqrt().sqrt();
let resolution = (2.0 / 3.0_f64.sqrt()) * (evaluation_error * fourth_derivative).sqrt();
Ok(Self {
law: CurvatureLaw::FiniteDifferenceOfValues,
resolution,
optimal_step: Some(optimal_step),
dominant_source: None,
})
}
pub fn analytic_weyl(hessian_error_2norm: f64) -> Result<Self, CurvatureResolutionError> {
if hessian_error_2norm.is_nan() || hessian_error_2norm < 0.0 {
return Err(CurvatureResolutionError::HessianError(hessian_error_2norm));
}
Ok(Self {
law: CurvatureLaw::AnalyticWeyl,
resolution: hessian_error_2norm,
optimal_step: None,
dominant_source: None,
})
}
pub fn analytic_weyl_from_components(
components: &[MeasuredHessianError],
) -> Result<Self, CurvatureResolutionError> {
let mut dominant: Option<MeasuredHessianError> = None;
for component in components {
if component.value.is_nan() || component.value < 0.0 {
return Err(CurvatureResolutionError::HessianError(component.value));
}
if dominant.is_none_or(|current| component.value > current.value) {
dominant = Some(*component);
}
}
let dominant = dominant.ok_or(CurvatureResolutionError::HessianError(f64::NAN))?;
Ok(Self {
law: CurvatureLaw::AnalyticWeyl,
resolution: dominant.value,
optimal_step: None,
dominant_source: Some(dominant.source),
})
}
pub fn law(&self) -> CurvatureLaw {
self.law
}
pub fn resolution(&self) -> f64 {
self.resolution
}
pub fn resolves(&self, curvature: f64) -> bool {
curvature.is_finite() && curvature.abs() > self.resolution
}
}
impl std::fmt::Display for CurvatureResolution {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.dominant_source {
Some(source) => write!(
formatter,
"{:.6e} [{}; set by {source}]",
self.resolution,
self.law.label()
),
None => write!(formatter, "{:.6e} [{}]", self.resolution, self.law.label()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SymmetricProbe {
pub step: f64,
pub forward: f64,
pub backward: f64,
}
impl SymmetricProbe {
pub fn new(step: f64, forward: f64, backward: f64) -> Self {
Self {
step,
forward,
backward,
}
}
pub fn numerator(&self, baseline: f64) -> f64 {
(self.forward - baseline) + (self.backward - baseline)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LadderCurvature {
pub curvature: f64,
pub curvature_uncertainty: f64,
pub fourth_derivative: f64,
pub evaluation_error: f64,
pub rungs: usize,
}
impl LadderCurvature {
pub fn finite_difference_resolution(
&self,
) -> Result<CurvatureResolution, CurvatureResolutionError> {
CurvatureResolution::finite_difference(self.evaluation_error, self.fourth_derivative.abs())
}
pub fn hessian_error_against(&self, analytic_curvature: f64) -> f64 {
if !analytic_curvature.is_finite()
|| !self.curvature.is_finite()
|| !self.curvature_uncertainty.is_finite()
{
return 0.0;
}
let disagreement = (analytic_curvature - self.curvature).abs();
(disagreement - self.curvature_uncertainty).max(0.0)
}
}
pub fn measure_symmetric_ladder(
baseline: f64,
probes: &[SymmetricProbe],
) -> Option<LadderCurvature> {
if !baseline.is_finite() {
return None;
}
let mut usable: Vec<(f64, f64)> = Vec::with_capacity(probes.len());
for probe in probes {
if !probe.step.is_finite() || probe.step <= 0.0 {
continue;
}
if !probe.forward.is_finite() || !probe.backward.is_finite() {
continue;
}
let numerator = probe.numerator(baseline);
if !numerator.is_finite() {
continue;
}
usable.push((probe.step, numerator));
}
usable.sort_by(|left, right| left.0.total_cmp(&right.0));
usable.dedup_by(|left, right| left.0 == right.0);
let rungs = usable.len();
if rungs < 3 {
return None;
}
let scale = usable
.iter()
.fold(0.0_f64, |accumulated, (step, _)| accumulated.max(*step));
if !(scale > 0.0) || !scale.is_finite() {
return None;
}
let mut a11 = 0.0_f64;
let mut a12 = 0.0_f64;
let mut a22 = 0.0_f64;
let mut b1 = 0.0_f64;
let mut b2 = 0.0_f64;
for (step, numerator) in &usable {
let unit = step / scale;
let square = unit * unit;
let quartic = square * square;
a11 += square * square;
a12 += square * quartic;
a22 += quartic * quartic;
b1 += square * numerator;
b2 += quartic * numerator;
}
let determinant = a11 * a22 - a12 * a12;
if !(determinant.abs() > f64::EPSILON * a11.abs().max(a22.abs()) * a11.abs().max(a22.abs())) {
return None;
}
let unit_curvature = (b1 * a22 - b2 * a12) / determinant;
let unit_quartic = (b2 * a11 - b1 * a12) / determinant;
let curvature = unit_curvature / (scale * scale);
let quartic_coefficient = unit_quartic / (scale * scale * scale * scale);
let fourth_derivative = 12.0 * quartic_coefficient;
if !curvature.is_finite() || !fourth_derivative.is_finite() {
return None;
}
let mut residual_square_sum = 0.0_f64;
for (step, numerator) in &usable {
let square = step * step;
let predicted = curvature * square + quartic_coefficient * square * square;
let residual = numerator - predicted;
residual_square_sum += residual * residual;
}
let residual_variance = residual_square_sum / ((rungs - 2) as f64);
if !residual_variance.is_finite() || residual_variance < 0.0 {
return None;
}
let evaluation_error = residual_variance.sqrt() / 2.0_f64.sqrt();
let inverse_11 = a22 / determinant;
let curvature_uncertainty = (residual_variance * inverse_11).sqrt() / (scale * scale);
if !curvature_uncertainty.is_finite() {
return None;
}
Some(LadderCurvature {
curvature,
curvature_uncertainty,
fourth_derivative,
evaluation_error,
rungs,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_two_laws_are_not_interchangeable_on_the_same_number() {
let eps_f = 1.0e-8;
let as_weyl = CurvatureResolution::analytic_weyl(eps_f).expect("resolution");
let as_difference = CurvatureResolution::finite_difference(eps_f, 1.0).expect("resolution");
assert_ne!(as_weyl.law(), as_difference.law());
assert!(
as_difference.resolution() > 1.0e4 * as_weyl.resolution(),
"sqrt(eps_f) is not eps_f: fd={:.6e} weyl={:.6e}",
as_difference.resolution(),
as_weyl.resolution()
);
}
#[test]
fn resolves_has_a_witness_on_both_sides() {
let resolved = CurvatureResolution::analytic_weyl(9.0e-8).expect("resolution");
assert!(
resolved.resolves(-3.199e-5),
"an eigenvalue far above ||dH||_2 must be resolved"
);
assert!(
!resolved.resolves(-8.0e-9),
"an eigenvalue below ||dH||_2 must NOT be resolved"
);
assert!(
!resolved.resolves(f64::NAN),
"a non-finite curvature carries no magnitude and cannot be resolved"
);
}
#[test]
fn missing_or_impossible_measurements_are_refused_not_defaulted() {
assert_eq!(
CurvatureResolution::finite_difference(0.0, 1.0),
Err(CurvatureResolutionError::EvaluationError(0.0))
);
assert_eq!(
CurvatureResolution::finite_difference(-1.0e-8, 1.0),
Err(CurvatureResolutionError::EvaluationError(-1.0e-8))
);
assert!(matches!(
CurvatureResolution::finite_difference(f64::INFINITY, 1.0),
Err(CurvatureResolutionError::EvaluationError(_))
));
assert_eq!(
CurvatureResolution::finite_difference(1.0e-8, 0.0),
Err(CurvatureResolutionError::FourthDerivativeBound(0.0))
);
assert_eq!(
CurvatureResolution::analytic_weyl(-1.0),
Err(CurvatureResolutionError::HessianError(-1.0))
);
assert!(matches!(
CurvatureResolution::analytic_weyl(f64::NAN),
Err(CurvatureResolutionError::HessianError(_))
));
}
#[test]
fn one_component_is_bit_identical_to_the_single_measurement_law() {
let value = 7.414_101e-16_f64;
let from_one =
CurvatureResolution::analytic_weyl_from_components(&[MeasuredHessianError::new(
"eigensolver backward error",
value,
)])
.expect("one non-negative component");
let direct = CurvatureResolution::analytic_weyl(value).expect("non-negative");
assert_eq!(
from_one.resolution().to_bits(),
direct.resolution().to_bits()
);
assert_eq!(from_one.law(), direct.law());
}
#[test]
fn no_measured_component_is_an_error_not_a_zero_resolution() {
assert!(matches!(
CurvatureResolution::analytic_weyl_from_components(&[]),
Err(CurvatureResolutionError::HessianError(_))
));
}
#[test]
fn a_negative_component_is_rejected_even_beside_a_larger_valid_one() {
assert_eq!(
CurvatureResolution::analytic_weyl_from_components(&[
MeasuredHessianError::new("valid", 1.0e-6),
MeasuredHessianError::new("invalid", -1.0e-9),
]),
Err(CurvatureResolutionError::HessianError(-1.0e-9))
);
assert!(matches!(
CurvatureResolution::analytic_weyl_from_components(&[
MeasuredHessianError::new("valid", 1.0e-6),
MeasuredHessianError::new("invalid", f64::NAN),
]),
Err(CurvatureResolutionError::HessianError(_))
));
}
fn deterministic_wobble(index: usize, magnitude: f64) -> f64 {
let phase = (index as f64) * std::f64::consts::PI * 0.6180339887498949;
magnitude * phase.sin()
}
#[test]
fn the_ladder_recovers_the_curvature_m4_and_eps_f_it_was_built_from() {
let curvature = -6.4e-6_f64;
let m4 = 3.5e-2_f64;
let eps_f = 2.0e-12_f64;
let gradient = 1.0e-3_f64;
let baseline = 2.2244462e3_f64;
let mut probes = Vec::new();
let mut step = 1.0_f64;
for index in 0..12 {
let quadratic = 0.5 * curvature * step * step;
let quartic = m4 * step.powi(4) / 24.0;
let forward = baseline
+ gradient * step
+ quadratic
+ quartic
+ deterministic_wobble(2 * index, eps_f);
let backward = baseline - gradient * step
+ quadratic
+ quartic
+ deterministic_wobble(2 * index + 1, eps_f);
probes.push(SymmetricProbe::new(step, forward, backward));
step *= 0.5;
}
let measured = measure_symmetric_ladder(baseline, &probes)
.expect("twelve well-formed rungs must yield a measurement");
assert_eq!(measured.rungs, 12);
assert!(
(measured.curvature - curvature).abs() <= 1.0e-3 * curvature.abs(),
"the intercept must recover the planted curvature: got {:.6e} want {curvature:.6e}",
measured.curvature
);
assert!(
(measured.fourth_derivative - m4).abs() <= 1.0e-3 * m4,
"twelve times the slope must recover the planted M4: got {:.6e} want {m4:.6e}",
measured.fourth_derivative
);
assert!(
measured.evaluation_error > 0.1 * eps_f && measured.evaluation_error < 10.0 * eps_f,
"the residual scatter must recover eps_f to within an order: got {:.6e} want \
{eps_f:.6e}",
measured.evaluation_error
);
assert!(
gradient > 100.0 * curvature.abs(),
"the negative control must actually be a hazard: g={gradient:.3e} vs \
c={curvature:.3e}"
);
}
#[test]
fn a_disagreement_is_measured_and_an_agreement_measures_nothing() {
let curvature = 1.0e-8_f64;
let m4 = 1.0e-2_f64;
let eps_f = 1.0e-13_f64;
let baseline = 1.0e3_f64;
let mut probes = Vec::new();
let mut step = 1.0_f64;
for index in 0..10 {
let value = baseline + 0.5 * curvature * step * step + m4 * step.powi(4) / 24.0;
probes.push(SymmetricProbe::new(
step,
value + deterministic_wobble(2 * index, eps_f),
value + deterministic_wobble(2 * index + 1, eps_f),
));
step *= 0.5;
}
let measured = measure_symmetric_ladder(baseline, &probes).expect("measurement");
let claimed = -6.4e-6_f64;
let error = measured.hessian_error_against(claimed);
let disagreement = (claimed - measured.curvature).abs();
assert!(
error > 0.0,
"a claim {claimed:.3e} against a measured {:.3e} must be a measured error",
measured.curvature
);
assert!(
error <= disagreement,
"the reported error must never exceed the raw disagreement: {error:.6e} > \
{disagreement:.6e}"
);
assert!(
error >= disagreement - measured.curvature_uncertainty,
"the reported error must be the disagreement net of the ladder's own uncertainty"
);
assert_eq!(
measured.hessian_error_against(measured.curvature),
0.0,
"agreement must measure nothing"
);
assert_eq!(
measured
.hessian_error_against(measured.curvature + 0.5 * measured.curvature_uncertainty),
0.0,
"a disagreement inside the ladder's own error bar has demonstrated nothing"
);
}
#[test]
fn a_ladder_that_cannot_determine_the_fit_returns_no_measurement() {
let baseline = 1.0_f64;
assert!(
measure_symmetric_ladder(
baseline,
&[
SymmetricProbe::new(1.0, 1.5, 1.5),
SymmetricProbe::new(0.5, 1.125, 1.125),
]
)
.is_none()
);
assert!(
measure_symmetric_ladder(
baseline,
&[
SymmetricProbe::new(1.0, 1.5, 1.5),
SymmetricProbe::new(1.0, 1.5, 1.5),
SymmetricProbe::new(1.0, 1.5, 1.5),
]
)
.is_none()
);
assert!(
measure_symmetric_ladder(
baseline,
&[
SymmetricProbe::new(1.0, f64::NAN, 1.5),
SymmetricProbe::new(0.5, 1.125, f64::INFINITY),
SymmetricProbe::new(0.25, 1.03, 1.03),
SymmetricProbe::new(0.125, 1.008, 1.008),
]
)
.is_none()
);
assert!(
measure_symmetric_ladder(
f64::NAN,
&[
SymmetricProbe::new(1.0, 1.5, 1.5),
SymmetricProbe::new(0.5, 1.125, 1.125),
SymmetricProbe::new(0.25, 1.03, 1.03),
]
)
.is_none()
);
}
#[test]
fn the_measured_pair_reconstructs_law_one_for_this_fixture() {
let curvature = 1.0e-4_f64;
let m4 = 1.0_f64;
let eps_f = 1.0e-12_f64;
let baseline = 10.0_f64;
let mut probes = Vec::new();
let mut step = 1.0_f64;
for index in 0..10 {
let value = baseline + 0.5 * curvature * step * step + m4 * step.powi(4) / 24.0;
probes.push(SymmetricProbe::new(
step,
value + deterministic_wobble(2 * index, eps_f),
value + deterministic_wobble(2 * index + 1, eps_f),
));
step *= 0.5;
}
let measured = measure_symmetric_ladder(baseline, &probes).expect("measurement");
let resolution = measured
.finite_difference_resolution()
.expect("a positive measured pair yields Law 1");
assert_eq!(resolution.law(), CurvatureLaw::FiniteDifferenceOfValues);
let expected = (2.0 / 3.0_f64.sqrt())
* (measured.evaluation_error * measured.fourth_derivative).sqrt();
assert!(
(resolution.resolution() - expected).abs() <= 1.0e-12 * expected,
"Law 1 must be reconstructed from the ladder's own two measurements"
);
assert!(
resolution.resolves(measured.curvature),
"a curvature {:.3e} must be resolvable at resolution {:.3e}",
measured.curvature,
resolution.resolution()
);
}
}