Skip to main content

axiolid_evaluate/
elevated.rs

1//! Derivatives, certified bounds and seams of elevated and banked curves in
2//! plan distance (#252).
3//!
4//! An [`Elevated3`] is `c(d) = (p(d), z(d))`: a plan `p` read by its own
5//! arc length `d` and a height `z` written against that same `d`. Its
6//! parameter is plan distance (ADR 0082), so its derivatives are taken in
7//! `d`: `c' = (p', z')` with `|p'| = 1` (not a unit vector in 3D: its length
8//! is `sqrt(1 + g^2)`, `g` the grade), `c'' = (k n, z'')` with `k` the plan
9//! curvature and `n` the plan's left normal, `c''' = (k' n - k^2 p', z''')`.
10//! A [`Banked3`]'s point is its centreline raised by the pivot `e(d)`, so it
11//! adds `e^(j)` to every `z^(j)`.
12//!
13//! # The certified 3D chord bound
14//!
15//! Over a span `[a, b]` of width `h`, let `l` be the linear interpolant of
16//! the ends, `l(d) = c(a) + (d - a) / h (c(b) - c(a))`. It lies on the 3D
17//! chord. Componentwise, `c - l = -int G(d, s) c''(s) ds` with the
18//! non-negative Green's function of `-d^2/dd^2` on `[a, b]`, whose integral
19//! is at most `h^2 / 8` ([`crate::bound`]'s chord argument). Applied to the
20//! horizontal and the vertical part separately, at the SAME `d`:
21//!
22//! - `|p(d) - l_p(d)| <= P = h^2 / 8 sup |p''| = h^2 / 8 sup |k|`, the plan's
23//!   own chord bound read at equal plan distance;
24//! - `|z(d) - l_z(d)| <= Z`, the profile's chord bound
25//!   ([`elevation_chord_bound`](crate::elevation::elevation_chord_bound)),
26//!   which is the same Taylor argument on `z`;
27//!
28//! and the two parts are orthogonal (`p` horizontal, `z` vertical), so
29//!
30//! ```text
31//! |c(d) - l(d)| <= sqrt(P^2 + Z^2).
32//! ```
33//!
34//! Every point of the curve is within that of the chord. The bound only
35//! needs `c` to be `C^1` with a bounded second derivative on the span: a
36//! curvature seam of the plan (a clothoid meeting an arc) does not stop it,
37//! a join of a chain or a seam of the profile (where the grade may jump)
38//! does, and those are cut first ([`elevated_breaks`]). A flattener that
39//! accepts a span once `sqrt(P^2 + Z^2) <= tol` has split the budget
40//! between plan and profile in quadrature: `P <= tol cos(a)`,
41//! `Z <= tol sin(a)` for the angle `a = atan(Z / P)` the span itself sets.
42//! A banked curve's vertical part is `z + e`, bounded by `Z + h^2 / 8 sup
43//! |e''|`.
44//!
45//! The plan's chord bound is NOT the plan's own `chord_bound2`: that bounds
46//! the distance from the plan arc to the plan chord, which for a circle is
47//! the exact sagitta at the arc's own nearest chord point, not at equal
48//! plan distance, and the vertical part could then be read at a different
49//! `d`. The Taylor bound at equal `d` is what composes.
50//!
51//! # Derivative bounds
52//!
53//! For a sweep's certificate (the tube wall of `axiolid-mesh-compile`'s
54//! deviation report) the first three derivatives are bounded the same way:
55//! `|c^(j)| <= sqrt(P_j^2 + Z_j^2)`, with `P_1` the plan frame's stretch,
56//! `P_2 = sup |k|`, `P_3 = sup |k'| + sup k^2` (in arc length
57//! `p''' = k' n - k^2 t`; a chain's parametric piece is bounded through its
58//! own derivative suprema), and `Z_j` from
59//! [`crate::elevation`]'s per-law closed forms. They need `C^2` within the
60//! span, so curvature seams of the plan are named as breaks for `k >= 2`.
61
62use axiolid_contracts::{GeomError, GeomResult};
63use axiolid_core::{Interval, Scalar, Vec3};
64use axiolid_curve::{Banked3, Curve2, Curve3, Elevated3};
65
66use crate::arc_length::{plan_derivative, plan_second_derivative};
67use crate::bound::{frame_stretch2, CurveDerivativeBounds, ROUNDING};
68
69fn invalid(detail: &str) -> GeomError {
70    GeomError::InvalidInput(detail.to_owned())
71}
72
73/// Plan length an elevated curve's parameter runs over: unbounded for a
74/// line, one full turn for a circle, the stored length of an intrinsic
75/// curve or a chain; `None` for a plan that cannot carry an elevation law
76/// or has no positive finite length.
77#[must_use]
78pub fn elevated_span(curve: &Elevated3) -> Option<Scalar> {
79    let span = match curve.plan.as_ref() {
80        Curve2::Line(_) => return Some(Scalar::INFINITY),
81        Curve2::Circle(c) => core::f64::consts::TAU * c.radius,
82        Curve2::Intrinsic(i) => i.length,
83        Curve2::Chain(c) => c.length()?,
84        _ => return None,
85    };
86    (span.is_finite() && span > 0.0).then_some(span)
87}
88
89/// Domain of an elevated curve in plan distance: `[0, L]`, `L` from
90/// [`elevated_span`] (infinite for a line plan), or the empty interval.
91pub(crate) fn elevated_domain(curve: &Elevated3) -> Interval {
92    elevated_span(curve).map_or(
93        Interval {
94            start: 0.0,
95            end: 0.0,
96        },
97        |end| Interval { start: 0.0, end },
98    )
99}
100
101/// Derivative of an elevated curve in plan distance: `(p', z')`. Not unit:
102/// its length is `sqrt(1 + g^2)` on an orthonormal plan frame.
103///
104/// # Errors
105///
106/// The plan's and the elevation law's refusals, by name.
107pub fn elevated_derivative(curve: &Elevated3, d: Scalar) -> GeomResult<Vec3> {
108    if !d.is_finite() {
109        return Err(invalid("plan distance must be finite"));
110    }
111    let plan = plan_derivative(&curve.plan, d)?;
112    let grade = crate::elevation::elevation_grade(&curve.elevation, d)?;
113    let out = Vec3::new(plan.x, plan.y, grade);
114    if out.is_finite() {
115        Ok(out)
116    } else {
117        Err(invalid("elevated curve derivative is not finite"))
118    }
119}
120
121/// Second derivative of an elevated curve in plan distance:
122/// `(k n, z'')`, the plan's curvature vector and the profile's `z''`.
123///
124/// # Errors
125///
126/// As [`elevated_derivative`].
127pub fn elevated_second_derivative(curve: &Elevated3, d: Scalar) -> GeomResult<Vec3> {
128    if !d.is_finite() {
129        return Err(invalid("plan distance must be finite"));
130    }
131    let plan = plan_second_derivative(&curve.plan, d)?;
132    let bend = crate::elevation::elevation_second(&curve.elevation, d)?;
133    let out = Vec3::new(plan.x, plan.y, bend);
134    if out.is_finite() {
135        Ok(out)
136    } else {
137        Err(invalid("elevated curve second derivative is not finite"))
138    }
139}
140
141/// `(sup |p'|, sup |p''|, sup |p'''|)` of the plan over `[lo, hi]` in plan
142/// distance, scaled by its frame's stretch; `p'''` is infinite across a
143/// curvature seam. `None` for a plan family or span not bounded here.
144fn plan_bounds(plan: &Curve2, lo: Scalar, hi: Scalar) -> Option<[Scalar; 3]> {
145    let slack = |length: Scalar| 1e-9 * length.max(1.0);
146    let out = match plan {
147        Curve2::Line(line) => {
148            let length = line.direction.length();
149            if !(length.is_finite() && length > 0.0) {
150                return None;
151            }
152            [1.0, 0.0, 0.0]
153        }
154        Curve2::Circle(c) => {
155            if !(c.radius.is_finite() && c.radius > 0.0) {
156                return None;
157            }
158            let stretch = frame_stretch2(&c.frame);
159            let k = 1.0 / c.radius;
160            [stretch, stretch * k, stretch * k * k]
161        }
162        Curve2::Intrinsic(i) => {
163            if !(lo >= 0.0 && hi <= i.length + slack(i.length)) {
164                return None;
165            }
166            let stretch = frame_stretch2(&i.start);
167            let k = crate::chain::curvature_bound(&i.curvature, lo, hi)?;
168            let seamed = i
169                .curvature
170                .seams_within(i.length)
171                .iter()
172                .any(|&seam| seam > lo && seam < hi);
173            let rate = if seamed {
174                Scalar::INFINITY
175            } else {
176                crate::chain::curvature_bound(&i.curvature.derivative(), lo, hi)?
177            };
178            [stretch, stretch * k, stretch * (rate + k * k)]
179        }
180        Curve2::Chain(chain) => {
181            let (p2, p3) = crate::chain::chain_plan_bounds(chain, lo, hi)?;
182            [frame_stretch2(&chain.start), p2, p3]
183        }
184        _ => return None,
185    };
186    (out[0].is_finite() && out[1].is_finite()).then_some(out)
187}
188
189/// Where an elevated curve may fail to be `C^k` (`k >= 1`), ascending: a
190/// chain plan's joins and every seam of the elevation law (the grade may
191/// jump there), and for `k >= 2` the curvature seams of an intrinsic plan
192/// or of a chain's intrinsic pieces.
193#[must_use]
194pub fn elevated_breaks(curve: &Elevated3, k: usize) -> Vec<Scalar> {
195    if k == 0 {
196        return Vec::new();
197    }
198    let mut out = crate::bound::continuity_breaks2(&curve.plan, 1);
199    out.extend(crate::elevation::elevation_seams(&curve.elevation));
200    if k >= 2 {
201        match curve.plan.as_ref() {
202            Curve2::Intrinsic(i) => out.extend(i.curvature.seams_within(i.length)),
203            Curve2::Chain(chain) => out.extend(crate::chain::chain_curvature_seams(chain)),
204            _ => {}
205        }
206    }
207    out.retain(|b| b.is_finite());
208    out.sort_by(Scalar::total_cmp);
209    out.dedup();
210    out
211}
212
213/// [`elevated_breaks`] of a banked curve's point path, plus the seams of
214/// its pivot law (its rate may jump there). The cant law moves only the
215/// section frame, not the point, so its seams are not named -- except
216/// where a held-rail pivot piece derives the point from the cant (#279):
217/// the cant's seams there are the pivot's.
218#[must_use]
219pub fn banked_breaks(curve: &Banked3, k: usize) -> Vec<Scalar> {
220    if k == 0 {
221        return Vec::new();
222    }
223    let mut out = elevated_breaks(&curve.base, k);
224    out.extend(curve.pivot.seams());
225    out.extend(crate::banked::derived_cant_seams(curve));
226    out.sort_by(Scalar::total_cmp);
227    out.dedup();
228    out
229}
230
231fn inside(breaks: &[Scalar], lo: Scalar, hi: Scalar) -> bool {
232    breaks.iter().any(|&b| b > lo && b < hi)
233}
234
235/// Certified 3D chord bound of an elevated curve over plan distances
236/// `[a, b]`: `sqrt(P^2 + Z^2)`, `P = h^2 / 8 sup |k|` the plan's and `Z`
237/// the profile's chord bound at equal plan distance (see the
238/// [module documentation](self) for the derivation).
239///
240/// `None` -- unbounded, never a guess -- across a chain join or a profile
241/// seam, and where either half is not bounded.
242#[must_use]
243pub fn elevated_chord_bound(curve: &Elevated3, a: Scalar, b: Scalar) -> Option<Scalar> {
244    let (lo, hi) = (a.min(b), a.max(b));
245    if !(lo.is_finite() && hi.is_finite()) {
246        return None;
247    }
248    if lo == hi {
249        return Some(0.0);
250    }
251    if inside(&elevated_breaks(curve, 1), lo, hi) {
252        return None;
253    }
254    let h = hi - lo;
255    let plan = h * h * 0.125 * plan_bounds(&curve.plan, lo, hi)?[1];
256    let profile = crate::elevation::elevation_chord_bound(&curve.elevation, lo, hi)?;
257    let bound = plan.hypot(profile) * (1.0 + ROUNDING);
258    bound.is_finite().then_some(bound)
259}
260
261/// [`elevated_chord_bound`] of a banked curve's point path: the vertical
262/// part is `z + e`, bounded by `Z + h^2 / 8 sup |e''|`.
263#[must_use]
264pub fn banked_chord_bound(curve: &Banked3, a: Scalar, b: Scalar) -> Option<Scalar> {
265    let (lo, hi) = (a.min(b), a.max(b));
266    if !(lo.is_finite() && hi.is_finite()) {
267        return None;
268    }
269    if lo == hi {
270        return Some(0.0);
271    }
272    if lo < 0.0 || inside(&banked_breaks(curve, 1), lo, hi) {
273        return None;
274    }
275    let h = hi - lo;
276    let taylor = h * h * 0.125;
277    let plan = taylor * plan_bounds(&curve.base.plan, lo, hi)?[1];
278    let profile = crate::elevation::elevation_chord_bound(&curve.base.elevation, lo, hi)?;
279    let pivot = taylor * crate::banked::pivot_bounds(curve, lo, hi)?[1];
280    let bound = plan.hypot(profile + pivot) * (1.0 + ROUNDING);
281    bound.is_finite().then_some(bound)
282}
283
284/// Certified suprema of `|c'|`, `|c''|`, `|c'''|` of an elevated curve in
285/// plan distance over `[a, b]`: `sqrt(P_j^2 + Z_j^2)` (module
286/// documentation). `None` where the curve may not be `C^2` strictly
287/// inside the span, or where either half is not bounded.
288#[must_use]
289pub fn elevated_derivative_bounds(
290    curve: &Elevated3,
291    a: Scalar,
292    b: Scalar,
293) -> Option<CurveDerivativeBounds> {
294    let (lo, hi) = (a.min(b), a.max(b));
295    if !(lo.is_finite() && hi.is_finite()) || inside(&elevated_breaks(curve, 2), lo, hi) {
296        return None;
297    }
298    let plan = plan_bounds(&curve.plan, lo, hi)?;
299    let profile = crate::elevation::profile_bounds(&curve.elevation, lo, hi)?;
300    combined(plan, profile)
301}
302
303/// [`elevated_derivative_bounds`] of a banked curve's point path, the
304/// pivot's derivatives added to the profile's.
305#[must_use]
306pub fn banked_derivative_bounds(
307    curve: &Banked3,
308    a: Scalar,
309    b: Scalar,
310) -> Option<CurveDerivativeBounds> {
311    let (lo, hi) = (a.min(b), a.max(b));
312    if !(lo.is_finite() && hi.is_finite()) || lo < 0.0 || inside(&banked_breaks(curve, 2), lo, hi) {
313        return None;
314    }
315    let plan = plan_bounds(&curve.base.plan, lo, hi)?;
316    let profile = crate::elevation::profile_bounds(&curve.base.elevation, lo, hi)?;
317    let pivot = crate::banked::pivot_bounds(curve, lo, hi)?;
318    combined(
319        plan,
320        [
321            profile[0] + pivot[0],
322            profile[1] + pivot[1],
323            profile[2] + pivot[2],
324        ],
325    )
326}
327
328fn combined(plan: [Scalar; 3], vertical: [Scalar; 3]) -> Option<CurveDerivativeBounds> {
329    let bound = |j: usize| plan[j].hypot(vertical[j]) * (1.0 + ROUNDING);
330    let out = CurveDerivativeBounds {
331        first: bound(0),
332        second: bound(1),
333        third: bound(2),
334    };
335    (out.first.is_finite() && out.second.is_finite() && out.third.is_finite()).then_some(out)
336}
337
338/// Whether [`crate::curve::flatten3`] certifies an elevated curve's chords:
339/// its plan is a line, a circle, an intrinsic curve whose curvature law is
340/// bounded, or a chain whose pieces all are
341/// ([`crate::chain::chain_certifies`]), and every piece of its profile is
342/// a polynomial or a circular arc. A circular arc's bound exists wherever
343/// the arc has heights, so no span the curve evaluates on is left to the
344/// midpoint sagitta.
345pub(crate) fn certifies_elevated(curve: &Elevated3) -> bool {
346    fn closed_form(law: &axiolid_curve::ElevationLaw) -> bool {
347        use axiolid_curve::ElevationLaw as L;
348        match law {
349            L::Polynomial { .. } | L::CircularArc { .. } => true,
350            L::Piecewise { laws, .. } => laws.iter().all(closed_form),
351            _ => false,
352        }
353    }
354    let plan = match curve.plan.as_ref() {
355        Curve2::Line(_) | Curve2::Circle(_) => true,
356        Curve2::Intrinsic(i) => {
357            crate::chain::curvature_bound(&i.curvature, 0.0, i.length.max(0.0)).is_some()
358        }
359        Curve2::Chain(chain) => crate::chain::chain_certifies(chain),
360        _ => false,
361    };
362    plan && curve.elevation.is_well_formed() && closed_form(&curve.elevation)
363}
364
365/// Where an elevated or banked curve turns a corner: the seams (of its
366/// profile, and of a banked curve's pivot) across which the grade of its
367/// point path jumps by more than `angle` radians, ascending.
368///
369/// A chain plan is tangent-continuous at its joins by construction, so
370/// only the vertical part can kink. Read from the laws on either side of
371/// each seam, not from nearby samples. Empty for every other family.
372///
373/// # Errors
374///
375/// The laws' refusals at a seam, by name.
376pub fn grade_corners3(curve: &Curve3, angle: Scalar) -> GeomResult<Vec<Scalar>> {
377    let (elevated, banked) = match curve {
378        Curve3::Elevated(e) => (e, None),
379        Curve3::Banked(b) => (&b.base, Some(b)),
380        _ => return Ok(Vec::new()),
381    };
382    let mut seams = crate::elevation::elevation_seams(&elevated.elevation);
383    if let Some(banked) = banked {
384        seams.extend(banked.pivot.seams());
385        seams.extend(crate::banked::derived_cant_seams(banked));
386    }
387    seams.sort_by(Scalar::total_cmp);
388    seams.dedup();
389    let mut out = Vec::new();
390    for seam in seams {
391        let mut before = crate::elevation::elevation_grade_before(&elevated.elevation, seam)?;
392        let mut after = crate::elevation::elevation_grade(&elevated.elevation, seam)?;
393        if let Some(banked) = banked {
394            let rate = |before: bool| {
395                crate::banked::pivot_jet(banked, seam, before)
396                    .map(|jet| jet[1])
397                    .ok_or_else(|| invalid("banked curve: pivot law has no rate at a seam"))
398            };
399            before += rate(true)?;
400            after += rate(false)?;
401        }
402        if (before.atan() - after.atan()).abs() > angle {
403            out.push(seam);
404        }
405    }
406    Ok(out)
407}