Skip to main content

axiolid_evaluate/bound/
elevated_positions.rs

1//! Certified positions of elevated and banked curves over an intrinsic
2//! plan (#393) or a chain plan (#438).
3//!
4//! An [`Elevated3`](axiolid_curve::Elevated3) is `(p(d), z(d))`: the plan
5//! `p` read by its own arc length and a closed-form height `z`. A
6//! [`Banked3`](axiolid_curve::Banked3) raises that point by the closed-form
7//! pivot `e(d)`. Only `p` is an integral, so the point's error is the plan's:
8//! [`ElevatedPositionTable`] reads `p` from an
9//! [`IntrinsicPositionTable`] (or, over a chain plan, a
10//! [`ChainPositionTable`]) and adds the height (and the pivot) in closed
11//! form. The closed-form height and pivot are not charged their own
12//! rounding, as for every closed-form point in the tube certificate of
13//! `axiolid-mesh-compile`.
14//!
15//! The evaluator's own point (`crate::arc_length::intrinsic_point`) is an
16//! estimate with no certified error, off by 1.4 cm at 96.5 m on a fast
17//! harmonic: [`chord_bound3`](super::chord_bound3) and the swept-disk
18//! certificate read this table instead of trusting it.
19//!
20//! # Evaluated ends (#393)
21//!
22//! [`chord_bound3`](super::chord_bound3) bounds the distance from the arc to
23//! the chord of the *evaluated* ends. For an elevated or banked curve over
24//! an intrinsic plan the exact-chord bound of `crate::elevated` (`h^2 / 8
25//! sup |k|` composed with the profile's) is not that: the evaluated plan
26//! point is off the exact one, and the chord of two moved ends moves by
27//! at most the larger of the two moves. `evaluated_chord_bound3` adds that
28//! larger gap, each the evaluator's point against a table point plus the
29//! table point's own error, to the exact-chord bound. Over a chain plan
30//! (#438) the ends are read as a chain's chord bound reads them
31//! ([`crate::chain::chain_chord_bound`]): the lower one as the piece that
32//! starts there, the upper one as the piece that ends there. Under an
33//! intrinsic profile (#481) the exact-chord bound is returned as it
34//! stands, and UNDER-reports the evaluated ends: `certifies_flattening3`
35//! does not claim it, so no consumer takes it as a certificate.
36//!
37//! # Jets over a chain plan (#438)
38//!
39//! A swept tube reads the tangent and the second derivative of its centre
40//! line too. Over an intrinsic plan those are closed forms of the heading,
41//! exact but for rounding; over a chain plan a parametric piece's are read at
42//! the evaluator's parameter. [`ElevatedPositionTable::jet3_on`] reads them
43//! from the chain table instead (the plan's `p'` and `p''`, the profile's
44//! `z'` and `z''` in closed form), and [`ElevatedPositionTable::jet_errors`]
45//! bounds how far each is from the exact one.
46
47use axiolid_core::{Point3, Scalar, Vec3};
48use axiolid_curve::{Curve2, Curve3, Elevated3, ElevationLaw};
49
50use super::interval::Iv;
51use super::{
52    frame_stretch2, inflate, ordered, ChainPositionTable, IntrinsicPositionTable, SeamSide,
53};
54
55/// The certified positions of the plan of an [`ElevatedPositionTable`].
56#[derive(Debug, Clone)]
57enum PlanPositions {
58    Intrinsic(IntrinsicPositionTable),
59    Chain(ChainPositionTable),
60}
61
62/// Certified positions of an elevated or banked curve over an intrinsic or
63/// a chain plan, in plan distance, read in any order: the plan from an
64/// [`IntrinsicPositionTable`] or a [`ChainPositionTable`], the height and
65/// the pivot in closed form.
66#[derive(Debug, Clone)]
67pub struct ElevatedPositionTable {
68    plan: PlanPositions,
69    curve: Curve3,
70    end: Scalar,
71}
72
73impl ElevatedPositionTable {
74    /// The table of `curve` over `[0, end]`, its plan points within about
75    /// `error` ([`IntrinsicPositionTable::new2`],
76    /// [`ChainPositionTable::new`]). `Some` only for an elevated or banked
77    /// curve whose plan is an intrinsic curve or a chain a
78    /// [`ChainPositionTable`] covers, whose profile is closed form (no
79    /// intrinsic profile), and for a banked curve whose cant and pivot laws
80    /// are well formed with no held-rail piece in the cant; `None`
81    /// otherwise, for an error or an end that is not finite and positive,
82    /// or an end past the plan's length.
83    #[must_use]
84    pub fn new(curve: &Curve3, error: Scalar, end: Scalar) -> Option<Self> {
85        let base: &Elevated3 = match curve {
86            Curve3::Elevated(e) => e,
87            Curve3::Banked(b) => {
88                if !(b.cant.is_well_formed() && b.pivot.is_well_formed())
89                    || b.cant.has_rail_pieces()
90                {
91                    return None;
92                }
93                &b.base
94            }
95            _ => return None,
96        };
97        if !crate::elevated::certifies_elevated(base) {
98            return None;
99        }
100        let plan = match base.plan.as_ref() {
101            Curve2::Intrinsic(plan) => {
102                PlanPositions::Intrinsic(IntrinsicPositionTable::new2(plan, error, end)?)
103            }
104            Curve2::Chain(chain) => {
105                PlanPositions::Chain(ChainPositionTable::new(chain, error, end)?)
106            }
107            _ => return None,
108        };
109        Some(Self {
110            plan,
111            curve: curve.clone(),
112            end,
113        })
114    }
115
116    /// The point at plan distance `d` and a certified bound on its
117    /// distance from the exact point: the plan table's error at `d`, with
118    /// the closed-form height (and pivot) exact. `None` outside `[0, end]`
119    /// or where the profile or the pivot has no value.
120    #[must_use]
121    pub fn point3(&self, d: Scalar) -> Option<(Point3, Scalar)> {
122        self.point3_on(d, SeamSide::Outgoing)
123    }
124
125    /// [`Self::point3`] on `side` of a join of a chain plan (the side makes
126    /// no difference over an intrinsic plan, or off a join).
127    #[must_use]
128    pub fn point3_on(&self, d: Scalar, side: SeamSide) -> Option<(Point3, Scalar)> {
129        if !(d.is_finite() && (0.0..=self.end).contains(&d)) {
130            return None;
131        }
132        let (plan, error) = match &self.plan {
133            PlanPositions::Intrinsic(table) => table.point2(d)?,
134            PlanPositions::Chain(table) => table.point2_on(d, side)?,
135        };
136        let (z, _, _) = self.vertical(d)?;
137        let out = Point3::new(plan.x, plan.y, z);
138        out.is_finite().then_some((out, error))
139    }
140
141    /// The height, grade and second derivative of the profile (and the
142    /// pivot) at `d`, in closed form, as the evaluator reads them.
143    fn vertical(&self, d: Scalar) -> Option<(Scalar, Scalar, Scalar)> {
144        let (law, pivot) = match &self.curve {
145            Curve3::Elevated(e) => (&e.elevation, None),
146            Curve3::Banked(b) => (&b.base.elevation, Some(b)),
147            _ => return None,
148        };
149        let height = crate::elevation::elevation_height(law, d).ok()?;
150        let grade = crate::elevation::elevation_grade(law, d).ok()?;
151        let bend = crate::elevation::elevation_second(law, d).ok()?;
152        let (e, rate, extra) = match pivot {
153            Some(b) => {
154                let (e, rate) = b.pivot_at(d).ok()?;
155                (e, rate, crate::banked::pivot_jet(b, d, false)?[2])
156            }
157            None => (0.0, 0.0, 0.0),
158        };
159        let out = (height + e, grade + rate, bend + extra);
160        (out.0.is_finite() && out.1.is_finite() && out.2.is_finite()).then_some(out)
161    }
162
163    /// The point, the derivative and the second derivative at plan distance
164    /// `d` on `side` of a join, `(c, c', c'')`: over a chain plan the plan
165    /// parts (`p'`, `p''`) from the chain table and the profile's and the
166    /// pivot's in closed form; over an intrinsic plan the curve's own
167    /// closed-form derivatives, bit for bit. [`Self::jet_errors`] bounds
168    /// their distances from the exact ones.
169    #[must_use]
170    pub fn jet3_on(&self, d: Scalar, side: SeamSide) -> Option<(Point3, Vec3, Vec3)> {
171        let (point, _) = self.point3_on(d, side)?;
172        let (first, second) = match &self.plan {
173            PlanPositions::Intrinsic(_) => (
174                crate::curve::derivative3(&self.curve, d).ok()?,
175                crate::curve::second_derivative3(&self.curve, d).ok()?,
176            ),
177            PlanPositions::Chain(table) => {
178                let (_, grade, bend) = self.vertical(d)?;
179                let (tangent, _) = table.tangent2_on(d, side)?;
180                let (curvature, _) = table.curvature2_on(d, side)?;
181                (
182                    Vec3::new(tangent.x, tangent.y, grade),
183                    Vec3::new(curvature.x, curvature.y, bend),
184                )
185            }
186        };
187        (first.is_finite() && second.is_finite()).then_some((point, first, second))
188    }
189
190    /// Certified bounds, uniform over the table, on how far a point, a
191    /// derivative and a second derivative it returns are from the exact
192    /// ones: `[e0, e1, e2]`. The plan's `max_error`, `max_tangent_error` and
193    /// `max_curvature_error` over a chain plan (the closed-form profile adds
194    /// none); `[max_error, 0, 0]` over an intrinsic plan, whose derivatives
195    /// are exact closed forms of the heading.
196    #[must_use]
197    pub fn jet_errors(&self) -> [Scalar; 3] {
198        match &self.plan {
199            PlanPositions::Intrinsic(table) => [table.max_error(), 0.0, 0.0],
200            PlanPositions::Chain(table) => [
201                table.max_error(),
202                table.max_tangent_error(),
203                table.max_curvature_error(),
204            ],
205        }
206    }
207
208    /// A certified bound on the error of every point the table returns:
209    /// the `max_error` of its plan's table.
210    #[must_use]
211    pub fn max_error(&self) -> Scalar {
212        self.jet_errors()[0]
213    }
214}
215
216/// Whether any piece of `law` is an intrinsic (vertical clothoid) profile.
217fn has_intrinsic_piece(law: &ElevationLaw) -> bool {
218    match law {
219        ElevationLaw::Intrinsic { .. } => true,
220        ElevationLaw::Piecewise { laws, .. } => laws.iter().any(has_intrinsic_piece),
221        _ => false,
222    }
223}
224
225/// Certified chord bound of an elevated or banked curve over plan
226/// distances `[a, b]`, for the *evaluated* ends (see the module
227/// documentation). `None` where the exact-chord bound is `None`, or where
228/// the table, the evaluator or the pivot refuses an end.
229pub(super) fn evaluated_chord_bound3(curve: &Curve3, a: Scalar, b: Scalar) -> Option<Scalar> {
230    let (law, base) = match curve {
231        Curve3::Elevated(e) => (crate::elevated::elevated_chord_bound(e, a, b)?, e),
232        Curve3::Banked(banked) => (
233            crate::elevated::banked_chord_bound(banked, a, b)?,
234            &banked.base,
235        ),
236        _ => return None,
237    };
238    let (start, length) = match base.plan.as_ref() {
239        Curve2::Intrinsic(plan) => (plan.start, plan.length),
240        Curve2::Chain(chain) => (chain.start, chain.length()?),
241        _ => return Some(law),
242    };
243    if has_intrinsic_piece(&base.elevation) {
244        // TODO(#481): a vertical clothoid's heights carry no certified
245        // error; `certifies_flattening3` does not claim it either.
246        return Some(law);
247    }
248    let (lo, hi) = ordered(a, b);
249    let stretch = frame_stretch2(&start);
250    let origin = start.origin.abs().max_element();
251    let floor = 64.0 * Scalar::EPSILON * (1.0 + origin + stretch * hi);
252    let table = ElevatedPositionTable::new(curve, (law / 8.0).max(floor), hi.min(length))?;
253    let gap = |s: Scalar, side: SeamSide| {
254        let (exact, error) = table.point3_on(s.min(length), side)?;
255        let read = crate::curve::evaluate3(curve, s).ok()?;
256        let d = [read.x - exact.x, read.y - exact.y, read.z - exact.z]
257            .map(|c| Iv::point(c).sq())
258            .into_iter()
259            .fold(Iv::point(0.0), Iv::add)
260            .sqrt();
261        Some(Iv::point(d.hi).add(Iv::point(error)).hi)
262    };
263    let g = gap(lo, SeamSide::Outgoing)?.max(gap(hi, SeamSide::Incoming)?);
264    let out = inflate(Iv::point(law).add(Iv::point(g)).hi);
265    out.is_finite().then_some(out)
266}