Skip to main content

axiolid_evaluate/station/frame_bound/
error.rs

1//! How far one station's frame lies from the exact frame law (#405); the
2//! derivation is the parent module's section "One station".
3
4use axiolid_core::Scalar;
5use axiolid_curve::SeamSide;
6
7use super::{FrameLaw, FrameReading, LawBasis, Slot};
8use crate::arc_parameter::ARC_LENGTH_TOLERANCE;
9use crate::bound::ROUNDING;
10use crate::station::certify::{AXIS_ROUNDING, POINT_ROUNDING};
11use crate::station::offset::OffsetTable;
12use crate::station::{admitted, StationCurve, StationPiece};
13
14/// How far a frame read at a station lies from the exact frame law's, see
15/// the [module documentation](super#one-station-405).
16#[non_exhaustive]
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct StationFrameError {
19    /// The largest distance of the read frame's point from the exact
20    /// frame's.
21    pub point: Scalar,
22    /// The largest distance of one of the read frame's axes from the exact
23    /// frame's.
24    pub axis: Scalar,
25}
26
27/// The arc-length tolerance around a distance: what counts as ON a seam.
28fn slack(distance: Scalar) -> Scalar {
29    ARC_LENGTH_TOLERANCE * distance.abs().max(1.0)
30}
31
32/// A station read: its distance along the basis and into the piece it is
33/// read on, and how it is read.
34#[derive(Clone, Copy)]
35struct Stand {
36    at: Scalar,
37    into: Scalar,
38    side: SeamSide,
39    reading: FrameReading,
40}
41
42/// The largest magnitude the closed-form read of `piece` works with: its
43/// curve's stored data (a circle's `|origin|_inf + r`, a polyline's points,
44/// a line's origin and direction, a plan's data under an elevation) and the
45/// placement's translation. The read's cancellations (`r - r cos a`) round
46/// at this scale, not at the point's.
47fn data_scale(piece: &StationPiece<'_>) -> Scalar {
48    use axiolid_curve::{Curve2, Curve3};
49    fn plan(curve: &Curve2) -> Scalar {
50        match curve {
51            Curve2::Line(l) => l.origin.abs().max_element() + l.direction.abs().max_element(),
52            Curve2::Circle(c) => c.frame.origin.abs().max_element() + c.radius.abs(),
53            Curve2::Polyline(p) => p
54                .points
55                .iter()
56                .map(|q| q.abs().max_element())
57                .fold(0.0, Scalar::max),
58            _ => Scalar::INFINITY,
59        }
60    }
61    let curve = match piece.curve {
62        StationCurve::Two(c) => plan(c),
63        StationCurve::Three(c) => match c {
64            Curve3::Line(l) => l.origin.abs().max_element() + l.direction.abs().max_element(),
65            Curve3::Circle(c) => c.frame.origin.abs().max_element() + c.radius.abs(),
66            Curve3::Polyline(p) => p
67                .points
68                .iter()
69                .map(|q| q.abs().max_element())
70                .fold(0.0, Scalar::max),
71            Curve3::Elevated(e) => plan(&e.plan),
72            Curve3::Banked(b) => plan(&b.base.plan),
73            _ => Scalar::INFINITY,
74        },
75        StationCurve::Offset(_) => Scalar::INFINITY,
76    };
77    let moved = piece
78        .placement
79        .map_or(0.0, |rigid| rigid.translation.abs().max_element());
80    curve.max(moved)
81}
82
83impl<'c> FrameLaw<'_, 'c> {
84    /// How far the frame the kernel reads at `distance` along the basis,
85    /// from `side` of a seam or joint the station is on, lies from the
86    /// exact frame law's there, read as `reading`; `None` where it is not
87    /// certified (see [one station](super#one-station-405)).
88    #[must_use]
89    pub fn error_at(
90        &self,
91        distance: Scalar,
92        side: SeamSide,
93        reading: FrameReading,
94    ) -> Option<StationFrameError> {
95        match &self.basis {
96            LawBasis::Atomic {
97                curve,
98                placement,
99                placement_exact,
100                whole,
101            } => {
102                let owned;
103                let (piece, length) = match whole {
104                    Some(whole) => (whole, Some(whole.length())),
105                    None => {
106                        // A curve with no bounded length (a line plan) is
107                        // read on a span about the station.
108                        let (mut piece, length) = match StationPiece::whole(*curve) {
109                            Ok(whole) => (whole, Some(whole.length())),
110                            Err(_) => {
111                                let lo = (distance - 1.0).max(0.0);
112                                (
113                                    StationPiece::between(*curve, lo, distance + 1.0).ok()?,
114                                    None,
115                                )
116                            }
117                        };
118                        if let Some(rigid) = placement {
119                            piece = piece.placed(*rigid, *placement_exact);
120                        }
121                        owned = piece;
122                        (&owned, length)
123                    }
124                };
125                if !piece.placement_exact {
126                    return None;
127                }
128                let at = admitted(distance, length).ok()?;
129                let stand = Stand {
130                    at,
131                    into: at - piece.start,
132                    side,
133                    reading,
134                };
135                self.error_in(0, piece, None, stand, (0.0, [false; 2]))
136            }
137            LawBasis::Composite(composite) => {
138                let at = admitted(distance, Some(composite.length())).ok()?;
139                let (k, into) = composite.locate_admitted(distance, side).ok()?;
140                let piece = &composite.pieces()[k];
141                if !piece.placement_exact {
142                    return None;
143                }
144                let joint = self.joint_error(composite, k)?;
145                let ends = [k > 0, k + 1 < composite.pieces().len()];
146                let table = composite.tables()[k].as_ref();
147                let stand = Stand {
148                    at,
149                    into,
150                    side,
151                    reading,
152                };
153                self.error_in(k, piece, table, stand, (joint, ends))
154            }
155        }
156    }
157
158    /// The error of the frame read at `stand` on piece `k`; `joint` the
159    /// distance of the piece's start from the true joint and `ends`
160    /// whether its start and end are joints of a composite.
161    fn error_in(
162        &self,
163        k: usize,
164        piece: &StationPiece<'c>,
165        table: Option<&OffsetTable>,
166        stand: Stand,
167        (joint, ends): (Scalar, [bool; 2]),
168    ) -> Option<StationFrameError> {
169        let Stand {
170            at, side, reading, ..
171        } = stand;
172        let length = piece.length();
173        let u = stand.into.clamp(0.0, length);
174        let here = self.read(k, piece, u, u, table, reading)?;
175        let (e_p, e_a) = match self.slot(k, piece, table) {
176            Slot::Closed => {
177                let point = piece.section_with(u, side, table).ok()?.point;
178                // `POINT_ROUNDING` of the largest magnitude in the read:
179                // the point, the curve's data and the placement.
180                let scale = point.abs().max_element().max(data_scale(piece)).max(1.0);
181                (POINT_ROUNDING * scale, AXIS_ROUNDING)
182            }
183            Slot::Intrinsic(_) => (here.point_error, here.axis_error),
184            Slot::Read(_) | Slot::Unbounded => return None,
185        };
186        let delta = here.measure + joint;
187        let (mut s_point, mut s_axis) = (0.0, 0.0);
188        if delta > 0.0 {
189            let (mut lo, mut hi) = (u - 2.0 * delta, u + 2.0 * delta);
190            let (measure, _) = piece.read_at(u, side);
191            let on = slack(at).min(slack(measure));
192            let mut seams: Vec<Scalar> = piece.seams().ok()?.iter().map(|s| s.distance).collect();
193            if ends[0] {
194                seams.push(0.0);
195            }
196            if ends[1] {
197                seams.push(length);
198            }
199            for c in seams {
200                if (c - u).abs() <= on {
201                    // On the seam: the side named.
202                    match side {
203                        SeamSide::Incoming => hi = hi.min(c),
204                        _ => lo = lo.max(c),
205                    }
206                } else if (c - u).abs() <= 2.0 * delta {
207                    return None;
208                }
209            }
210            let span = self.read(k, piece, lo.max(0.0), hi.min(length), table, reading)?;
211            s_point = span.point[0];
212            s_axis = span.tangent[0].max(span.lateral[0]).max(span.up[0]);
213        }
214        let grow = 1.0 + ROUNDING;
215        let out = StationFrameError {
216            point: (e_p + delta * s_point) * grow,
217            axis: (e_a + delta * s_axis) * grow,
218        };
219        (out.point.is_finite() && out.axis.is_finite()).then_some(out)
220    }
221}