219 lines
7.4 KiB
Rust
219 lines
7.4 KiB
Rust
//! The named tolerance classes.
|
|
//!
|
|
//! Appendix D §"Tolerance Classes": specifications and conformance documents
|
|
//! **must not** introduce ad-hoc epsilon constants. Every numerical tolerance
|
|
//! that affects normative behavior must be declared as a named [`Tolerance`]
|
|
//! belonging to one of the five [`ToleranceClass`]es, with an explicit unit
|
|
//! (implied by the class), absolute and optional relative bounds, and a
|
|
//! [`ToleranceGovernance`] category.
|
|
//!
|
|
//! Tolerances never apply to identity: ids, graph membership, hash identity,
|
|
//! and operation ordering are exact, never "within tolerance".
|
|
//!
|
|
//! The concrete tolerance *values* for each profile and tier are normative in
|
|
//! the companion specifications (Quality Metric Catalog, Reference Suite,
|
|
//! Performance Reference Suite); this crate provides only the vocabulary.
|
|
|
|
use crate::float::CanonicalF64;
|
|
|
|
/// The five tolerance classes. The measurement unit is implied by the class.
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub enum ToleranceClass {
|
|
/// Acoustic pitch comparison, in cents (Chapter 2 §"Cents as the Offset
|
|
/// Unit"; Chapter 4 reference-pitch frequency resolution).
|
|
AcousticCents,
|
|
|
|
/// Layout coordinate comparison, in staff spaces (Chapter 7 §7.2).
|
|
LayoutCoordinate,
|
|
|
|
/// Quality-metric comparison; absolute on the `[0.0, 1.0]`
|
|
/// `NormalizedMetric` scale (Chapter 9).
|
|
QualityMetric,
|
|
|
|
/// Tempo-integration residual: maximum permitted error in
|
|
/// `musical_to_wallclock` / `wallclock_to_musical` conversion (Chapter 3).
|
|
TempoIntegration,
|
|
|
|
/// Solver residual: maximum permitted constraint violation for a soft
|
|
/// constraint to count as satisfied (Chapter 9).
|
|
SolverResidual,
|
|
}
|
|
|
|
impl ToleranceClass {
|
|
/// A stable, locale-independent unit label for diagnostics. Non-normative
|
|
/// text; the class identity is what is normative.
|
|
pub const fn unit(self) -> &'static str {
|
|
match self {
|
|
ToleranceClass::AcousticCents => "cents",
|
|
ToleranceClass::LayoutCoordinate => "staff spaces",
|
|
ToleranceClass::QualityMetric => "normalized [0,1]",
|
|
ToleranceClass::TempoIntegration => "wallclock seconds",
|
|
ToleranceClass::SolverResidual => "constraint units",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// How a tolerance participates in canonical behavior.
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub enum ToleranceGovernance {
|
|
/// Affects canonical equality (rare; usually only diagnostic comparison).
|
|
Equality,
|
|
|
|
/// A validation threshold: a constraint counts as satisfied when the
|
|
/// violation is below the tolerance.
|
|
Validation,
|
|
|
|
/// Affects only diagnostic output; canonical state is unaffected.
|
|
Diagnostic,
|
|
}
|
|
|
|
/// A fully specified numerical tolerance.
|
|
///
|
|
/// Bounds are held as [`CanonicalF64`] so a tolerance can never carry NaN,
|
|
/// infinity, or `-0.0` — the same float hygiene canonical state requires
|
|
/// everywhere else.
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
|
pub struct Tolerance {
|
|
/// Which class this tolerance belongs to; fixes the unit.
|
|
pub class: ToleranceClass,
|
|
|
|
/// Absolute tolerance, in the class's unit.
|
|
pub absolute: CanonicalF64,
|
|
|
|
/// Optional relative tolerance, applied to nonzero values.
|
|
pub relative: Option<CanonicalF64>,
|
|
|
|
/// What the tolerance governs.
|
|
pub governance: ToleranceGovernance,
|
|
}
|
|
|
|
impl Tolerance {
|
|
/// Constructs an absolute-only tolerance, rejecting non-finite bounds and
|
|
/// negative absolute bounds (a tolerance is a non-negative magnitude).
|
|
pub fn absolute(
|
|
class: ToleranceClass,
|
|
absolute: f64,
|
|
governance: ToleranceGovernance,
|
|
) -> Option<Self> {
|
|
let absolute = CanonicalF64::new(absolute)?;
|
|
if absolute.get() < 0.0 {
|
|
return None;
|
|
}
|
|
Some(Tolerance {
|
|
class,
|
|
absolute,
|
|
relative: None,
|
|
governance,
|
|
})
|
|
}
|
|
|
|
/// Adds a relative bound, rejecting a non-finite or negative value.
|
|
pub fn with_relative(mut self, relative: f64) -> Option<Self> {
|
|
let relative = CanonicalF64::new(relative)?;
|
|
if relative.get() < 0.0 {
|
|
return None;
|
|
}
|
|
self.relative = Some(relative);
|
|
Some(self)
|
|
}
|
|
|
|
/// Whether `value` is within tolerance of `reference`: the combined
|
|
/// absolute-or-relative test, `|value - reference| <= absolute +
|
|
/// relative * |reference|`.
|
|
///
|
|
/// Returns `false` unless both operands are finite. A tolerance bounds a
|
|
/// real measurement against a real reference; a NaN or infinite operand is
|
|
/// never "within tolerance" (without this guard, `within(0.0, inf)` with a
|
|
/// relative bound would spuriously return `true`, since both the difference
|
|
/// and the bound become infinite).
|
|
///
|
|
/// This is a deterministic *validation/diagnostic* comparison. It must
|
|
/// never be used to decide identity (Appendix D: tolerances never apply to
|
|
/// ids, membership, ordering, or hashes).
|
|
pub fn within(&self, value: f64, reference: f64) -> bool {
|
|
if !value.is_finite() || !reference.is_finite() {
|
|
return false;
|
|
}
|
|
let diff = (value - reference).abs();
|
|
let bound = self.absolute.get() + self.relative.map_or(0.0, |r| r.get() * reference.abs());
|
|
diff <= bound
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn absolute_constructor_rejects_bad_bounds() {
|
|
assert!(Tolerance::absolute(
|
|
ToleranceClass::AcousticCents,
|
|
f64::NAN,
|
|
ToleranceGovernance::Validation
|
|
)
|
|
.is_none());
|
|
assert!(Tolerance::absolute(
|
|
ToleranceClass::AcousticCents,
|
|
-1.0,
|
|
ToleranceGovernance::Validation
|
|
)
|
|
.is_none());
|
|
assert!(Tolerance::absolute(
|
|
ToleranceClass::AcousticCents,
|
|
0.0,
|
|
ToleranceGovernance::Validation
|
|
)
|
|
.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn within_uses_absolute_and_relative() {
|
|
let t = Tolerance::absolute(
|
|
ToleranceClass::LayoutCoordinate,
|
|
0.01,
|
|
ToleranceGovernance::Validation,
|
|
)
|
|
.unwrap();
|
|
assert!(t.within(1.005, 1.0));
|
|
assert!(!t.within(1.02, 1.0));
|
|
|
|
let tr = t.with_relative(0.1).unwrap();
|
|
// bound = 0.01 + 0.1 * |10| = 1.01
|
|
assert!(tr.within(11.0, 10.0));
|
|
assert!(!tr.within(11.1, 10.0));
|
|
}
|
|
|
|
#[test]
|
|
fn within_rejects_non_finite_operands() {
|
|
let tr = Tolerance::absolute(
|
|
ToleranceClass::TempoIntegration,
|
|
0.01,
|
|
ToleranceGovernance::Validation,
|
|
)
|
|
.unwrap()
|
|
.with_relative(0.1)
|
|
.unwrap();
|
|
// Without the finite guard, diff and bound both become inf -> true.
|
|
assert!(!tr.within(0.0, f64::INFINITY));
|
|
assert!(!tr.within(f64::INFINITY, 0.0));
|
|
assert!(!tr.within(f64::NAN, 0.0));
|
|
assert!(!tr.within(0.0, f64::NAN));
|
|
}
|
|
|
|
#[test]
|
|
fn classes_have_distinct_units() {
|
|
let classes = [
|
|
ToleranceClass::AcousticCents,
|
|
ToleranceClass::LayoutCoordinate,
|
|
ToleranceClass::QualityMetric,
|
|
ToleranceClass::TempoIntegration,
|
|
ToleranceClass::SolverResidual,
|
|
];
|
|
for (i, a) in classes.iter().enumerate() {
|
|
for b in &classes[i + 1..] {
|
|
assert_ne!(a, b);
|
|
}
|
|
}
|
|
}
|
|
}
|