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