Skip to main content

axiolid_evaluate/station/
certify.rs

1//! Certified lengths of station pieces, and so certified joint and seam
2//! distances (#294, ADR 0082 amendment).
3//!
4//! A composite basis places its joints at the running sum of its pieces'
5//! lengths ([`super::CompositeBasis`]). Most of those lengths are read from
6//! stored data -- a line's, a circle's, a polyline's, an intrinsic curve's,
7//! a chain's, an elevated or banked curve's plan distance, a constant
8//! offset of a line or of a circle -- and are exact, rounding aside. The
9//! others are quadratures, estimates to [`super::OFFSET_TOLERANCE`] or the
10//! arc-length tolerance: an ellipse, a B-spline, and every other offset.
11//! This module bounds those from both sides, so that a joint's distance is
12//! a [`DistanceInterval`] that CONTAINS the true sum of the lengths before
13//! it, measured between the points the kernel reads as each piece's ends.
14//!
15//! # Chords and their deficit
16//!
17//! Where the measured curve `P(t)` is evaluated in closed form at its
18//! native parameter -- a line, a circle, an ellipse or a B-spline, and any
19//! offset of one, 2D or 3D, measured by arc length -- each panel `[a, b]`,
20//! `h = b - a`, is bounded by its chord `c = |P(b) - P(a)|` and certified
21//! bounds `M2`, `M3` on `|P''|`, `|P'''|` there:
22//!
23//! - `c` is a lower bound of the arc length (a chord is never longer);
24//! - `|P'(v)| >= m = |P'(mid)| - M2 h / 2` everywhere on the panel, so the
25//!   tangent turns by at most `beta = h M2 / (2 m)` from the midpoint's and
26//!   the arc length is at most `c / cos(beta)`;
27//! - the deficit `L - c` is bounded from both sides through the velocity
28//!   across the chord, a zero-mean function whose square integral `M2`
29//!   bounds as `h^3`, and `M3` with the panel's own velocities to within
30//!   `h^4` (#307; see the `panels` module), so a panel's width falls as `h^4`.
31//!
32//! The panels are refined adaptively: the widest is halved first, until
33//! their widths sum to an eighth of the tolerance (`panels::adapt`), so
34//! an ordinary curve certifies to `1e-8` relative and below within the
35//! budget. `M2` and `M3` come from the certified derivative suprema of
36//! [`crate::bound`] (`|B''|` to `|B''''|` of the base) and, for an offset,
37//! the frame's: a unit vector `w / |w|` has `|u'| <= |w'| / r`, `|u''| <=
38//! |w''| / r + 3 |w'|^2 / r^2` and `|u'''| <= |w'''| / r + 9 |w'| |w''| /
39//! r^2 + 15 |w'|^3 / r^3` where `r` bounds `|w|` from below (`|B'|` for the
40//! tangent, `|B'_xy|` for the reference-up lateral, `|V x B'|` for a 3D
41//! offset's direction), and Leibniz for each law component times its axis.
42//! Each point carries a rounding error of [`POINT_ROUNDING`] relative to
43//! its largest coordinate, which widens the chain of chords.
44//!
45//! An offset by distances along an ellipse or a B-spline (#307) reads its
46//! law in the base's arc length, a quadrature: the base is measured by
47//! chords first, and the law at a native parameter is read through that
48//! certified length (see the `beside` module), the measured curve being the offset
49//! whose law runs linearly in the base's true arc length between the
50//! points the kernel reads as the piece's ends.
51//!
52//! # Midpoints of a speed in closed form
53//!
54//! Where the base is parameterised by its own arc length in the plan and
55//! its curvature is a law -- an intrinsic curve or a chain (2D), a 3D
56//! intrinsic curve with no torsion in a level plane (#302), the plan of an
57//! elevated curve (measured by plan length) -- the offset's speed is
58//! a closed form in the base's curvature `k` and, in a section frame that
59//! leans with the grade, the grade `p = z'`: for a displacement `a` along
60//! the left normal, `b` along the plan tangent and `c` vertical, all
61//! linear in the base distance `v`,
62//!
63//! `f = sqrt((1 + b' - a k)^2 + (a' + b k)^2 + c'^2)`
64//!
65//! (`c'` only where the offset is measured in 3D, beside a 2D base or a
66//! level 3D intrinsic curve), with
67//! `b = longitudinal * cos g - vertical * sin g`, `tan g = p`, in a
68//! section frame beside an elevated curve and `b = longitudinal`
69//! otherwise. Each panel is the midpoint rule, `h f(mid)`, within `h^3 /
70//! 24 sup |f''|`; `sup |f''|` is assembled from suprema of `k`, `k'`,
71//! `k''` (the law and its derivatives) and of `z'` to `z''''` (the
72//! profile's) by the Leibniz and chain rules, with `f >= inf (1 + b' -
73//! a k) > 0` (a cusp is refused). Its panels are refined adaptively too.
74//!
75//! # Refusals
76//!
77//! By a typed [`GeomError::UnsupportedInput`] naming
78//! [`UNCERTIFIED_LENGTH`]: an offset of a banked curve, of a 3D intrinsic
79//! curve with a torsion law not identically zero or out of a level plane
80//! (a start tangent or normal with a `z` component), of a chain's
81//! parametric piece or of an elevated curve with an
82//! intrinsic profile in a leaning frame; a 3D offset direction that is not
83//! vertical beside a plan-parameterised base; a base placed in a frame
84//! that tilts `+Z` there; a skewed frame; and a tolerance the rounding of
85//! the points already exceeds. A tolerance the budget of
86//! [`MAX_CERTIFIED_PANELS`] panels does not reach is
87//! [`GeomError::BudgetExceeded`].
88
89use std::borrow::Cow;
90
91use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
92use axiolid_core::{Frame2, Point2, Point3, Scalar, Vec2, Vec3};
93use axiolid_curve::{
94    ChainPiece2, CurvatureLaw, Curve2, Curve3, ElevationLaw, Intrinsic3, Line2, Line3, OffsetFrame,
95    OffsetLaw,
96};
97pub use axiolid_curve_evaluate_contract::DistanceInterval;
98
99use super::composite::{StationCurve, StationPiece};
100use super::offset::{OffsetTable, StationOffset};
101use super::{station_length2, station_length3, StationSeam, KEEPS_UP_TOLERANCE};
102use crate::arc_parameter::{
103    parameter_at_arc_length2, parameter_at_arc_length3, ARC_LENGTH_TOLERANCE,
104};
105use crate::bound::{
106    continuity_breaks2, continuity_breaks3, curve_derivative_bounds2, curve_derivative_bounds3,
107    curve_fourth_bound2, curve_fourth_bound3, CurveDerivativeBounds,
108};
109use crate::curve::{
110    derivative2, derivative3, domain2, domain3, evaluate2, evaluate3, second_derivative2,
111    second_derivative3,
112};
113use beside::chord_offset_length;
114use panels::{adapt, finish, product, Compensated, Jet, Measured, PANEL_SHARE, VELOCITY_ROUNDING};
115
116mod beside;
117mod intrinsic;
118mod panels;
119mod reading;
120
121pub(crate) use intrinsic::{reads_intrinsic, IntrinsicReader};
122pub(crate) use reading::{PieceReader, AXIS_ROUNDING};
123
124/// The input a length no certified bound here reaches is refused for.
125pub const UNCERTIFIED_LENGTH: &str =
126    "a certified length of a station piece no bound here reaches: an offset of a banked curve, a \
127     3D intrinsic curve with torsion or out of a level plane, a chain's parametric piece or an \
128     elevated curve with an intrinsic profile in a leaning frame; a 3D offset direction that is \
129     not vertical beside a plan-measured or intrinsic base; a base placed in a frame that tilts \
130     +Z; a skewed frame; or a tolerance below the points' rounding";
131
132/// The rounding error of a point evaluated in closed form, relative to the
133/// largest coordinate magnitude of the curve's data, the point and the
134/// displacement (at least 1): about 45 units in the last place, several
135/// times what a de Boor or conic evaluation and a frame accumulate.
136pub const POINT_ROUNDING: Scalar = 1e-14;
137
138/// The rounding error of a speed evaluated in closed form, relative.
139const VALUE_ROUNDING: Scalar = 1e-14;
140
141/// Most panels one certified length may use.
142pub const MAX_CERTIFIED_PANELS: usize = 1 << 16;
143
144/// Suprema of `|cos(atan p)|` and its first three derivatives in `p`.
145const COS_OF_ATAN: [Scalar; 4] = [1.0, 0.39, 1.0, 1.94];
146
147/// Suprema of `|sin(atan p)|` and its first three derivatives in `p`.
148const SIN_OF_ATAN: [Scalar; 4] = [1.0, 1.0, 0.86, 3.0];
149
150fn uncertified() -> GeomError {
151    GeomError::UnsupportedInput {
152        backend: BackendId::new("axiolid-evaluate"),
153        operation: Operation::CurveEvaluation,
154        input: UNCERTIFIED_LENGTH,
155    }
156}
157
158fn budget() -> GeomError {
159    GeomError::BudgetExceeded {
160        resource: "certified length panels",
161    }
162}
163
164fn slack(distance: Scalar) -> Scalar {
165    ARC_LENGTH_TOLERANCE * distance.abs().max(1.0)
166}
167
168/// A tolerance a certified length can be asked for: finite and positive.
169///
170/// # Errors
171///
172/// Any other, by name.
173pub(crate) fn checked_tolerance(tolerance: Scalar) -> GeomResult<Scalar> {
174    if tolerance.is_finite() && tolerance > 0.0 {
175        Ok(tolerance)
176    } else {
177        Err(GeomError::InvalidInput(format!(
178            "station: a certified distance needs a finite, positive tolerance, not {tolerance}"
179        )))
180    }
181}
182
183// --- sums ---------------------------------------------------------------------
184
185/// A running sum of lengths: exact while every term is, widened for the
186/// rounding of its additions once one is not.
187#[derive(Debug, Clone, Copy)]
188pub(crate) struct Running {
189    lower: Scalar,
190    upper: Scalar,
191    terms: usize,
192    exact: bool,
193}
194
195impl Running {
196    pub(crate) const ZERO: Self = Self {
197        lower: 0.0,
198        upper: 0.0,
199        terms: 0,
200        exact: true,
201    };
202
203    /// The sum with `term` added.
204    pub(crate) fn plus(self, term: DistanceInterval) -> Self {
205        Self {
206            lower: self.lower + term.lower,
207            upper: self.upper + term.upper,
208            terms: self.terms + 1,
209            exact: self.exact && term.is_point(),
210        }
211    }
212
213    /// The certified interval of the sum.
214    pub(crate) fn interval(self) -> DistanceInterval {
215        if self.exact {
216            return DistanceInterval::new(self.lower, self.upper);
217        }
218        let rounding = 2.0 * (self.terms as Scalar + 1.0) * Scalar::EPSILON;
219        DistanceInterval::new(
220            self.lower - rounding * self.lower.abs(),
221            self.upper + rounding * self.upper.abs(),
222        )
223    }
224}
225
226// --- piece lengths ------------------------------------------------------------
227
228/// The certified length of `piece` in its station measure: a point where
229/// it is read from stored data, else an interval no wider than
230/// `tolerance`; an offset read through `table` (built when `None`).
231///
232/// # Errors
233///
234/// The refusals of the [module documentation](self#refusals), and the
235/// evaluators' own.
236pub(crate) fn piece_length(
237    piece: &StationPiece<'_>,
238    table: Option<&OffsetTable>,
239    tolerance: Scalar,
240) -> GeomResult<DistanceInterval> {
241    match piece.curve {
242        StationCurve::Offset(offset) => {
243            if offset.measure_is_exact() {
244                return Ok(DistanceInterval::point(piece.length()));
245            }
246            let built;
247            let table = match table {
248                Some(table) => table,
249                None => {
250                    built = offset.table()?;
251                    &built
252                }
253            };
254            let a = offset.base_measure(piece.start, table)?;
255            let b = offset.base_measure(piece.end, table)?;
256            offset_length(&offset, a.min(b), a.max(b), tolerance)
257        }
258        curve if curve.measure_is_exact() => Ok(DistanceInterval::point(piece.length())),
259        curve => curve_span(curve, piece.start, piece.end, tolerance),
260    }
261}
262
263/// The certified arc length of `curve` (not an offset) between its station
264/// measures `m0 <= m1`: exact where its measure is read from stored data,
265/// else by chords between the native parameters the kernel reads them at.
266///
267/// # Errors
268///
269/// As [`piece_length`].
270pub(crate) fn curve_span(
271    curve: StationCurve<'_>,
272    m0: Scalar,
273    m1: Scalar,
274    tolerance: Scalar,
275) -> GeomResult<DistanceInterval> {
276    if curve.measure_is_exact() {
277        return Ok(DistanceInterval::point(m1 - m0));
278    }
279    let native = Native::of(curve, m0, m1)?;
280    let (t0, t1) = (native.parameter(m0)?, native.parameter(m1)?);
281    // `P''` continuous on every panel, for its third-derivative bound.
282    let mut breaks = native.breaks(2);
283    breaks.extend(native.breaks(1));
284    native.measured(|measured| measured.chord_turning(t0.min(t1), t0.max(t1), &breaks, tolerance))
285}
286
287/// The certified length of `offset` between its base measures `va <= vb`
288/// (into the base piece, in the direction of travel), in its own
289/// convention.
290fn offset_length(
291    offset: &StationOffset<'_>,
292    va: Scalar,
293    vb: Scalar,
294    tolerance: Scalar,
295) -> GeomResult<DistanceInterval> {
296    if vb <= va {
297        return Ok(DistanceInterval::point(0.0));
298    }
299    if let Some(model) = PlanModel::of(offset)? {
300        return model.length(va, vb, tolerance);
301    }
302    chord_offset_length(offset, va, vb, tolerance)
303}
304
305// --- closed-form points -------------------------------------------------------
306
307/// A base curve evaluated in closed form at its native parameter, 2D lifted
308/// to `z = 0`; a polyline's segment as a line in its measure.
309#[derive(Debug, Clone)]
310enum Native<'c> {
311    Two(Cow<'c, Curve2>),
312    Three(Cow<'c, Curve3>),
313}
314
315impl<'c> Native<'c> {
316    /// The closed-form curve under `curve` for the measures `[m0, m1]`.
317    fn of(curve: StationCurve<'c>, m0: Scalar, m1: Scalar) -> GeomResult<Self> {
318        let mid = 0.5 * (m0 + m1);
319        match curve {
320            StationCurve::Two(Curve2::Polyline(polyline)) => {
321                let (origin, unit) = segment(&polyline.points, polyline.closed, mid, |p| {
322                    Vec3::new(p.x, p.y, 0.0)
323                })?;
324                Ok(Self::Two(Cow::Owned(Curve2::Line(Line2 {
325                    origin: Point2::new(origin.x, origin.y),
326                    direction: Vec2::new(unit.x, unit.y),
327                }))))
328            }
329            StationCurve::Three(Curve3::Polyline(polyline)) => {
330                let (origin, unit) = segment(&polyline.points, polyline.closed, mid, |p| p)?;
331                Ok(Self::Three(Cow::Owned(Curve3::Line(Line3 {
332                    origin,
333                    direction: unit,
334                }))))
335            }
336            StationCurve::Two(
337                curve @ (Curve2::Line(_)
338                | Curve2::Circle(_)
339                | Curve2::Ellipse(_)
340                | Curve2::BSpline(_)),
341            ) => Ok(Self::Two(Cow::Borrowed(curve))),
342            StationCurve::Three(
343                curve @ (Curve3::Line(_)
344                | Curve3::Circle(_)
345                | Curve3::Ellipse(_)
346                | Curve3::BSpline(_)),
347            ) => Ok(Self::Three(Cow::Borrowed(curve))),
348            _ => Err(uncertified()),
349        }
350    }
351
352    fn point(&self, t: Scalar) -> GeomResult<Point3> {
353        match self {
354            Self::Two(curve) => {
355                let p = evaluate2(curve, t)?;
356                Ok(Point3::new(p.x, p.y, 0.0))
357            }
358            Self::Three(curve) => evaluate3(curve, t),
359        }
360    }
361
362    fn derivative(&self, t: Scalar) -> GeomResult<Vec3> {
363        match self {
364            Self::Two(curve) => {
365                let d = derivative2(curve, t)?;
366                Ok(Vec3::new(d.x, d.y, 0.0))
367            }
368            Self::Three(curve) => derivative3(curve, t),
369        }
370    }
371
372    fn second_derivative(&self, t: Scalar) -> GeomResult<Vec3> {
373        match self {
374            Self::Two(curve) => {
375                let d = second_derivative2(curve, t)?;
376                Ok(Vec3::new(d.x, d.y, 0.0))
377            }
378            Self::Three(curve) => second_derivative3(curve, t),
379        }
380    }
381
382    /// The largest coordinate magnitude of the curve's data: a B-spline's
383    /// control points, a conic's centre plus its axes, a line's origin.
384    /// What a point's rounding is relative to.
385    fn extent(&self) -> Scalar {
386        let flat = |v: Vec2| v.abs().max_element();
387        let out = match self {
388            Self::Two(curve) => match curve.as_ref() {
389                Curve2::Line(line) => flat(line.origin),
390                Curve2::Circle(c) => {
391                    flat(c.frame.origin) + c.radius * (flat(c.frame.x) + flat(c.frame.y))
392                }
393                Curve2::Ellipse(e) => {
394                    flat(e.frame.origin)
395                        + e.semi_axis_x * flat(e.frame.x)
396                        + e.semi_axis_y * flat(e.frame.y)
397                }
398                Curve2::BSpline(spline) => spline
399                    .control_points
400                    .iter()
401                    .map(|p| flat(*p))
402                    .fold(0.0, Scalar::max),
403                _ => Scalar::INFINITY,
404            },
405            Self::Three(curve) => match curve.as_ref() {
406                Curve3::Line(line) => line.origin.abs().max_element(),
407                Curve3::Circle(c) => {
408                    c.frame.origin.abs().max_element()
409                        + c.radius * (c.frame.x.abs().max_element() + c.frame.y.abs().max_element())
410                }
411                Curve3::Ellipse(e) => {
412                    e.frame.origin.abs().max_element()
413                        + e.semi_axis_x * e.frame.x.abs().max_element()
414                        + e.semi_axis_y * e.frame.y.abs().max_element()
415                }
416                Curve3::BSpline(spline) => spline
417                    .control_points
418                    .iter()
419                    .map(|p| p.abs().max_element())
420                    .fold(0.0, Scalar::max),
421                _ => Scalar::INFINITY,
422            },
423        };
424        out.max(1.0)
425    }
426
427    /// Certified suprema of `|B'|`, `|B''|`, `|B'''|` over `[a, b]`.
428    fn bounds(&self, a: Scalar, b: Scalar) -> GeomResult<CurveDerivativeBounds> {
429        match self {
430            Self::Two(curve) => curve_derivative_bounds2(curve, a, b),
431            Self::Three(curve) => curve_derivative_bounds3(curve, a, b),
432        }
433        .filter(|d| d.first.is_finite() && d.second.is_finite() && d.third.is_finite())
434        .ok_or_else(uncertified)
435    }
436
437    /// A certified supremum of `|B''''|` over `[a, b]`, where one is known.
438    fn fourth(&self, a: Scalar, b: Scalar) -> Option<Scalar> {
439        match self {
440            Self::Two(curve) => curve_fourth_bound2(curve, a, b),
441            Self::Three(curve) => curve_fourth_bound3(curve, a, b),
442        }
443        .filter(|d| d.is_finite())
444    }
445
446    /// `run` given the curve measured by chords: its points exact but for
447    /// rounding, `P''` and `P'''` bounded by its own suprema.
448    fn measured<R>(&self, run: impl FnOnce(&Measured<'_>) -> GeomResult<R>) -> GeomResult<R> {
449        let point = |t: Scalar| Ok((self.point(t)?, 0.0));
450        let velocity = |t: Scalar| {
451            let d = self.derivative(t)?;
452            Ok((d, VELOCITY_ROUNDING * d.length()))
453        };
454        let bounds = |a: Scalar, b: Scalar| {
455            let d = self.bounds(a, b)?;
456            Ok((d.second, Some(d.third)))
457        };
458        run(&Measured {
459            point: &point,
460            velocity: &velocity,
461            bounds: &bounds,
462            scale: self.extent(),
463        })
464    }
465
466    /// Where the curve may fail to be `C^k`.
467    fn breaks(&self, k: usize) -> Vec<Scalar> {
468        match self {
469            Self::Two(curve) => continuity_breaks2(curve, k),
470            Self::Three(curve) => continuity_breaks3(curve, k),
471        }
472    }
473
474    /// `dm/dt`, the station measure's rate in the native parameter, where
475    /// it is a constant: a line's `|d|`, a circle's radius.
476    fn rate(&self) -> Option<Scalar> {
477        match self {
478            Self::Two(curve) => match curve.as_ref() {
479                Curve2::Line(line) => Some(line.direction.length()),
480                Curve2::Circle(circle) => Some(circle.radius),
481                _ => None,
482            },
483            Self::Three(curve) => match curve.as_ref() {
484                Curve3::Line(line) => Some(line.direction.length()),
485                Curve3::Circle(circle) => Some(circle.radius),
486                _ => None,
487            },
488        }
489    }
490
491    /// The native parameter the kernel reads the station measure `m` at:
492    /// `m / rate` on a line or a circle; on an ellipse or a B-spline, the
493    /// curve's start, end and seams (corner knots) exactly where `m` is
494    /// within the arc-length tolerance of them, the arc-length inverse
495    /// elsewhere (an ellipse's measure may run past one turn, into the
496    /// next).
497    fn parameter(&self, m: Scalar) -> GeomResult<Scalar> {
498        if let Some(rate) = self.rate() {
499            return Ok(m / rate);
500        }
501        let (length, lo, hi, seams) = match self {
502            Self::Two(curve) => {
503                let domain = domain2(curve);
504                (
505                    station_length2(curve)?,
506                    domain.start.min(domain.end),
507                    domain.start.max(domain.end),
508                    super::station_seams2(curve)?,
509                )
510            }
511            Self::Three(curve) => {
512                let domain = domain3(curve);
513                (
514                    station_length3(curve)?,
515                    domain.start.min(domain.end),
516                    domain.start.max(domain.end),
517                    super::station_seams3(curve)?,
518                )
519            }
520        };
521        let length = length.ok_or_else(uncertified)?;
522        let closed = match self {
523            Self::Two(curve) => matches!(curve.as_ref(), Curve2::Ellipse(_)),
524            Self::Three(curve) => matches!(curve.as_ref(), Curve3::Ellipse(_)),
525        };
526        let (turn, m) = if closed && m > length + slack(length) {
527            (hi - lo, m - length)
528        } else {
529            (0.0, m)
530        };
531        let t = if m.abs() <= slack(0.0) {
532            lo
533        } else if (m - length).abs() <= slack(length) {
534            hi
535        } else if let Some(seam) = seams
536            .iter()
537            .find(|seam| (seam.distance - m).abs() <= slack(m))
538        {
539            seam.parameter
540        } else {
541            match self {
542                Self::Two(curve) => parameter_at_arc_length2(curve, lo, m)?,
543                Self::Three(curve) => parameter_at_arc_length3(curve, lo, m)?,
544            }
545        };
546        Ok(turn + t)
547    }
548}
549
550/// The native parameter the kernel reads the station measure `m` at on a
551/// closed-form curve (a line, a circle, an ellipse, a B-spline): the one
552/// the certified lengths are measured between.
553pub(crate) fn read_parameter(curve: StationCurve<'_>, m: Scalar) -> GeomResult<Scalar> {
554    Native::of(curve, m, m)?.parameter(m)
555}
556
557/// The certified arc length of a closed-form 2D curve (a line, a circle, an
558/// ellipse, a B-spline) from a start parameter, by chord panels (#438): an
559/// interval for `lambda(t)` at every parameter, so a chain can enclose the
560/// parameter its arc-length inverse reads instead of trusting the
561/// evaluator's.
562#[derive(Debug, Clone)]
563pub(crate) struct CurveLengths {
564    native: Native<'static>,
565    table: beside::BaseTable,
566}
567
568impl CurveLengths {
569    /// The lengths of `curve` over the parameters `[t0, t1]`, each panel
570    /// refined until the panels' widths sum to `target`.
571    ///
572    /// # Errors
573    ///
574    /// A curve of another family, and the certificate's own refusals.
575    pub(crate) fn new(curve: &Curve2, t0: Scalar, t1: Scalar, target: Scalar) -> GeomResult<Self> {
576        if !matches!(
577            curve,
578            Curve2::Line(_) | Curve2::Circle(_) | Curve2::Ellipse(_) | Curve2::BSpline(_)
579        ) {
580            return Err(uncertified());
581        }
582        let native = Native::Two(Cow::Owned(curve.clone()));
583        let mut breaks = native.breaks(2);
584        breaks.extend(native.breaks(1));
585        let table = native.measured(|m| beside::BaseTable::build(m, t0, t1, &breaks, target))?;
586        Ok(Self { native, table })
587    }
588
589    /// Bounds of the arc length from the start parameter to `t`.
590    ///
591    /// # Errors
592    ///
593    /// The evaluators' own.
594    pub(crate) fn at(&self, t: Scalar) -> GeomResult<(Scalar, Scalar)> {
595        self.native.measured(|m| self.table.at(m, t))
596    }
597
598    /// Bounds of the whole length over the table's parameters.
599    pub(crate) fn total(&self) -> (Scalar, Scalar) {
600        self.table.total()
601    }
602
603    /// The parameter spans of the panels, in order.
604    pub(crate) fn panels(&self) -> Vec<(Scalar, Scalar)> {
605        self.table.spans()
606    }
607
608    /// The coordinate magnitude a point's rounding is relative to.
609    pub(crate) fn extent(&self) -> Scalar {
610        self.native.extent()
611    }
612}
613
614/// A polyline's segment holding the measure `mid`, as the origin and unit
615/// direction of the line whose own measure is the polyline's there.
616fn segment<P: Copy>(
617    points: &[P],
618    closed: bool,
619    mid: Scalar,
620    lift: impl Fn(P) -> Vec3,
621) -> GeomResult<(Point3, Vec3)> {
622    let count = points.len();
623    let spans = if closed {
624        count
625    } else {
626        count.saturating_sub(1)
627    };
628    let mut run = 0.0;
629    let mut found = None;
630    for i in 0..spans {
631        let (a, b) = (lift(points[i]), lift(points[(i + 1) % count]));
632        let length = (b - a).length();
633        if length > 0.0 {
634            found = Some((a, (b - a) / length, run));
635            if mid <= run + length {
636                break;
637            }
638        }
639        run += length;
640    }
641    let (start, unit, at) = found.ok_or_else(uncertified)?;
642    Ok((start - unit * at, unit))
643}
644
645// --- speeds in closed form ------------------------------------------------------
646
647fn sum<const N: usize>(f: Jet<N>, g: Jet<N>) -> Jet<N> {
648    core::array::from_fn(|k| f[k] + g[k])
649}
650
651/// Faa di Bruno to the third order: suprema of `(outer o p)^(k)` from the
652/// suprema `outer` of the outer function's derivatives and `p` of the
653/// inner's.
654fn composed(outer: Jet<4>, p: Jet<4>) -> Jet<4> {
655    [
656        outer[0],
657        outer[1] * p[1],
658        outer[2] * p[1] * p[1] + outer[1] * p[2],
659        outer[3] * p[1].powi(3) + 3.0 * outer[2] * p[1] * p[2] + outer[1] * p[3],
660    ]
661}
662
663/// A displacement component linear in the base distance over `[0,
664/// length]`: its suprema `[max(|a|, |b|), |b - a| / length, 0, 0]`.
665fn linear(pair: (Scalar, Scalar), length: Scalar) -> Jet<4> {
666    [
667        pair.0.abs().max(pair.1.abs()),
668        (pair.1 - pair.0).abs() / length,
669        0.0,
670        0.0,
671    ]
672}
673
674/// Where a plan-parameterised base's curvature comes from.
675#[derive(Debug, Clone, Copy)]
676enum Curvature<'a> {
677    /// The same everywhere: a line (zero) or a circle (`+-1 / r`).
678    Constant(Scalar),
679    /// `sign * law(s - shift)`: an intrinsic curve or a chain's intrinsic
680    /// piece, `sign` the start frame's handedness.
681    Law {
682        law: &'a CurvatureLaw,
683        shift: Scalar,
684        sign: Scalar,
685    },
686}
687
688impl Curvature<'_> {
689    fn value(&self, s: Scalar) -> GeomResult<Scalar> {
690        match self {
691            Self::Constant(k) => Ok(*k),
692            Self::Law { law, shift, sign } => crate::frenet::law_value(law, s - shift)
693                .map(|k| sign * k)
694                .ok_or_else(uncertified),
695        }
696    }
697
698    /// Suprema of `|k|`, `|k'|`, `|k''|` over the base measures `[lo, hi]`.
699    fn jet(&self, lo: Scalar, hi: Scalar) -> GeomResult<Jet<3>> {
700        match self {
701            Self::Constant(k) => Ok([k.abs(), 0.0, 0.0]),
702            Self::Law { law, shift, .. } => {
703                let (lo, hi) = (lo - shift, hi - shift);
704                let first = law.derivative();
705                let second = first.derivative();
706                let bound = |law: &CurvatureLaw| {
707                    crate::chain::curvature_bound(law, lo, hi)
708                        .filter(|b| b.is_finite())
709                        .ok_or_else(uncertified)
710                };
711                Ok([bound(law)?, bound(&first)?, bound(&second)?])
712            }
713        }
714    }
715}
716
717/// The handedness of an orthonormal 2D frame, `+1` or `-1`.
718///
719/// # Errors
720///
721/// A frame that is not orthonormal (its measure would not be its curve's
722/// arc length), as uncertified.
723fn handedness(frame: &Frame2) -> GeomResult<Scalar> {
724    let skew = (frame.x.length() - 1.0)
725        .abs()
726        .max((frame.y.length() - 1.0).abs())
727        .max(frame.x.dot(frame.y).abs());
728    if skew.is_nan() || skew > 1e-12 {
729        return Err(uncertified());
730    }
731    Ok(frame.x.perp_dot(frame.y).signum())
732}
733
734/// The curvature of a plan, parameterised by its arc length, over the
735/// measures `[lo, hi]` (inside one piece of it).
736fn plan_curvature(plan: &Curve2, lo: Scalar, hi: Scalar) -> GeomResult<Curvature<'_>> {
737    match plan {
738        Curve2::Line(_) | Curve2::Polyline(_) => Ok(Curvature::Constant(0.0)),
739        Curve2::Circle(circle) => {
740            let sign = handedness(&circle.frame)?;
741            if !(circle.radius.is_finite() && circle.radius > 0.0) {
742                return Err(uncertified());
743            }
744            Ok(Curvature::Constant(sign / circle.radius))
745        }
746        Curve2::Intrinsic(intrinsic) => Ok(Curvature::Law {
747            law: &intrinsic.curvature,
748            shift: 0.0,
749            sign: handedness(&intrinsic.start)?,
750        }),
751        Curve2::Chain(chain) => {
752            let sign = handedness(&chain.start)?;
753            let mid = 0.5 * (lo + hi);
754            match chain.piece_at(mid) {
755                Some((_, ChainPiece2::Intrinsic { curvature, .. }, local)) => Ok(Curvature::Law {
756                    law: curvature,
757                    shift: mid - local,
758                    sign,
759                }),
760                _ => Err(uncertified()),
761            }
762        }
763        _ => Err(uncertified()),
764    }
765}
766
767/// The curvature of a 3D intrinsic curve that is a 2D one lifted: planar
768/// (its torsion law identically zero, read structurally from the stored
769/// law by [`Intrinsic3::is_planar`]) in a level plane (its start tangent
770/// and normal stored with no `z` component, so every tangent is
771/// horizontal). There its reference-up frame is the 2D curve's frame
772/// lifted: `lateral` the left normal, `up` `+Z`, and its curvature to the
773/// left the law times the start frame's handedness in plan (#302).
774///
775/// # Errors
776///
777/// A torsion law not identically zero, a start frame whose tangent or
778/// normal has a `z` component (the plane tilts, and the lateral leaves
779/// it), or a start frame not orthonormal in plan, as uncertified.
780fn level_curvature(intrinsic: &Intrinsic3) -> GeomResult<Curvature<'_>> {
781    let (x, y) = (intrinsic.start.x, intrinsic.start.y);
782    if !intrinsic.is_planar() || x.z != 0.0 || y.z != 0.0 {
783        return Err(uncertified());
784    }
785    let plan = Frame2 {
786        origin: Point2::new(intrinsic.start.origin.x, intrinsic.start.origin.y),
787        x: Vec2::new(x.x, x.y),
788        y: Vec2::new(y.x, y.y),
789    };
790    Ok(Curvature::Law {
791        law: &intrinsic.curvature,
792        shift: 0.0,
793        sign: handedness(&plan)?,
794    })
795}
796
797/// An offset beside a base parameterised by its plan arc length, its speed
798/// in closed form (see the [module documentation](self)).
799struct PlanModel<'a> {
800    curvature: Curvature<'a>,
801    /// The profile, where the frame leans with its grade and the law has a
802    /// vertical or longitudinal part.
803    grade: Option<&'a ElevationLaw>,
804    lateral: (Scalar, Scalar),
805    vertical: (Scalar, Scalar),
806    longitudinal: (Scalar, Scalar),
807    /// Whether a vertical rate lengthens the offset: beside a 2D base,
808    /// measured in 3D.
809    vertical_rate: bool,
810    start: Scalar,
811    end: Scalar,
812    reversed: bool,
813}
814
815impl<'a> PlanModel<'a> {
816    /// The model of `offset`, `None` where its base is not parameterised by
817    /// its plan arc length with a curvature law (an intrinsic curve or a
818    /// chain, a level planar 3D intrinsic curve, or an elevated curve).
819    fn of(offset: &StationOffset<'a>) -> GeomResult<Option<Self>> {
820        let base = offset.base();
821        let (lo, hi) = (base.start, base.end);
822        let (curvature, elevation, vertical_rate) = match base.curve {
823            StationCurve::Two(plan @ (Curve2::Intrinsic(_) | Curve2::Chain(_))) => {
824                (plan_curvature(plan, lo, hi)?, None, true)
825            }
826            StationCurve::Three(Curve3::Elevated(elevated)) => (
827                plan_curvature(&elevated.plan, lo, hi)?,
828                Some(&elevated.elevation),
829                false,
830            ),
831            StationCurve::Three(Curve3::Intrinsic(intrinsic)) => {
832                (level_curvature(intrinsic)?, None, true)
833            }
834            StationCurve::Three(Curve3::Banked(_)) => return Err(uncertified()),
835            _ => return Ok(None),
836        };
837        if let Some(rigid) = base.placement {
838            if (rigid.transform_vector3(Vec3::Z) - Vec3::Z).length() > KEEPS_UP_TOLERANCE {
839                return Err(uncertified());
840            }
841        }
842        let zero = (0.0, 0.0);
843        let (lateral, vertical, longitudinal, leans) = match offset.law() {
844            OffsetLaw::Planar { distance } => ((distance, distance), zero, zero, false),
845            OffsetLaw::Directed {
846                distance,
847                reference_direction,
848            } => {
849                let v = reference_direction.normalize();
850                if v.x.hypot(v.y) > 1e-12 {
851                    return Err(uncertified());
852                }
853                let a = distance * v.z.signum();
854                ((a, a), zero, zero, false)
855            }
856            OffsetLaw::Linear { start, end, frame } => {
857                let leans = match frame {
858                    OffsetFrame::Section => true,
859                    OffsetFrame::Plan => false,
860                    _ => return Err(uncertified()),
861                };
862                (
863                    (start.lateral, end.lateral),
864                    (start.vertical, end.vertical),
865                    (start.longitudinal, end.longitudinal),
866                    leans,
867                )
868            }
869            _ => return Err(uncertified()),
870        };
871        let grade = elevation.filter(|_| leans && (vertical != zero || longitudinal != zero));
872        Ok(Some(Self {
873            curvature,
874            grade,
875            lateral,
876            vertical,
877            longitudinal,
878            vertical_rate,
879            start: base.start,
880            end: base.end,
881            reversed: base.reversed,
882        }))
883    }
884
885    fn length_of_base(&self) -> Scalar {
886        self.end - self.start
887    }
888
889    /// The base's own measure at `v` into the piece.
890    fn measure(&self, v: Scalar) -> Scalar {
891        if self.reversed {
892            self.end - v
893        } else {
894            self.start + v
895        }
896    }
897
898    fn travel(&self) -> Scalar {
899        if self.reversed {
900            -1.0
901        } else {
902            1.0
903        }
904    }
905
906    /// The forward speed `1 + b' - a k` and the speed `f` at `v`.
907    fn speed(&self, v: Scalar) -> GeomResult<(Scalar, Scalar)> {
908        let length = self.length_of_base();
909        let s = self.measure(v);
910        let u = (v / length).clamp(0.0, 1.0);
911        let at = |pair: (Scalar, Scalar)| pair.0 + (pair.1 - pair.0) * u;
912        let rate = |pair: (Scalar, Scalar)| (pair.1 - pair.0) / length;
913        let k = self.travel() * self.curvature.value(s)?;
914        let (a, da) = (at(self.lateral), rate(self.lateral));
915        let (long, dlong) = (at(self.longitudinal), rate(self.longitudinal));
916        let (vert, dvert) = (at(self.vertical), rate(self.vertical));
917        let (b, db) = match self.grade {
918            Some(law) => {
919                let p = self.travel() * crate::elevation::elevation_grade(law, s)?;
920                let q = crate::elevation::elevation_second(law, s)?;
921                let cos = 1.0 / p.hypot(1.0);
922                let sin = p * cos;
923                let turn = q / (1.0 + p * p);
924                (
925                    long * cos - vert * sin,
926                    dlong * cos - long * sin * turn - dvert * sin - vert * cos * turn,
927                )
928            }
929            None => (long, dlong),
930        };
931        let forward = 1.0 + db - a * k;
932        let across = da + b * k;
933        let rise = if self.vertical_rate { dvert } else { 0.0 };
934        let speed = (forward * forward + across * across + rise * rise).sqrt();
935        if !speed.is_finite() {
936            return Err(uncertified());
937        }
938        Ok((forward, speed))
939    }
940
941    /// Suprema over the panel `[va, vb]` of the forward speed `A = 1 + b' -
942    /// a k` and the cross speed `B = a' + b k` and their first two
943    /// derivatives, and `sup |A'|` first, for the floor of `A`.
944    fn bounds(&self, va: Scalar, vb: Scalar) -> GeomResult<(Scalar, Jet<3>, Jet<3>)> {
945        let length = self.length_of_base();
946        let (sa, sb) = (self.measure(va), self.measure(vb));
947        let (lo, hi) = (sa.min(sb), sa.max(sb));
948        let k = self.curvature.jet(lo, hi)?;
949        let k: Jet<4> = [k[0], k[1], k[2], 0.0];
950        let a = linear(self.lateral, length);
951        let long = linear(self.longitudinal, length);
952        let vert = linear(self.vertical, length);
953        let b = match self.grade {
954            Some(law) => {
955                let z = crate::elevation::profile_bounds4(law, lo, hi).ok_or_else(uncertified)?;
956                let p: Jet<4> = z;
957                sum(
958                    product(long, composed(COS_OF_ATAN, p)),
959                    product(vert, composed(SIN_OF_ATAN, p)),
960                )
961            }
962            None => long,
963        };
964        let ak = product(a, k);
965        let bk = product(b, k);
966        let forward = [1.0 + b[1] + ak[0], b[2] + ak[1], b[3] + ak[2]];
967        let across = [a[1] + bk[0], a[2] + bk[1], a[3] + bk[2]];
968        Ok((forward[1], forward, across))
969    }
970
971    /// The midpoint rule over the base distances `[a, b]`: the panel's
972    /// bounds `(a, b, lower, upper)`, unbounded where its forward speed
973    /// may vanish.
974    fn panel(&self, a: Scalar, b: Scalar) -> GeomResult<(Scalar, Scalar, Scalar, Scalar)> {
975        let h = b - a;
976        let mid = 0.5 * (a + b);
977        let (forward_mid, speed_mid) = self.speed(mid)?;
978        let (rate, forward, across) = self.bounds(a, b)?;
979        let floor = forward_mid - 0.5 * h * rate;
980        let error = if floor > 0.0 {
981            let g1 = 2.0 * (forward[0] * forward[1] + across[0] * across[1]);
982            let g2 = 2.0
983                * (forward[1] * forward[1]
984                    + forward[0] * forward[2]
985                    + across[1] * across[1]
986                    + across[0] * across[2]);
987            let gmin = floor * floor;
988            let bend = g2 / (2.0 * floor) + g1 * g1 / (4.0 * gmin * floor);
989            h * h * h / 24.0 * bend + VALUE_ROUNDING * h * speed_mid
990        } else {
991            Scalar::INFINITY
992        };
993        if !error.is_finite() {
994            return Ok((a, b, Scalar::NEG_INFINITY, Scalar::INFINITY));
995        }
996        Ok((a, b, h * speed_mid - error, h * speed_mid + error))
997    }
998
999    /// The certified length between the base distances `va < vb`, its
1000    /// panels refined where they are widest ([`panels::adapt`]).
1001    fn length(&self, va: Scalar, vb: Scalar, tolerance: Scalar) -> GeomResult<DistanceInterval> {
1002        let refined = adapt(
1003            vec![self.panel(va, vb)?],
1004            PANEL_SHARE * tolerance,
1005            |p| p.3 - p.2,
1006            |&(a, b, ..)| {
1007                let mid = 0.5 * (a + b);
1008                if !(mid > a && mid < b) {
1009                    return Err(uncertified());
1010                }
1011                Ok((self.panel(a, mid)?, self.panel(mid, b)?))
1012            },
1013        )?;
1014        let (mut lower, mut upper) = (Compensated::default(), Compensated::default());
1015        for &(_, _, lo, hi) in &refined.panels {
1016            lower.add(lo);
1017            upper.add(hi);
1018        }
1019        finish(lower.value(), upper.value(), tolerance).map_err(|error| {
1020            if refined.exhausted {
1021                budget()
1022            } else {
1023                error
1024            }
1025        })
1026    }
1027}
1028
1029// --- seams ------------------------------------------------------------------------
1030
1031/// A seam of a station basis whose frame may jump, with its certified
1032/// distance (#294).
1033#[derive(Debug, Clone, Copy, PartialEq)]
1034pub(crate) struct CertifiedSeam {
1035    /// Where the kernel reads it: the distance a station on it is read at.
1036    pub(crate) distance: Scalar,
1037    /// Where it truly lies.
1038    pub(crate) interval: DistanceInterval,
1039}
1040
1041/// The certified seams of an atomic curve whose frame may jump there,
1042/// `seams` its seams (`station_seams2`/`station_seams3`): exact ones at
1043/// their distance, a B-spline's corner knot by chords from its start.
1044pub(crate) fn atomic_seams(
1045    curve: StationCurve<'_>,
1046    seams: Vec<StationSeam>,
1047    tolerance: Scalar,
1048) -> GeomResult<Vec<CertifiedSeam>> {
1049    seams
1050        .into_iter()
1051        .filter(|seam| !seam.smooth)
1052        .map(|seam| {
1053            let interval = if seam.exact {
1054                DistanceInterval::point(seam.distance)
1055            } else {
1056                curve_span(curve, 0.0, seam.distance, tolerance)?
1057            };
1058            Ok(CertifiedSeam {
1059                distance: seam.distance,
1060                interval,
1061            })
1062        })
1063        .collect()
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068    use super::*;
1069
1070    /// The suprema the leaning section's jets use are upper bounds of
1071    /// `cos(atan p)`, `sin(atan p)` and their first three derivatives:
1072    /// sampled densely, and the derivatives checked against differences.
1073    #[test]
1074    fn the_grade_suprema_bound_their_functions() {
1075        let derivatives = |p: Scalar| {
1076            let u = 1.0 + p * p;
1077            (
1078                [
1079                    u.powf(-0.5),
1080                    -p * u.powf(-1.5),
1081                    (2.0 * p * p - 1.0) * u.powf(-2.5),
1082                    p * (9.0 - 6.0 * p * p) * u.powf(-3.5),
1083                ],
1084                [
1085                    p * u.powf(-0.5),
1086                    u.powf(-1.5),
1087                    -3.0 * p * u.powf(-2.5),
1088                    (12.0 * p * p - 3.0) * u.powf(-3.5),
1089                ],
1090            )
1091        };
1092        for i in -200_000..=200_000 {
1093            let p = Scalar::from(i) * 1e-4;
1094            let (cos, sin) = derivatives(p);
1095            for k in 0..4 {
1096                assert!(cos[k].abs() <= COS_OF_ATAN[k], "cos^({k}) at {p}");
1097                assert!(sin[k].abs() <= SIN_OF_ATAN[k], "sin^({k}) at {p}");
1098            }
1099        }
1100        let h = 1e-5;
1101        for p in [-3.0, -0.4, 0.0, 0.3, 2.5] {
1102            let (at, _) = derivatives(p);
1103            let (up, _) = derivatives(p + h);
1104            let (down, _) = derivatives(p - h);
1105            for k in 0..3 {
1106                let difference = (up[k] - down[k]) / (2.0 * h);
1107                assert!(
1108                    (difference - at[k + 1]).abs() < 1e-6,
1109                    "cos^({}) at {p}",
1110                    k + 1
1111                );
1112            }
1113        }
1114    }
1115
1116    /// `y = a x^3` over `x in [0, reach]` as an exact cubic Bezier, with its
1117    /// arc length from 0 to `x` by the binomial series `sum_n C(1/2, n) (9
1118    /// a^2)^n x^(4n+1) / (4n+1)`: both independent of the chord panels.
1119    fn cubic(a: Scalar, reach: Scalar) -> Curve2 {
1120        Curve2::BSpline(axiolid_curve::BSplineCurve2 {
1121            degree: 3,
1122            control_points: vec![
1123                Point2::new(0.0, 0.0),
1124                Point2::new(reach / 3.0, 0.0),
1125                Point2::new(2.0 * reach / 3.0, 0.0),
1126                Point2::new(reach, a * reach.powi(3)),
1127            ],
1128            knots: vec![0.0, 1.0],
1129            multiplicities: vec![4, 4],
1130            weights: None,
1131            closed: false,
1132            self_intersect: None,
1133            knot_spec: axiolid_curve::KnotSpec::PiecewiseBezier,
1134        })
1135    }
1136
1137    fn cubic_series(a: Scalar, x: Scalar) -> Scalar {
1138        let z = 9.0 * a * a * x.powi(4);
1139        let (mut binomial, mut power, mut sum) = (1.0, 1.0, 0.0);
1140        for n in 0..200 {
1141            let term = binomial * power / (4.0 * Scalar::from(n) + 1.0);
1142            sum += term;
1143            if term.abs() < 1e-30 {
1144                break;
1145            }
1146            binomial *= (0.5 - Scalar::from(n)) / (Scalar::from(n) + 1.0);
1147            power *= z;
1148        }
1149        x * sum
1150    }
1151
1152    #[test]
1153    fn curve_lengths_enclose_the_cubic_series() {
1154        let (radius, reach) = (300.0, 60.0);
1155        let a = 1.0 / (6.0 * radius * reach);
1156        let curve = cubic(a, reach);
1157        for target in [1e-9, 1e-11] {
1158            let lengths = CurveLengths::new(&curve, 0.0, 1.0, target).unwrap();
1159            for i in 0..=1000 {
1160                let t = Scalar::from(i) / 1000.0;
1161                let (lo, hi) = lengths.at(t).unwrap();
1162                let series = cubic_series(a, t * reach);
1163                assert!(
1164                    lo <= series && series <= hi,
1165                    "t={t} target={target}: {lo} {series} {hi}"
1166                );
1167                assert!(
1168                    hi - lo <= 2.0 * target,
1169                    "t={t} target={target}: width {}",
1170                    hi - lo
1171                );
1172            }
1173            let (lo, hi) = lengths.total();
1174            let series = cubic_series(a, reach);
1175            assert!(lo <= series && series <= hi);
1176            assert!(!lengths.panels().is_empty());
1177        }
1178    }
1179
1180    #[test]
1181    fn curve_lengths_refuse_a_family_without_a_closed_form() {
1182        let curve = Curve2::Polyline(axiolid_curve::Polyline2 {
1183            points: vec![Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)],
1184            closed: false,
1185        });
1186        assert!(CurveLengths::new(&curve, 0.0, 1.0, 1e-9).is_err());
1187    }
1188}