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}