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}