Skip to main content

axiolid_evaluate/bound/
intrinsic.rs

1//! Chord and derivative bounds of intrinsic curves from their curvature
2//! laws (#317).
3//!
4//! An intrinsic curve is parameterised by its arc length `s`. In its start
5//! frame's coordinates its tangent `T` is a unit vector and `T' = k N`
6//! (Frenet: also for a space curve, whose torsion turns `N` but not the
7//! size of `T'`), so `|c''| = |k|`; in 2D `c''' = k' N - k^2 T` gives
8//! `|c'''| <= |k'| + k^2`.
9//! The start frame maps those coordinates to the curve's, stretching by at
10//! most [`frame_stretch2`] (`frame_stretch3`), so the suprema of `|k|` and
11//! `|k'|` over a span (`crate::chain::curvature_bound`, the law's own
12//! closed-form bound, term by term) give certified derivative suprema, and
13//! `h^2 / 8 sup |c''|` the chord bound.
14//!
15//! A seam of a piecewise law is where `k` may jump. The heading, its
16//! integral, stays continuous, so `c` is `C^1` with `c''` bounded across a
17//! seam and the chord bound crosses it. `c'''` does not exist there: the
18//! third-derivative bound refuses a span with a seam strictly inside.
19//!
20//! These bound the curve the law defines. The evaluator reads its points
21//! by quadrature (`crate::arc_length`, `crate::frenet`) to a stated
22//! estimate; a caller that needs a bound on the points reads them with
23//! [`super::IntrinsicPositions`] instead.
24//!
25//! [`evaluated_chord_bound2`] bounds the chord of the *evaluated* ends
26//! (#414). A chord whose ends each move by at most `g` moves by at most
27//! `g` everywhere, so the law's bound plus the larger of the two ends'
28//! distances from the exact points holds. Each distance is the evaluator's
29//! point against a certified [`IntrinsicPositionTable`] point, plus that
30//! point's own certified error. The chain bound (`crate::chain`) charges
31//! the evaluated ends of its pieces the same way against a
32//! [`ChainPositionTable`](super::ChainPositionTable) (#438).
33
34use axiolid_core::Scalar;
35use axiolid_curve::{CurvatureLaw, Intrinsic2, Intrinsic3};
36
37use super::interval::Iv;
38use super::{
39    frame_stretch2, frame_stretch3, inflate, ordered, taylor, CurveDerivativeBounds,
40    IntrinsicPositionTable,
41};
42use crate::arc_length::intrinsic_point;
43use crate::chain::curvature_bound;
44
45/// The span `[lo, hi]` of an intrinsic curve of `length`, or `None` outside
46/// `[0, length]`.
47fn span(a: Scalar, b: Scalar, length: Scalar) -> Option<(Scalar, Scalar)> {
48    if !(a.is_finite() && b.is_finite() && length.is_finite() && length > 0.0) {
49        return None;
50    }
51    let (lo, hi) = ordered(a, b);
52    (lo >= 0.0 && hi <= length).then_some((lo, hi))
53}
54
55/// Certified derivative suprema of an intrinsic law placed by a frame of
56/// `stretch` over `[lo, hi]`: `|c'| <= stretch`, `|c''| <= stretch K`,
57/// `|c'''| <= stretch (K' + K^2)`, the last infinite across a seam.
58fn law_bounds(
59    law: &CurvatureLaw,
60    length: Scalar,
61    stretch: Scalar,
62    lo: Scalar,
63    hi: Scalar,
64) -> Option<CurveDerivativeBounds> {
65    let k = curvature_bound(law, lo, hi)?;
66    let seamed = law
67        .seams_within(length)
68        .iter()
69        .any(|&seam| seam > lo && seam < hi);
70    let third = if seamed {
71        Scalar::INFINITY
72    } else {
73        curvature_bound(&law.derivative(), lo, hi)? + k * k
74    };
75    Some(CurveDerivativeBounds {
76        first: inflate(stretch),
77        second: inflate(stretch * k),
78        third: inflate(stretch * third),
79    })
80}
81
82/// Certified derivative suprema of a 2D intrinsic curve over `[a, b]` of
83/// its arc length; `None` outside `[0, length]` or for a law with no
84/// closed-form bound. `third` is infinite over a span with a seam of the
85/// law strictly inside.
86#[must_use]
87pub fn intrinsic_derivative_bounds2(
88    curve: &Intrinsic2,
89    a: Scalar,
90    b: Scalar,
91) -> Option<CurveDerivativeBounds> {
92    let (lo, hi) = span(a, b, curve.length)?;
93    law_bounds(
94        &curve.curvature,
95        curve.length,
96        frame_stretch2(&curve.start),
97        lo,
98        hi,
99    )
100}
101
102/// Certified chord bound of a 2D intrinsic curve over `[a, b]`:
103/// `h^2 / 8 sigma sup |k|`, across seams of its law too. `None` outside
104/// `[0, length]` or for a law with no closed-form bound.
105#[must_use]
106pub fn intrinsic_chord_bound2(curve: &Intrinsic2, a: Scalar, b: Scalar) -> Option<Scalar> {
107    let bounds = intrinsic_derivative_bounds2(curve, a, b)?;
108    let out = inflate(taylor(b - a, bounds.second));
109    out.is_finite().then_some(out)
110}
111
112/// Certified chord bound of a 3D intrinsic curve over `[a, b]`: `h^2 / 8
113/// sigma sup |k|`, whatever its torsion (`|c''| = |k|`). `None` outside
114/// `[0, length]` or for a curvature law with no closed-form bound.
115#[must_use]
116pub fn intrinsic_chord_bound3(curve: &Intrinsic3, a: Scalar, b: Scalar) -> Option<Scalar> {
117    let (lo, hi) = span(a, b, curve.length)?;
118    let k = curvature_bound(&curve.curvature, lo, hi)?;
119    let out = inflate(taylor(hi - lo, frame_stretch3(&curve.start) * k));
120    out.is_finite().then_some(out)
121}
122
123/// Certified bound on the distance from the 2D intrinsic curve over `[a,
124/// b]` to the chord joining its *evaluated* ends (the quadrature points of
125/// `crate::arc_length`, not the exact ones): the law's bound
126/// ([`intrinsic_chord_bound2`]) plus the larger distance of an evaluated end
127/// from the exact point, each certified against a position table whose
128/// error is an eighth of the law's bound (or a few ulps of the placement,
129/// if more). `None` outside `[0, length]`, for a law with no closed-form
130/// bound, or where the table cannot be built.
131pub(super) fn evaluated_chord_bound2(curve: &Intrinsic2, a: Scalar, b: Scalar) -> Option<Scalar> {
132    let (lo, hi) = span(a, b, curve.length)?;
133    let law = intrinsic_chord_bound2(curve, lo, hi)?;
134    let stretch = frame_stretch2(&curve.start);
135    let origin = curve.start.origin.abs().max_element();
136    let floor = 64.0 * Scalar::EPSILON * (1.0 + origin + stretch * hi);
137    let table = IntrinsicPositionTable::new2(curve, (law / 8.0).max(floor), hi)?;
138    let gap = |s: Scalar| {
139        let (exact, error) = table.point2(s)?;
140        let read = intrinsic_point(curve, s).ok()?;
141        let d = [read.x - exact.x, read.y - exact.y]
142            .map(|c| Iv::point(c).sq())
143            .into_iter()
144            .fold(Iv::point(0.0), Iv::add)
145            .sqrt();
146        Some(Iv::point(d.hi).add(Iv::point(error)).hi)
147    };
148    let g = gap(lo)?.max(gap(hi)?);
149    let out = inflate(Iv::point(law).add(Iv::point(g)).hi);
150    out.is_finite().then_some(out)
151}