axiolid_evaluate/station/frame_bound/bend.rs
1//! The signed bend of a section frame law (#361, ADR 0082 amendment).
2//!
3//! A section placed at the distance `s` lies in the plane through its
4//! point normal to the frame's tangent `N`. A point `A = O + a L + b U + c
5//! T` of the section, with the offsets `a`, `b`, `c` and the frame `(T, L,
6//! U)` read at `s`, moves along `N` at
7//!
8//! `F = O'.N + c' - a L.N' - b U.N'`
9//!
10//! (`T.T' = 0`, and `L.N = U.N = 0`, so `L' . N = -L . N'`): the section
11//! planes advance past the point while `F` is positive, and stop where it
12//! is not, the point on the inner side of the bend at or beyond its centre
13//! of curvature. [`FrameLaw::bend`] bounds the three coefficients
14//!
15//! - `sigma = O'.N`, the forward rate of the basis point;
16//! - `beta_L = L.N'` and `beta_U = U.N'`, how fast the frame's tangent
17//! turns toward the lateral and the up axis,
18//!
19//! over a span, and a caller decides `F` for the points of its sections.
20//! They are affine in the point's coordinates, so over a section the
21//! smallest `F` stands at a vertex.
22//!
23//! # Closed forms
24//!
25//! With `v = O'(s)` and `w = O''(s)` in the world, per unit of station
26//! distance, the Section reading has `N = v / |v|`, so `sigma = v . N`,
27//! `beta_L = L . w / |v|` and `beta_U = U . w / |v|`. The plan reading has
28//! `N` the horizontal part of the tangent, `U = +Z` and `N' `horizontal, so
29//! `beta_U = 0` and `beta_L = L_p . w / |v_h|`. On a plan circle `beta_L`
30//! is the curvature, on a gradient curve `beta_U` is `z'' / |v|^2`.
31//!
32//! The jet `(v, w)` is closed form where the point is: a line, a circle
33//! with an orthonormal frame, a polyline's segment, an elevated or a banked
34//! curve over a line or a circle, and a piece of an intrinsic curve (a
35//! banked curve over an intrinsic plan included, #396), whose heading and
36//! curvature are closed form (`IntrinsicReader::jet`).
37//! A reversed piece negates `v` and keeps `w`; a placement maps both.
38//!
39//! # Bounds over a span
40//!
41//! At the span's middle each coefficient is computed from the jet and the
42//! frame the kernel reads, within `(64 eps + 2 e) |v|` for `sigma` and
43//! `(64 eps + 3 e) |w| / |v|` for the other two, `e` the frames' certified
44//! axis error. Over the span it moves by at most half the span (plus the
45//! distance a read may lie off) times the coefficient's Lipschitz
46//! constant, from the span's [`FrameLawBounds`]:
47//!
48//! - `sigma`: `sup |O''| + sup |O'| sup |N'|`;
49//! - `beta_L`: `sup |L'| sup |N'| + sup |N''|`, and `beta_U` likewise.
50//!
51//! Frames read by quadrature (an ellipse, a B-spline, an offset) and
52//! every frame without certified bounds have no bend.
53
54use axiolid_core::{Scalar, Vec3};
55use axiolid_curve::{Curve2, Curve3, SeamSide};
56
57use super::{
58 closed_form_elevated, lift, orthonormal, polyline_segment, unit, FrameLaw, FrameLawBounds,
59 FrameReading, Slot,
60};
61use crate::station::{StationCurve, StationPiece};
62
63/// Certified bounds, over a span of station distance, of the coefficients
64/// of the signed bend of a section frame law; see the
65/// [module documentation](super#the-signed-bend-361).
66#[non_exhaustive]
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct BendBounds {
69 /// Certified `[lo, hi]` over the span of `O'.N`.
70 pub forward: [Scalar; 2],
71 /// Certified `[lo, hi]` over the span of `L.N'`.
72 pub lateral: [Scalar; 2],
73 /// Certified `[lo, hi]` over the span of `U.N'`.
74 pub up: [Scalar; 2],
75 /// `[forward, lateral, up]` at the span's middle, each within
76 /// `middle_error`.
77 pub middle: [Scalar; 3],
78 /// How far each `middle` value may lie from the coefficient there.
79 pub middle_error: [Scalar; 3],
80}
81
82/// The jet `(O', O'')` of a closed-form piece at distance `u` into it, in
83/// station distance, as the piece presents it (reversed, placed); `None`
84/// for a curve whose point is not closed form.
85pub(crate) fn closed_jet(piece: &StationPiece<'_>, u: Scalar) -> Option<(Vec3, Vec3)> {
86 let (m, _) = piece.read_at(u, SeamSide::Outgoing);
87 let jet = match piece.curve {
88 StationCurve::Two(curve) => jet2(curve, m)?,
89 StationCurve::Three(curve) => jet3(curve, m)?,
90 StationCurve::Offset(_) => return None,
91 };
92 presented(piece, jet)
93}
94
95/// A curve's jet in its own measure as `piece` presents it: negated
96/// velocity when reversed, then placed.
97pub(crate) fn presented(piece: &StationPiece<'_>, (v, w): (Vec3, Vec3)) -> Option<(Vec3, Vec3)> {
98 let (v, w) = if piece.reversed { (-v, w) } else { (v, w) };
99 let (v, w) = match piece.placement {
100 Some(rigid) => (rigid.transform_vector3(v), rigid.transform_vector3(w)),
101 None => (v, w),
102 };
103 (v.is_finite() && w.is_finite()).then_some((v, w))
104}
105
106fn jet2(curve: &Curve2, m: Scalar) -> Option<(Vec3, Vec3)> {
107 match curve {
108 Curve2::Line(line) => Some((unit(lift(line.direction))?, Vec3::ZERO)),
109 Curve2::Circle(c) => {
110 if !orthonormal(lift(c.frame.x), lift(c.frame.y)) {
111 return None;
112 }
113 let t = m / c.radius;
114 let v = lift(crate::curve::derivative2(curve, t).ok()?) / c.radius;
115 let w = lift(crate::curve::second_derivative2(curve, t).ok()?) / (c.radius * c.radius);
116 Some((v, w))
117 }
118 Curve2::Polyline(polyline) => {
119 let step =
120 polyline_segment(&polyline.points, polyline.closed, m, m, |a, b| lift(b - a))?;
121 Some((unit(step)?, Vec3::ZERO))
122 }
123 _ => None,
124 }
125}
126
127fn jet3(curve: &Curve3, m: Scalar) -> Option<(Vec3, Vec3)> {
128 match curve {
129 Curve3::Line(line) => Some((unit(line.direction)?, Vec3::ZERO)),
130 Curve3::Circle(c) => {
131 if !orthonormal(c.frame.x, c.frame.y) {
132 return None;
133 }
134 let t = m / c.radius;
135 let v = crate::curve::derivative3(curve, t).ok()? / c.radius;
136 let w = crate::curve::second_derivative3(curve, t).ok()? / (c.radius * c.radius);
137 Some((v, w))
138 }
139 Curve3::Polyline(polyline) => {
140 let step = polyline_segment(&polyline.points, polyline.closed, m, m, |a, b| b - a)?;
141 Some((unit(step)?, Vec3::ZERO))
142 }
143 Curve3::Elevated(elevated) => {
144 if !closed_form_elevated(elevated) {
145 return None;
146 }
147 Some((
148 crate::elevated::elevated_derivative(elevated, m).ok()?,
149 crate::elevated::elevated_second_derivative(elevated, m).ok()?,
150 ))
151 }
152 Curve3::Banked(banked) => {
153 if !closed_form_elevated(&banked.base) {
154 return None;
155 }
156 Some((
157 crate::banked::banked_derivative(banked, m).ok()?,
158 crate::banked::banked_second_derivative(banked, m).ok()?,
159 ))
160 }
161 _ => None,
162 }
163}
164
165/// The Lipschitz constants, in station distance, of `sigma`, `beta_L` and
166/// `beta_U` from the span's frame bounds (`beta_U` is zero in the plan
167/// reading, where `U = +Z` and `N'` is horizontal).
168fn lipschitz(bounds: &FrameLawBounds, plan: bool) -> [Scalar; 3] {
169 let beta = |axis: [Scalar; 2]| axis[0] * bounds.tangent[0] + bounds.tangent[1];
170 [
171 bounds.point[1] + bounds.point[0] * bounds.tangent[0],
172 beta(bounds.lateral),
173 if plan { 0.0 } else { beta(bounds.up) },
174 ]
175}
176
177impl FrameLaw<'_, '_> {
178 /// Whether the frame over the span `[a, b]` has a closed-form jet, so
179 /// that [`Self::bend`] may bound it over narrower spans: `None` from
180 /// `bend` can then be "not bounded at this width" (a span longer than
181 /// twice a bend's radius), which halving the span usually decides. It
182 /// can stay `None` however narrow the span (a vertical tangent under the
183 /// plan reading, joints after a piece read by quadrature that cannot be
184 /// certified, a placement whose columns are not orthogonal). `false` for
185 /// frames read by quadrature, every frame without a closed form and a
186 /// piece placed at a station whose frame is not exact (unless the law
187 /// reads placements as exact, [`FrameLaw::with_placements_read_as_exact`]).
188 #[must_use]
189 pub fn has_bend(&self, a: Scalar, b: Scalar) -> bool {
190 let Some(at) = self.locate(a, b) else {
191 return false;
192 };
193 // A piece placed inexactly bounds nothing (#408), whatever its jet.
194 if !at.piece.placement_exact && !self.placements_read_as_exact {
195 return false;
196 }
197 // Builds the piece's slot, whatever the span's bounds are.
198 let _ = self.read(
199 at.slot,
200 &at.piece,
201 at.from,
202 at.to,
203 at.table,
204 FrameReading::Section,
205 );
206 let mid = 0.5 * (at.from + at.to);
207 match self.slots[at.slot].get() {
208 Some(Slot::Closed) => closed_jet(&at.piece, mid).is_some(),
209 Some(Slot::Intrinsic(reader)) => {
210 let (m, _) = at.piece.read_at(mid, SeamSide::Outgoing);
211 reader.jet(m).is_some()
212 }
213 _ => false,
214 }
215 }
216
217 /// Certified bounds of the coefficients of the signed bend over the
218 /// distances `[a, b]` along the basis, a span that crosses no seam,
219 /// read as `reading`; see the [module documentation](super#the-signed-bend-361).
220 ///
221 /// `None` for frames read by quadrature (an ellipse, a B-spline, an
222 /// offset) and wherever [`Self::bounds`] is `None`.
223 #[must_use]
224 pub fn bend(&self, a: Scalar, b: Scalar, reading: FrameReading) -> Option<BendBounds> {
225 let bounds = self.bounds(a, b, reading)?;
226 let at = self.locate(a, b)?;
227 let mid = 0.5 * (at.from + at.to);
228 let (v, w) = match self.slots[at.slot].get()? {
229 Slot::Closed => closed_jet(&at.piece, mid)?,
230 Slot::Intrinsic(reader) => {
231 let (m, _) = at.piece.read_at(mid, SeamSide::Outgoing);
232 presented(&at.piece, reader.jet(m)?)?
233 }
234 Slot::Read(_) | Slot::Unbounded => return None,
235 };
236 let mut frame = at.piece.section_on(mid, SeamSide::Outgoing).ok()?;
237 let plan = reading == FrameReading::Plan;
238 if plan {
239 frame = frame.plan().ok()?;
240 }
241 let speed = if plan { v.x.hypot(v.y) } else { v.length() };
242 if !(speed.is_finite() && speed > 0.0) {
243 return None;
244 }
245 let middle = [
246 v.dot(frame.tangent),
247 frame.lateral.dot(w) / speed,
248 if plan { 0.0 } else { frame.up.dot(w) / speed },
249 ];
250 let (rounding, axis) = (64.0 * Scalar::EPSILON, bounds.axis_error);
251 let turn = (rounding + 3.0 * axis) * w.length() / speed;
252 let middle_error = [
253 (rounding + 2.0 * axis) * v.length(),
254 turn,
255 if plan { 0.0 } else { turn },
256 ];
257 let reach = 0.5 * (b - a) + bounds.measure;
258 let lip = lipschitz(&bounds, plan);
259 let range = |j: usize| {
260 let pad = middle_error[j] + reach * lip[j];
261 [middle[j] - pad, middle[j] + pad]
262 };
263 let mut forward = range(0);
264 forward[0] = forward[0].max(bounds.forward);
265 let out = BendBounds {
266 forward,
267 lateral: range(1),
268 up: range(2),
269 middle,
270 middle_error,
271 };
272 [out.forward, out.lateral, out.up]
273 .iter()
274 .flatten()
275 .chain(&out.middle)
276 .chain(&out.middle_error)
277 .all(|x| x.is_finite())
278 .then_some(out)
279 }
280}