use crate::no_arb::evidence::{
DiagnosticError, RootEvidence, RootTermination, ScanConfig, ScanEvidence,
};
use crate::numerics::{brent_root_with_evidence, index_to_f64};
use crate::smile::raw::RawSvi;
const SCAN_POINTS: usize = 401;
const SCAN_MARGIN: f64 = 1.0;
const REFINE_TOL: f64 = 1e-10;
const REFINE_MAX_ITER: usize = 200;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CalendarDiagnostic {
violation_observed: bool,
min_difference: f64,
worst_k: f64,
refined_crossing: Option<f64>,
evidence: ScanEvidence,
}
impl CalendarDiagnostic {
#[must_use]
pub const fn violation_observed(self) -> bool {
self.violation_observed
}
#[must_use]
pub const fn min_difference(self) -> f64 {
self.min_difference
}
#[must_use]
pub const fn worst_k(self) -> f64 {
self.worst_k
}
#[must_use]
pub const fn refined_crossing(self) -> Option<f64> {
self.refined_crossing
}
#[must_use]
pub const fn evidence(self) -> ScanEvidence {
self.evidence
}
}
pub fn calendar_scan(
early: &RawSvi,
late: &RawSvi,
k_lo: f64,
k_hi: f64,
) -> Result<CalendarDiagnostic, DiagnosticError> {
if !k_lo.is_finite() || !k_hi.is_finite() {
return Err(DiagnosticError::NonFiniteBound);
}
if k_lo >= k_hi {
return Err(DiagnosticError::InvalidOrder);
}
let lo = k_lo - SCAN_MARGIN;
let hi = k_hi + SCAN_MARGIN;
if ScanConfig::new(lo, hi, SCAN_POINTS, 0.0).is_none() {
return Err(DiagnosticError::DomainOverflow);
}
let step = (hi - lo) / index_to_f64(SCAN_POINTS - 1);
let difference = |k: f64| late.total_variance(k) - early.total_variance(k);
let mut min_difference = f64::INFINITY;
let mut max_variance_scale = 1.0_f64;
let mut worst_k = lo;
let mut samples = Vec::with_capacity(SCAN_POINTS);
for index in 0..SCAN_POINTS {
let k = step.mul_add(index_to_f64(index), lo);
let early_variance = early.total_variance(k);
let late_variance = late.total_variance(k);
let value = late_variance - early_variance;
if !value.is_finite() {
return Err(DiagnosticError::NonFiniteEvaluation);
}
max_variance_scale = max_variance_scale
.max(early_variance.abs())
.max(late_variance.abs());
if value < min_difference {
min_difference = value;
worst_k = k;
}
samples.push((k, value));
}
let evaluation_tolerance = 128.0 * f64::EPSILON * max_variance_scale;
let Some(config) = ScanConfig::new(lo, hi, SCAN_POINTS, evaluation_tolerance) else {
return Err(DiagnosticError::DomainOverflow);
};
let violation_observed = min_difference < -evaluation_tolerance;
let refinement_bracket = violation_observed
.then(|| {
samples
.windows(2)
.filter(|pair| {
pair[0].1 == 0.0
|| pair[1].1 == 0.0
|| pair[0].1.is_sign_negative() != pair[1].1.is_sign_negative()
})
.min_by(|left, right| {
let left_distance = (0.5 * (left[0].0 + left[1].0) - worst_k).abs();
let right_distance = (0.5 * (right[0].0 + right[1].0) - worst_k).abs();
left_distance.total_cmp(&right_distance)
})
.map(|pair| (pair[0].0, pair[1].0))
})
.flatten();
let refinement = refinement_bracket.and_then(|(lower, upper)| {
brent_root_with_evidence(difference, lower, upper, REFINE_TOL, REFINE_MAX_ITER)
});
let refined_crossing = refinement.map(crate::numerics::BrentRoot::root);
let root_evidence = refinement.map(|root| {
RootEvidence::new(
root.root(),
root.lower(),
root.upper(),
root.residual(),
root.evaluations(),
if root.exact() {
RootTermination::ExactRoot
} else {
RootTermination::BracketTolerance
},
)
});
Ok(CalendarDiagnostic {
violation_observed,
min_difference,
worst_k,
refined_crossing,
evidence: ScanEvidence::new(
config,
(k_lo, k_hi),
SCAN_POINTS,
SCAN_POINTS,
refinement_bracket.is_some(),
root_evidence,
),
})
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod tests {
use super::*;
#[test]
fn ordered_slices_have_no_bounded_witness() {
let early =
RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).expect("valid test or documentation fixture");
let late =
RawSvi::new(0.08, 0.3, -0.2, 0.0, 0.1).expect("valid test or documentation fixture");
let report = calendar_scan(&early, &late, -0.5, 0.5).expect("finite diagnostic bounds");
assert!(!report.violation_observed());
assert!(report.min_difference() > 0.0);
}
#[test]
fn rejects_unordered_bounds() {
let early = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).expect("valid test fixture");
let late = RawSvi::new(0.08, 0.3, -0.2, 0.0, 0.1).expect("valid test fixture");
assert_eq!(
calendar_scan(&early, &late, 1.0, -1.0),
Err(DiagnosticError::InvalidOrder)
);
assert_eq!(
calendar_scan(&early, &late, 0.0, 0.0),
Err(DiagnosticError::InvalidOrder)
);
}
#[test]
fn crossing_slices_produce_a_witness() {
let early =
RawSvi::new(0.08, 0.3, -0.2, 0.0, 0.1).expect("valid test or documentation fixture");
let late =
RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).expect("valid test or documentation fixture");
let report = calendar_scan(&early, &late, -0.5, 0.5).expect("finite diagnostic bounds");
assert!(report.violation_observed());
assert!(report.min_difference() < 0.0);
}
#[test]
fn conclusion_is_limited_to_the_observed_domain() {
let early =
RawSvi::new(0.02, 0.5, 0.0, 0.0, 0.1).expect("valid test or documentation fixture");
let late =
RawSvi::new(1.0, 0.1, 0.0, 0.0, 0.1).expect("valid test or documentation fixture");
assert!(
!calendar_scan(&early, &late, -1.0, 1.0)
.expect("finite diagnostic bounds")
.violation_observed()
);
assert!(
calendar_scan(&early, &late, -3.0, 3.0)
.expect("finite diagnostic bounds")
.violation_observed()
);
assert!(
calendar_scan(&early, &late, -3.0, 3.0)
.expect("finite diagnostic bounds")
.refined_crossing()
.is_some()
);
let evidence = calendar_scan(&early, &late, -3.0, 3.0)
.expect("finite diagnostic bounds")
.evidence();
assert_eq!(evidence.requested_domain(), (-3.0, 3.0));
assert!(evidence.config().lower() < -3.0);
assert!(evidence.refinement_attempted());
assert!(evidence.refinement().is_some());
}
}