Skip to main content

axiolid_evaluate/
bound.rs

1//! Certified derivative and chord bounds (#232).
2//!
3//! A chord of a curve, or a triangle of a surface, is only as close to the
4//! exact geometry as the geometry's second derivative allows, and a sample
5//! at the midpoint does not show that: a midpoint can sit on the chord while
6//! the curve bulges either side of it. This module bounds the derivatives
7//! from the data itself, so the numbers it returns are upper bounds, never
8//! estimates.
9//!
10//! # The chord bound
11//!
12//! For a curve `c` that is `C^1` with a bounded second derivative on
13//! `[a, b]`, the linear interpolant `l` of its ends satisfies
14//! `|c(t) - l(t)| <= (b - a)^2 / 8 * sup |c''|`: componentwise,
15//! `c - l = -int G(t, s) c''(s) ds` with the non-negative Green's function
16//! of `-d^2/dt^2` on `[a, b]`, whose integral is `(t - a)(b - t) / 2 <=
17//! (b - a)^2 / 8`. `l(t)` lies on the chord, so every point of the arc is
18//! within that of the chord. A circular arc of at most half a turn uses its
19//! exact sagitta instead.
20//!
21//! # Where the derivative bounds come from
22//!
23//! - Conics: `c'' = -(r_x cos t X + r_y sin t Y)`, bounded in closed form
24//!   over the interval (the frame is used as given, so a skewed frame is
25//!   accounted for, not assumed away).
26//! - B-splines: the derivative of a B-spline is a B-spline on the derivative
27//!   control polygon, and a B-spline lies in the convex hull of the control
28//!   points that act on its span, so each derivative is bounded by the
29//!   largest of those control points. A rational spline `c = A / w` is
30//!   bounded through the quotient rule, with `w` bounded below by its
31//!   smallest active weight (weights are positive) and `c` bounded by the
32//!   control points' spread about their centre; the centre is subtracted
33//!   first, which leaves every derivative unchanged.
34//! - Elementary surfaces: closed forms over the parameter box. B-spline
35//!   surfaces: the derivative control nets of the tensor product.
36//! - Implicit curves (ADR 0077): cell by cell, the solved parameter's
37//!   slope and bend through the implicit function theorem, with interval
38//!   bounds of the field's partials over the stretch's box; a bridge cell
39//!   by its cubic's control points (`implicit`, #249).
40//!
41//! A B-spline is only as smooth as its knots allow. A knot of multiplicity
42//! `m` leaves a degree `d` spline `C^(d - m)` there, so a bound that needs a
43//! continuous `k`-th derivative refuses an interval with such a knot
44//! strictly inside: the derivative bound would hold on each side but the
45//! Taylor argument does not cross the jump. [`continuity_breaks2`] and
46//! [`continuity_breaks3`] name those knots so a caller can split there.
47//!
48//! Everything is computed in floating point from the data; the bounds are
49//! inflated by [`ROUNDING`] relative so rounding in their own arithmetic
50//! cannot make them undershoot.
51
52use axiolid_core::{Frame2, Frame3, Scalar, Vec2, Vec3};
53use axiolid_curve::{BSplineCurve, Curve2, Curve3};
54use axiolid_surface::{BSplineSurface, Surface};
55
56use crate::curve::span_in;
57use crate::nurbs::SplineAxis;
58
59mod implicit;
60
61/// Relative inflation applied to every returned bound, so the rounding of
62/// the bound's own arithmetic cannot make it undershoot.
63pub const ROUNDING: Scalar = 1e-12;
64
65fn inflate(value: Scalar) -> Scalar {
66    value * (1.0 + ROUNDING)
67}
68
69/// Largest operator norm of a frame's linear part, bounded through its
70/// Gram matrix: `sigma^2 = lambda_max(F^T F) <= 1 + |F^T F - I|_F`. Exact
71/// for an orthonormal frame, and never below the true stretch for a skewed
72/// or scaled one.
73fn gram_stretch(columns: &[Vec3]) -> Scalar {
74    let mut off: Scalar = 0.0;
75    for (i, a) in columns.iter().enumerate() {
76        for (j, b) in columns.iter().enumerate() {
77            let target = if i == j { 1.0 } else { 0.0 };
78            let entry = a.dot(*b) - target;
79            off += entry * entry;
80        }
81    }
82    (1.0 + off.sqrt()).sqrt()
83}
84
85/// The largest factor by which a 3D frame stretches a local vector.
86#[must_use]
87pub fn frame_stretch3(frame: &Frame3) -> Scalar {
88    inflate(gram_stretch(&[frame.x, frame.y, frame.z]))
89}
90
91/// The largest factor by which a 2D frame stretches a local vector.
92#[must_use]
93pub fn frame_stretch2(frame: &Frame2) -> Scalar {
94    let lift = |v: Vec2| Vec3::new(v.x, v.y, 0.0);
95    inflate(gram_stretch(&[lift(frame.x), lift(frame.y)]))
96}
97
98// --- interval helpers -------------------------------------------------------
99
100/// Whether `[lo, hi]` contains `offset + k * period` for some integer `k`.
101fn contains_lattice(lo: Scalar, hi: Scalar, offset: Scalar, period: Scalar) -> bool {
102    let k = ((lo - offset) / period).ceil();
103    offset + k * period <= hi
104}
105
106/// `max cos^2` over `[lo, hi]`.
107fn max_cos2(lo: Scalar, hi: Scalar) -> Scalar {
108    if contains_lattice(lo, hi, 0.0, core::f64::consts::PI) {
109        1.0
110    } else {
111        lo.cos().powi(2).max(hi.cos().powi(2))
112    }
113}
114
115/// `min cos^2` over `[lo, hi]`.
116fn min_cos2(lo: Scalar, hi: Scalar) -> Scalar {
117    if contains_lattice(lo, hi, core::f64::consts::FRAC_PI_2, core::f64::consts::PI) {
118        0.0
119    } else {
120        lo.cos().powi(2).min(hi.cos().powi(2))
121    }
122}
123
124/// `max |cos|` over `[lo, hi]`.
125fn max_abs_cos(lo: Scalar, hi: Scalar) -> Scalar {
126    max_cos2(lo, hi).sqrt()
127}
128
129/// `max |sin|` over `[lo, hi]`.
130fn max_abs_sin(lo: Scalar, hi: Scalar) -> Scalar {
131    (1.0 - min_cos2(lo, hi)).max(0.0).sqrt()
132}
133
134/// `(min cos, max cos)` over `[lo, hi]`.
135fn cos_range(lo: Scalar, hi: Scalar) -> (Scalar, Scalar) {
136    let tau = core::f64::consts::TAU;
137    let max = if contains_lattice(lo, hi, 0.0, tau) {
138        1.0
139    } else {
140        lo.cos().max(hi.cos())
141    };
142    let min = if contains_lattice(lo, hi, core::f64::consts::PI, tau) {
143        -1.0
144    } else {
145        lo.cos().min(hi.cos())
146    };
147    (min, max)
148}
149
150fn ordered(a: Scalar, b: Scalar) -> (Scalar, Scalar) {
151    if a <= b {
152        (a, b)
153    } else {
154        (b, a)
155    }
156}
157
158/// `sup |c''|` of the conic `o + r_x cos t X + r_y sin t Y` over `[a, b]`.
159///
160/// `|c''|^2 = r_x^2 |X|^2 cos^2 + r_y^2 |Y|^2 sin^2 + 2 r_x r_y X.Y cos sin`;
161/// the first two terms are `beta^2 + (alpha^2 - beta^2) cos^2`, extreme
162/// where `cos^2` is, and the cross term is at most `r_x r_y |X.Y|`.
163fn conic_second(rx: Scalar, ry: Scalar, x: Vec3, y: Vec3, a: Scalar, b: Scalar) -> Scalar {
164    let (lo, hi) = ordered(a, b);
165    let alpha2 = (rx * rx) * x.length_squared();
166    let beta2 = (ry * ry) * y.length_squared();
167    let cos2 = if alpha2 >= beta2 {
168        max_cos2(lo, hi)
169    } else {
170        min_cos2(lo, hi)
171    };
172    let main = beta2 + (alpha2 - beta2) * cos2;
173    let cross = (rx * ry * x.dot(y)).abs();
174    (main.max(0.0) + cross).sqrt()
175}
176
177/// The exact sagitta bound of a circular arc of at most half a turn, when
178/// the frame is orthonormal; `None` otherwise.
179fn circle_sagitta(radius: Scalar, x: Vec3, y: Vec3, a: Scalar, b: Scalar) -> Option<Scalar> {
180    let h = (b - a).abs();
181    let orthonormal = (x.length_squared() - 1.0).abs() <= 1e-12
182        && (y.length_squared() - 1.0).abs() <= 1e-12
183        && x.dot(y).abs() <= 1e-12;
184    (orthonormal && h <= core::f64::consts::PI).then(|| radius.abs() * (1.0 - (0.5 * h).cos()))
185}
186
187fn lift2(v: Vec2) -> Vec3 {
188    Vec3::new(v.x, v.y, 0.0)
189}
190
191// --- B-spline derivative bounds ---------------------------------------------
192
193/// Suprema of the first three derivatives of a curve over an interval.
194///
195/// Each is the supremum over the open pieces between the curve's own
196/// breaks: at a knot where a derivative jumps, both one-sided values are
197/// covered. Whether a Taylor argument may cross such a knot is the
198/// caller's question; see [`continuity_breaks3`].
199#[derive(Debug, Clone, Copy, PartialEq)]
200pub struct CurveDerivativeBounds {
201    /// `sup |c'|`.
202    pub first: Scalar,
203    /// `sup |c''|`.
204    pub second: Scalar,
205    /// `sup |c'''|`.
206    pub third: Scalar,
207}
208
209/// Derivative control polygon of one homogeneous axis: `d (P_{i+1} - P_i) /
210/// (u_{i+d+1} - u_{i+1})`, the zero-width spans contributing nothing.
211fn hodograph<const N: usize>(
212    points: &[[Scalar; N]],
213    weights: &[Scalar],
214    knots: &[Scalar],
215    degree: usize,
216) -> (Vec<[Scalar; N]>, Vec<Scalar>) {
217    let mut out_points = Vec::with_capacity(points.len().saturating_sub(1));
218    let mut out_weights = Vec::with_capacity(weights.len().saturating_sub(1));
219    for i in 0..points.len().saturating_sub(1) {
220        let width = knots[i + degree + 1] - knots[i + 1];
221        let factor = if width > 0.0 {
222            degree as Scalar / width
223        } else {
224            0.0
225        };
226        out_points.push(core::array::from_fn(|k| {
227            (points[i + 1][k] - points[i][k]) * factor
228        }));
229        out_weights.push((weights[i + 1] - weights[i]) * factor);
230    }
231    (out_points, out_weights)
232}
233
234fn norm<const N: usize>(p: &[Scalar; N]) -> Scalar {
235    p.iter().map(|c| c * c).sum::<Scalar>().sqrt()
236}
237
238/// Rational derivative bounds from homogeneous ones: `A = w c`, so
239/// `c' = (A' - w' c) / w`, `c'' = (A'' - 2 w' c' - w'' c) / w` and
240/// `c''' = (A''' - 3 w'' c' - 3 w' c'' - w''' c) / w`.
241fn quotient_bounds(a: [Scalar; 4], w: [Scalar; 4], radius: Scalar, w_min: Scalar) -> [Scalar; 3] {
242    let first = (a[1] + w[1] * radius) / w_min;
243    let second = (a[2] + 2.0 * w[1] * first + w[2] * radius) / w_min;
244    let third = (a[3] + 3.0 * w[2] * first + 3.0 * w[1] * second + w[3] * radius) / w_min;
245    [first, second, third]
246}
247
248/// `sup |c^(k)|`, `k = 1, 2, 3`, of a B-spline over `[a, b]`, and its
249/// chord coefficient `K`: every point of the arc is within `(b - a)^2 / 8
250/// K` of the chord. `None` for invalid data or a non-positive weight.
251///
252/// The chord coefficient is not `sup |c''|` for a rational spline. With
253/// the centred homogeneous numerator `A = w c` and the linear
254/// interpolants `l_A`, `l_w` of `A` and `w` over `[a, b]`, the point
255/// `q = l_A / l_w` lies on the chord (a projective map keeps segments), and
256/// `c - q = ((A - l_A) - q (w - l_w)) / w`. Both differences are
257/// polynomial chord errors, so `|c - q| <= (b - a)^2 / 8 (|A''| + R
258/// |w''|) / w_min` with `R` bounding `|q|` (the active points' spread):
259/// no first-derivative terms, which the quotient rule for `c''` carries.
260fn bspline_bounds<P, const N: usize>(
261    curve: &BSplineCurve<P>,
262    to: impl Fn(&P) -> [Scalar; N],
263    a: Scalar,
264    b: Scalar,
265) -> Option<(CurveDerivativeBounds, Scalar)> {
266    let count = curve.control_points.len();
267    let axis = SplineAxis::new(
268        &curve.knots,
269        &curve.multiplicities,
270        curve.degree,
271        count,
272        "B-spline curve",
273    )
274    .ok()?;
275    let weights: Vec<Scalar> = match &curve.weights {
276        Some(w) if w.len() == count => w.clone(),
277        Some(_) => return None,
278        None => vec![1.0; count],
279    };
280    if weights.iter().any(|w| !(w.is_finite() && *w > 0.0)) {
281        return None;
282    }
283    let degree = axis.degree;
284    let (lo, hi) = ordered(axis.clamp(a), axis.clamp(b));
285    let first_span = span_in(&axis.knots, count, degree, lo);
286    let last_span = span_in(&axis.knots, count, degree, hi);
287    // Control points acting on the spans [first_span, last_span]; every
288    // hodograph's active points fall in the same index window.
289    let window = first_span - degree..=last_span;
290    let points: Vec<[Scalar; N]> = curve.control_points.iter().map(&to).collect();
291    if points.iter().flatten().any(|c| !c.is_finite()) {
292        return None;
293    }
294    // Centre the active points so `|c|` (which the rational quotient rule
295    // multiplies by weight derivatives) is their spread, not their offset.
296    let mut low = [Scalar::INFINITY; N];
297    let mut high = [Scalar::NEG_INFINITY; N];
298    for p in &points[window.clone()] {
299        for k in 0..N {
300            low[k] = low[k].min(p[k]);
301            high[k] = high[k].max(p[k]);
302        }
303    }
304    let centre: [Scalar; N] = core::array::from_fn(|k| 0.5 * (low[k] + high[k]));
305    let radius = points[window.clone()]
306        .iter()
307        .map(|p| norm(&core::array::from_fn::<Scalar, N, _>(|k| p[k] - centre[k])))
308        .fold(0.0, Scalar::max);
309    let w_min = weights[window.clone()]
310        .iter()
311        .copied()
312        .fold(Scalar::INFINITY, Scalar::min);
313    let mut hom: Vec<[Scalar; N]> = points
314        .iter()
315        .zip(&weights)
316        .map(|(p, w)| core::array::from_fn(|k| (p[k] - centre[k]) * w))
317        .collect();
318    let mut hw = weights.clone();
319    let mut knots: &[Scalar] = &axis.knots;
320    let mut a_sup = [0.0; 4];
321    let mut w_sup = [0.0; 4];
322    for order in 1..=3 {
323        let current_degree = degree + 1 - order;
324        if current_degree == 0 || hom.len() < 2 {
325            break;
326        }
327        let (next, next_w) = hodograph(&hom, &hw, knots, current_degree);
328        knots = &knots[1..knots.len() - 1];
329        let end = (*window.end()).min(next.len().saturating_sub(1));
330        let start = (*window.start()).min(end);
331        a_sup[order] = next[start..=end].iter().map(norm).fold(0.0, Scalar::max);
332        w_sup[order] = next_w[start..=end]
333            .iter()
334            .map(|w| w.abs())
335            .fold(0.0, Scalar::max);
336        hom = next;
337        hw = next_w;
338    }
339    let [first, second, third] = quotient_bounds(a_sup, w_sup, radius, w_min);
340    let bounds = CurveDerivativeBounds {
341        first: inflate(first),
342        second: inflate(second),
343        third: inflate(third),
344    };
345    let chord = inflate((a_sup[2] + radius * w_sup[2]) / w_min).min(bounds.second);
346    (bounds.first.is_finite()
347        && bounds.second.is_finite()
348        && bounds.third.is_finite()
349        && chord.is_finite())
350    .then_some((bounds, chord))
351}
352
353/// Interior knots of a B-spline where it may fail to be `C^k`: those of
354/// multiplicity above `degree - k`.
355fn bspline_breaks<P>(curve: &BSplineCurve<P>, k: usize) -> Vec<Scalar> {
356    let degree = usize::from(curve.degree);
357    let interior = curve.knots.len().saturating_sub(2);
358    curve
359        .knots
360        .iter()
361        .zip(&curve.multiplicities)
362        .skip(1)
363        .take(interior)
364        .filter(|(_, &m)| (m as usize) + k > degree)
365        .map(|(&knot, _)| knot)
366        .collect()
367}
368
369/// Parameters strictly inside the curve's domain where a 2D curve may fail
370/// to be `C^k` (`k >= 1`): a polyline's vertices, a B-spline's knots of
371/// multiplicity above `degree - k`, an arc-length chain's joins (where its
372/// curvature may jump; the chain is `C^1` there, but each piece is bounded
373/// on its own). Empty for the smooth families.
374#[must_use]
375pub fn continuity_breaks2(curve: &Curve2, k: usize) -> Vec<Scalar> {
376    match curve {
377        Curve2::BSpline(b) => bspline_breaks(b, k),
378        Curve2::Polyline(p) if k >= 1 => polyline_breaks(p.points.len(), p.closed),
379        // A chain is tangent-continuous at its joins by construction, but
380        // each piece is bounded on its own, so the joins are named for
381        // every `k`.
382        Curve2::Chain(c) if k >= 1 => c.joins().unwrap_or_default(),
383        // An implicit curve's cells are bounded one at a time (#249).
384        Curve2::Implicit(c) if k >= 1 => implicit::joins(c),
385        _ => Vec::new(),
386    }
387}
388
389/// [`continuity_breaks2`] for a 3D curve.
390#[must_use]
391pub fn continuity_breaks3(curve: &Curve3, k: usize) -> Vec<Scalar> {
392    match curve {
393        Curve3::BSpline(b) => bspline_breaks(b, k),
394        Curve3::Polyline(p) if k >= 1 => polyline_breaks(p.points.len(), p.closed),
395        // A chain plan's joins and the profile's seams; curvature seams of
396        // the plan for `k >= 2` (#252).
397        Curve3::Elevated(e) => crate::elevated::elevated_breaks(e, k),
398        Curve3::Banked(b) => crate::elevated::banked_breaks(b, k),
399        _ => Vec::new(),
400    }
401}
402
403fn polyline_breaks(count: usize, closed: bool) -> Vec<Scalar> {
404    let segments = if closed {
405        count
406    } else {
407        count.saturating_sub(1)
408    };
409    (1..segments).map(|k| k as Scalar).collect()
410}
411
412fn breaks_inside(breaks: &[Scalar], a: Scalar, b: Scalar) -> bool {
413    let (lo, hi) = ordered(a, b);
414    breaks.iter().any(|&t| t > lo && t < hi)
415}
416
417/// Certified derivative suprema of a 3D curve over `[a, b]`, or `None` for
418/// a family this module cannot bound.
419///
420/// Covered: lines, circles, ellipses and B-splines (rational ones too).
421#[must_use]
422pub fn curve_derivative_bounds3(
423    curve: &Curve3,
424    a: Scalar,
425    b: Scalar,
426) -> Option<CurveDerivativeBounds> {
427    if !(a.is_finite() && b.is_finite()) {
428        return None;
429    }
430    match curve {
431        Curve3::Line(line) => Some(CurveDerivativeBounds {
432            first: inflate(line.direction.length()),
433            second: 0.0,
434            third: 0.0,
435        }),
436        Curve3::Circle(c) => Some(conic_bounds(c.radius, c.radius, c.frame.x, c.frame.y, a, b)),
437        Curve3::Ellipse(e) => Some(conic_bounds(
438            e.semi_axis_x,
439            e.semi_axis_y,
440            e.frame.x,
441            e.frame.y,
442            a,
443            b,
444        )),
445        Curve3::BSpline(spline) => bspline_bounds(spline, |p| [p.x, p.y, p.z], a, b).map(|b| b.0),
446        // In plan distance, plan and profile combined (#252).
447        Curve3::Elevated(e) => crate::elevated::elevated_derivative_bounds(e, a, b),
448        Curve3::Banked(banked) => crate::elevated::banked_derivative_bounds(banked, a, b),
449        _ => None,
450    }
451}
452
453/// Certified derivative suprema of a 2D curve over `[a, b]`; see
454/// [`curve_derivative_bounds3`]. Also covers sinusoids.
455#[must_use]
456pub fn curve_derivative_bounds2(
457    curve: &Curve2,
458    a: Scalar,
459    b: Scalar,
460) -> Option<CurveDerivativeBounds> {
461    if !(a.is_finite() && b.is_finite()) {
462        return None;
463    }
464    match curve {
465        Curve2::Line(line) => Some(CurveDerivativeBounds {
466            first: inflate(line.direction.length()),
467            second: 0.0,
468            third: 0.0,
469        }),
470        Curve2::Circle(c) => Some(conic_bounds(
471            c.radius,
472            c.radius,
473            lift2(c.frame.x),
474            lift2(c.frame.y),
475            a,
476            b,
477        )),
478        Curve2::Ellipse(e) => Some(conic_bounds(
479            e.semi_axis_x,
480            e.semi_axis_y,
481            lift2(e.frame.x),
482            lift2(e.frame.y),
483            a,
484            b,
485        )),
486        Curve2::BSpline(spline) => bspline_bounds(spline, |p| [p.x, p.y], a, b).map(|b| b.0),
487        // `(t, mean + a cos t + b sin t)`: every derivative's wave has
488        // amplitude `sqrt(a^2 + b^2)`.
489        Curve2::Sinusoid(w) => {
490            let amplitude = w.cosine.hypot(w.sine);
491            Some(CurveDerivativeBounds {
492                first: inflate((1.0 + amplitude * amplitude).sqrt()),
493                second: inflate(amplitude),
494                third: inflate(amplitude),
495            })
496        }
497        _ => None,
498    }
499}
500
501/// A conic's derivatives cycle: `c' = -c'''` is `c''` a quarter turn on,
502/// so `|c'|^2 <= max(r_x^2 |X|^2, r_y^2 |Y|^2) + r_x r_y |X.Y|` bounds
503/// both; `c''` gets the interval's own tighter bound.
504fn conic_bounds(
505    rx: Scalar,
506    ry: Scalar,
507    x: Vec3,
508    y: Vec3,
509    a: Scalar,
510    b: Scalar,
511) -> CurveDerivativeBounds {
512    let speed = ((rx * rx * x.length_squared()).max(ry * ry * y.length_squared())
513        + (rx * ry * x.dot(y)).abs())
514    .sqrt();
515    CurveDerivativeBounds {
516        first: inflate(speed),
517        second: inflate(conic_second(rx, ry, x, y, a, b)),
518        third: inflate(speed),
519    }
520}
521
522/// Certified chord bound of a 2D curve over `[a, b]`.
523///
524/// An upper bound on the distance from any point of the curve between `a`
525/// and `b` to the chord joining `c(a)` and `c(b)`. `None` when the family
526/// has no derivative bound here, or when the curve may not be `C^1`
527/// strictly inside the interval (a polyline vertex, a B-spline knot of full
528/// multiplicity): split there first ([`continuity_breaks2`]).
529#[must_use]
530pub fn chord_bound2(curve: &Curve2, a: Scalar, b: Scalar) -> Option<Scalar> {
531    if !(a.is_finite() && b.is_finite()) {
532        return None;
533    }
534    if a == b {
535        return Some(0.0);
536    }
537    if breaks_inside(&continuity_breaks2(curve, 1), a, b) {
538        return None;
539    }
540    match curve {
541        Curve2::Line(_) | Curve2::Polyline(_) => Some(0.0),
542        Curve2::Circle(c) => Some(inflate(
543            circle_sagitta(c.radius, lift2(c.frame.x), lift2(c.frame.y), a, b).unwrap_or_else(
544                || {
545                    taylor(
546                        b - a,
547                        conic_second(c.radius, c.radius, lift2(c.frame.x), lift2(c.frame.y), a, b),
548                    )
549                },
550            ),
551        )),
552        Curve2::BSpline(spline) => bspline_bounds(spline, |p| [p.x, p.y], a, b)
553            .map(|(_, chord)| inflate(taylor(b - a, chord))),
554        Curve2::Chain(chain) => crate::chain::chain_chord_bound(chain, a, b),
555        Curve2::Implicit(c) => {
556            let (lo, hi) = ordered(a, b);
557            implicit::chord_bound(c, lo, hi).map(inflate)
558        }
559        _ => curve_derivative_bounds2(curve, a, b).map(|d| inflate(taylor(b - a, d.second))),
560    }
561}
562
563/// Certified chord bound of a 3D curve over `[a, b]`; see [`chord_bound2`].
564#[must_use]
565pub fn chord_bound3(curve: &Curve3, a: Scalar, b: Scalar) -> Option<Scalar> {
566    if !(a.is_finite() && b.is_finite()) {
567        return None;
568    }
569    if a == b {
570        return Some(0.0);
571    }
572    if breaks_inside(&continuity_breaks3(curve, 1), a, b) {
573        return None;
574    }
575    match curve {
576        Curve3::Line(_) | Curve3::Polyline(_) => Some(0.0),
577        Curve3::Circle(c) => Some(inflate(
578            circle_sagitta(c.radius, c.frame.x, c.frame.y, a, b).unwrap_or_else(|| {
579                taylor(
580                    b - a,
581                    conic_second(c.radius, c.radius, c.frame.x, c.frame.y, a, b),
582                )
583            }),
584        )),
585        Curve3::BSpline(spline) => bspline_bounds(spline, |p| [p.x, p.y, p.z], a, b)
586            .map(|(_, chord)| inflate(taylor(b - a, chord))),
587        // Plan and profile chord bounds at equal plan distance, composed
588        // in quadrature (#252; `crate::elevated`).
589        Curve3::Elevated(e) => crate::elevated::elevated_chord_bound(e, a, b),
590        Curve3::Banked(banked) => crate::elevated::banked_chord_bound(banked, a, b),
591        _ => curve_derivative_bounds3(curve, a, b).map(|d| inflate(taylor(b - a, d.second))),
592    }
593}
594
595/// `h^2 / 8 * sup |c''|`.
596fn taylor(h: Scalar, second: Scalar) -> Scalar {
597    h * h * 0.125 * second
598}
599
600/// Whether [`crate::curve::flatten2`] certifies its chords for this family:
601/// every point of the curve then lies within the chord tolerance of the
602/// returned polyline (#232). Lines, polylines, circles, ellipses,
603/// sinusoids, B-splines with positive weights and arc-length chains whose
604/// pieces are all bounded ([`crate::chain::chain_certifies`]); other families are
605/// flattened on their midpoint sagitta alone, which is a measurement, not a
606/// bound.
607#[must_use]
608pub fn certifies_flattening2(curve: &Curve2) -> bool {
609    match curve {
610        Curve2::Line(_)
611        | Curve2::Polyline(_)
612        | Curve2::Circle(_)
613        | Curve2::Ellipse(_)
614        | Curve2::Sinusoid(_) => true,
615        Curve2::BSpline(b) => spline_weights_positive(b.weights.as_deref()),
616        Curve2::Chain(c) => crate::chain::chain_certifies(c),
617        _ => false,
618    }
619}
620
621/// [`certifies_flattening2`] for [`crate::curve::flatten3`].
622#[must_use]
623pub fn certifies_flattening3(curve: &Curve3) -> bool {
624    match curve {
625        Curve3::Line(_) | Curve3::Polyline(_) | Curve3::Circle(_) | Curve3::Ellipse(_) => true,
626        Curve3::BSpline(b) => spline_weights_positive(b.weights.as_deref()),
627        // Certified where both halves always bound: a certified plan and
628        // a profile of closed-form pieces (#252). An intrinsic profile's
629        // bound rests on a below-vertical certificate that may run out of
630        // budget, and a banked curve's on its pivot forms: neither is
631        // claimed for every span.
632        Curve3::Elevated(e) => crate::elevated::certifies_elevated(e),
633        Curve3::Banked(_) => false,
634        _ => false,
635    }
636}
637
638fn spline_weights_positive(weights: Option<&[Scalar]>) -> bool {
639    weights.is_none_or(|w| w.iter().all(|w| w.is_finite() && *w > 0.0))
640}
641
642// --- surfaces ---------------------------------------------------------------
643
644/// Suprema of a surface's partial derivatives over a parameter box.
645///
646/// `du`, `dv` bound `|S_u|`, `|S_v|`; `duu`, `duv`, `dvv` bound the second
647/// partials. `smooth` is false when a B-spline knot line of full
648/// multiplicity crosses the box: the second-derivative bounds then hold on
649/// each side but not across, and a caller must fall back to a first-order
650/// argument there.
651#[derive(Debug, Clone, Copy, PartialEq)]
652pub struct SurfaceBounds {
653    /// `sup |S_u|`.
654    pub du: Scalar,
655    /// `sup |S_v|`.
656    pub dv: Scalar,
657    /// `sup |S_uu|`.
658    pub duu: Scalar,
659    /// `sup |S_uv|`.
660    pub duv: Scalar,
661    /// `sup |S_vv|`.
662    pub dvv: Scalar,
663    /// Whether the second-derivative bounds may be used across the box.
664    pub smooth: bool,
665    /// Coefficients `[A, B, C]` for linear interpolation over a parameter
666    /// triangle in the box: every surface point over the triangle is within
667    /// `1/2 sum_i l_i q(y_i - x)`, `q(d) = A d_u^2 + 2 B |d_u d_v| + C
668    /// d_v^2`, of the triangle spanned by the corners' surface points.
669    ///
670    /// For a polynomial surface they are `duu, duv, dvv`. For a rational
671    /// B-spline `S = A / w` they are `(|A_uu| + R |w_uu|) / w_min` and so on
672    /// (centred numerator, `R` the active points' spread): the point
673    /// `l_A / l_w` of the homogeneous interpolants is a convex combination
674    /// of the corners' surface points, so it lies in their triangle, and
675    /// `S - l_A / l_w = ((A - l_A) - (l_A / l_w)(w - l_w)) / w` carries no
676    /// first-derivative terms, unlike the quotient rule for `S_uu`.
677    pub interpolation: [Scalar; 3],
678}
679
680impl SurfaceBounds {
681    fn inflated(self) -> Self {
682        Self {
683            du: inflate(self.du),
684            dv: inflate(self.dv),
685            duu: inflate(self.duu),
686            duv: inflate(self.duv),
687            dvv: inflate(self.dvv),
688            smooth: self.smooth,
689            interpolation: self.interpolation.map(inflate),
690        }
691    }
692
693    fn finite(self) -> Option<Self> {
694        [self.du, self.dv, self.duu, self.duv, self.dvv]
695            .iter()
696            .chain(&self.interpolation)
697            .all(|v| v.is_finite())
698            .then_some(self)
699    }
700}
701
702/// Derivative bounds of one surface, prepared once for many boxes.
703///
704/// A B-spline surface's derivative control nets are built on construction,
705/// so each [`Self::bounds`] call only scans the window of the box.
706#[derive(Debug, Clone)]
707pub struct SurfaceBoundOracle<'a> {
708    surface: &'a Surface,
709    spline: Option<SplineNets>,
710}
711
712/// Homogeneous derivative nets of a B-spline surface, centred.
713#[derive(Debug, Clone)]
714struct SplineNets {
715    u_knots: Vec<Scalar>,
716    v_knots: Vec<Scalar>,
717    p: usize,
718    q: usize,
719    rows: usize,
720    cols: usize,
721    points: Vec<Vec<[Scalar; 3]>>,
722    weights: Vec<Vec<Scalar>>,
723    u_breaks: Vec<Scalar>,
724    v_breaks: Vec<Scalar>,
725}
726
727impl<'a> SurfaceBoundOracle<'a> {
728    /// Prepare the bounds of `surface`, or `None` for a family (or data)
729    /// this module cannot bound.
730    #[must_use]
731    pub fn new(surface: &'a Surface) -> Option<Self> {
732        let spline = match surface {
733            Surface::Plane(_)
734            | Surface::Cylinder(_)
735            | Surface::EllipticalCylinder(_)
736            | Surface::Cone(_)
737            | Surface::Sphere(_)
738            | Surface::Torus(_) => None,
739            Surface::BSpline(b) => Some(SplineNets::new(b)?),
740            _ => return None,
741        };
742        Some(Self { surface, spline })
743    }
744
745    /// Bounds over the box `[u.0, u.1] x [v.0, v.1]`; `None` where the
746    /// surface is not defined (a cone past its apex) or the data is not
747    /// finite.
748    #[must_use]
749    pub fn bounds(&self, u: (Scalar, Scalar), v: (Scalar, Scalar)) -> Option<SurfaceBounds> {
750        let (u0, u1) = ordered(u.0, u.1);
751        let (v0, v1) = ordered(v.0, v.1);
752        if ![u0, u1, v0, v1].iter().all(|x| x.is_finite()) {
753            return None;
754        }
755        let raw = match self.surface {
756            Surface::Plane(p) => SurfaceBounds {
757                du: p.frame.x.length(),
758                dv: p.frame.y.length(),
759                duu: 0.0,
760                duv: 0.0,
761                dvv: 0.0,
762                smooth: true,
763                interpolation: [0.0; 3],
764            },
765            Surface::Cylinder(c) => {
766                let s = frame_stretch3(&c.frame);
767                SurfaceBounds {
768                    du: s * c.radius.abs(),
769                    dv: c.frame.z.length(),
770                    duu: s * c.radius.abs(),
771                    duv: 0.0,
772                    dvv: 0.0,
773                    smooth: true,
774                    interpolation: [0.0; 3],
775                }
776            }
777            Surface::EllipticalCylinder(c) => {
778                let s = frame_stretch3(&c.frame);
779                let r = c.semi_axis_x.abs().max(c.semi_axis_y.abs());
780                SurfaceBounds {
781                    du: s * r,
782                    dv: c.frame.z.length(),
783                    duu: s * r,
784                    duv: 0.0,
785                    dvv: 0.0,
786                    smooth: true,
787                    interpolation: [0.0; 3],
788                }
789            }
790            Surface::Cone(c) => {
791                let s = frame_stretch3(&c.frame);
792                let slope = c.semi_angle.tan();
793                let (r0, r1) = (c.radius + v0 * slope, c.radius + v1 * slope);
794                if r0 < 0.0 || r1 < 0.0 {
795                    return None;
796                }
797                let rho = r0.max(r1);
798                SurfaceBounds {
799                    du: s * rho,
800                    dv: s * (1.0 + slope * slope).sqrt(),
801                    duu: s * rho,
802                    duv: s * slope.abs(),
803                    dvv: 0.0,
804                    smooth: true,
805                    interpolation: [0.0; 3],
806                }
807            }
808            Surface::Sphere(sphere) => {
809                let s = frame_stretch3(&sphere.frame) * sphere.radius.abs();
810                let cos = max_abs_cos(v0, v1);
811                SurfaceBounds {
812                    du: s * cos,
813                    dv: s,
814                    duu: s * cos,
815                    duv: s * max_abs_sin(v0, v1),
816                    dvv: s,
817                    smooth: true,
818                    interpolation: [0.0; 3],
819                }
820            }
821            Surface::Torus(t) => {
822                let s = frame_stretch3(&t.frame);
823                let (cmin, cmax) = cos_range(v0, v1);
824                let r = t.minor_radius.abs();
825                let ring = (t.major_radius + r * cmin)
826                    .abs()
827                    .max((t.major_radius + r * cmax).abs());
828                SurfaceBounds {
829                    du: s * ring,
830                    dv: s * r,
831                    duu: s * ring,
832                    duv: s * r * max_abs_sin(v0, v1),
833                    dvv: s * r,
834                    smooth: true,
835                    interpolation: [0.0; 3],
836                }
837            }
838            Surface::BSpline(_) => self.spline.as_ref()?.bounds(u0, u1, v0, v1)?,
839            _ => return None,
840        };
841        let raw = if matches!(self.surface, Surface::BSpline(_)) {
842            raw
843        } else {
844            SurfaceBounds {
845                interpolation: [raw.duu, raw.duv, raw.dvv],
846                ..raw
847            }
848        };
849        raw.inflated().finite()
850    }
851}
852
853impl SplineNets {
854    fn new(b: &BSplineSurface) -> Option<Self> {
855        let rows = b.control_points.len();
856        let cols = b.control_points.first()?.len();
857        if b.control_points.iter().any(|row| row.len() != cols) {
858            return None;
859        }
860        let ua = SplineAxis::new(&b.u_knots, &b.u_multiplicities, b.u_degree, rows, "u").ok()?;
861        let va = SplineAxis::new(&b.v_knots, &b.v_multiplicities, b.v_degree, cols, "v").ok()?;
862        let weights: Vec<Vec<Scalar>> = match &b.weights {
863            Some(net) => {
864                if net.len() != rows || net.iter().any(|row| row.len() != cols) {
865                    return None;
866                }
867                net.clone()
868            }
869            None => vec![vec![1.0; cols]; rows],
870        };
871        if weights
872            .iter()
873            .flatten()
874            .any(|w| !(w.is_finite() && *w > 0.0))
875        {
876            return None;
877        }
878        let points: Vec<Vec<[Scalar; 3]>> = b
879            .control_points
880            .iter()
881            .map(|row| row.iter().map(|p| [p.x, p.y, p.z]).collect())
882            .collect();
883        if points.iter().flatten().flatten().any(|c| !c.is_finite()) {
884            return None;
885        }
886        let breaks = |knots: &[Scalar], mults: &[u32], degree: u16| {
887            let interior = knots.len().saturating_sub(2);
888            knots
889                .iter()
890                .zip(mults)
891                .skip(1)
892                .take(interior)
893                .filter(|(_, &m)| m as usize >= usize::from(degree))
894                .map(|(&k, _)| k)
895                .collect::<Vec<_>>()
896        };
897        Some(Self {
898            u_breaks: breaks(&b.u_knots, &b.u_multiplicities, b.u_degree),
899            v_breaks: breaks(&b.v_knots, &b.v_multiplicities, b.v_degree),
900            u_knots: ua.knots,
901            v_knots: va.knots,
902            p: ua.degree,
903            q: va.degree,
904            rows,
905            cols,
906            points,
907            weights,
908        })
909    }
910
911    /// Bounds over a box: centre the active net, take its derivative nets
912    /// in the window and apply the quotient rule.
913    #[allow(clippy::needless_range_loop)]
914    fn bounds(&self, u0: Scalar, u1: Scalar, v0: Scalar, v1: Scalar) -> Option<SurfaceBounds> {
915        let clamp = |x: Scalar, knots: &[Scalar], d: usize, n: usize| x.clamp(knots[d], knots[n]);
916        let (cu0, cu1) = (
917            clamp(u0, &self.u_knots, self.p, self.rows),
918            clamp(u1, &self.u_knots, self.p, self.rows),
919        );
920        let (cv0, cv1) = (
921            clamp(v0, &self.v_knots, self.q, self.cols),
922            clamp(v1, &self.v_knots, self.q, self.cols),
923        );
924        let su0 = span_in(&self.u_knots, self.rows, self.p, cu0);
925        let su1 = span_in(&self.u_knots, self.rows, self.p, cu1);
926        let sv0 = span_in(&self.v_knots, self.cols, self.q, cv0);
927        let sv1 = span_in(&self.v_knots, self.cols, self.q, cv1);
928        let (r0, r1) = (su0 - self.p, su1);
929        let (c0, c1) = (sv0 - self.q, sv1);
930
931        // Centre the active points.
932        let mut low = [Scalar::INFINITY; 3];
933        let mut high = [Scalar::NEG_INFINITY; 3];
934        for row in &self.points[r0..=r1] {
935            for p in &row[c0..=c1] {
936                for k in 0..3 {
937                    low[k] = low[k].min(p[k]);
938                    high[k] = high[k].max(p[k]);
939                }
940            }
941        }
942        let centre: [Scalar; 3] = core::array::from_fn(|k| 0.5 * (low[k] + high[k]));
943        let mut radius: Scalar = 0.0;
944        let mut w_min = Scalar::INFINITY;
945        // The window plus the rows and columns a second hodograph reads.
946        let (wr0, wr1) = (r0, r1.min(self.rows - 1));
947        let (wc0, wc1) = (c0, c1.min(self.cols - 1));
948        let mut hom: Vec<Vec<[Scalar; 4]>> = Vec::with_capacity(wr1 + 1 - wr0);
949        for i in wr0..=wr1 {
950            let mut row = Vec::with_capacity(wc1 + 1 - wc0);
951            for j in wc0..=wc1 {
952                let p = self.points[i][j];
953                let w = self.weights[i][j];
954                let d: [Scalar; 3] = core::array::from_fn(|k| p[k] - centre[k]);
955                radius = radius.max(norm(&d));
956                w_min = w_min.min(w);
957                row.push([d[0] * w, d[1] * w, d[2] * w, w]);
958            }
959            hom.push(row);
960        }
961        // Derivatives along u act on rows, along v on columns; the knot
962        // slices are offset by the window start.
963        let du_net =
964            |net: &Vec<Vec<[Scalar; 4]>>, knots: &[Scalar], degree: usize, offset: usize| {
965                let mut out = Vec::new();
966                for i in 0..net.len().saturating_sub(1) {
967                    let width = knots[offset + i + degree + 1] - knots[offset + i + 1];
968                    let f = if width > 0.0 {
969                        degree as Scalar / width
970                    } else {
971                        0.0
972                    };
973                    out.push(
974                        net[i]
975                            .iter()
976                            .zip(&net[i + 1])
977                            .map(|(a, b)| core::array::from_fn(|k| (b[k] - a[k]) * f))
978                            .collect::<Vec<[Scalar; 4]>>(),
979                    );
980                }
981                out
982            };
983        let dv_net =
984            |net: &Vec<Vec<[Scalar; 4]>>, knots: &[Scalar], degree: usize, offset: usize| {
985                net.iter()
986                    .map(|row| {
987                        (0..row.len().saturating_sub(1))
988                            .map(|j| {
989                                let width = knots[offset + j + degree + 1] - knots[offset + j + 1];
990                                let f = if width > 0.0 {
991                                    degree as Scalar / width
992                                } else {
993                                    0.0
994                                };
995                                core::array::from_fn(|k| (row[j + 1][k] - row[j][k]) * f)
996                            })
997                            .collect::<Vec<[Scalar; 4]>>()
998                    })
999                    .collect::<Vec<_>>()
1000            };
1001        let sup = |net: &Vec<Vec<[Scalar; 4]>>| {
1002            let mut a: Scalar = 0.0;
1003            let mut w: Scalar = 0.0;
1004            for p in net.iter().flatten() {
1005                a = a.max((p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt());
1006                w = w.max(p[3].abs());
1007            }
1008            (a, w)
1009        };
1010        // The k-th u-hodograph uses the knots of the (k-1)-th trimmed at
1011        // both ends; in full-vector indexing its knot offset grows by one.
1012        let (p, q) = (self.p, self.q);
1013        let nu = if p >= 1 {
1014            du_net(&hom, &self.u_knots, p, wr0)
1015        } else {
1016            Vec::new()
1017        };
1018        let nuu = if p >= 2 {
1019            du_net(&nu, &self.u_knots, p - 1, wr0 + 1)
1020        } else {
1021            Vec::new()
1022        };
1023        let nv = if q >= 1 {
1024            dv_net(&hom, &self.v_knots, q, wc0)
1025        } else {
1026            Vec::new()
1027        };
1028        let nvv = if q >= 2 {
1029            dv_net(&nv, &self.v_knots, q - 1, wc0 + 1)
1030        } else {
1031            Vec::new()
1032        };
1033        let nuv = if p >= 1 && q >= 1 {
1034            dv_net(&nu, &self.v_knots, q, wc0)
1035        } else {
1036            Vec::new()
1037        };
1038        let (au, wu) = sup(&nu);
1039        let (av, wv) = sup(&nv);
1040        let (auu, wuu) = sup(&nuu);
1041        let (avv, wvv) = sup(&nvv);
1042        let (auv, wuv) = sup(&nuv);
1043        if !(w_min.is_finite() && w_min > 0.0) {
1044            return None;
1045        }
1046        // S = A / w: S_u = (A_u - w_u S) / w, S_uu = (A_uu - 2 w_u S_u -
1047        // w_uu S) / w, S_uv = (A_uv - w_u S_v - w_v S_u - w_uv S) / w.
1048        let su = (au + wu * radius) / w_min;
1049        let sv = (av + wv * radius) / w_min;
1050        let suu = (auu + 2.0 * wu * su + wuu * radius) / w_min;
1051        let svv = (avv + 2.0 * wv * sv + wvv * radius) / w_min;
1052        let suv = (auv + wu * sv + wv * su + wuv * radius) / w_min;
1053        // Evaluation clamps to the domain, which kinks the surface at its
1054        // edge, so a box reaching past it is not smooth either.
1055        let inside = cu0 == u0 && cu1 == u1 && cv0 == v0 && cv1 == v1;
1056        let smooth = inside
1057            && !breaks_inside(&self.u_breaks, cu0, cu1)
1058            && !breaks_inside(&self.v_breaks, cv0, cv1);
1059        Some(SurfaceBounds {
1060            du: su,
1061            dv: sv,
1062            duu: suu,
1063            duv: suv,
1064            dvv: svv,
1065            smooth,
1066            interpolation: [
1067                (auu + radius * wuu) / w_min,
1068                (auv + radius * wuv) / w_min,
1069                (avv + radius * wvv) / w_min,
1070            ],
1071        })
1072    }
1073}
1074
1075#[cfg(test)]
1076mod tests;