1use core::f64::consts::FRAC_PI_2;
45
46use axiolid_contracts::{GeomError, GeomResult};
47use axiolid_core::{Frame2, Point2, Scalar, Vec2};
48use axiolid_curve::{CurvatureLaw, ElevationLaw, Intrinsic2};
49
50use crate::arc_length::intrinsic_point;
51
52pub const INVERSION_TOLERANCE: Scalar = 1e-12;
55
56const MAX_INVERSION_STEPS: usize = 100;
60
61const MAX_BRACKET_ROUNDS: usize = 64;
63
64const MAX_CERTIFY_DEPTH: u32 = 24;
66
67const MAX_CERTIFY_PANELS: usize = 1 << 16;
69
70const ACCEPT: Scalar = 15.0 / 16.0;
73
74fn invalid(detail: &str) -> GeomError {
75 GeomError::InvalidInput(detail.to_owned())
76}
77
78pub fn elevation_height(law: &ElevationLaw, distance: Scalar) -> GeomResult<Scalar> {
93 match leaf(law, distance)? {
94 (
95 ElevationLaw::Intrinsic {
96 height,
97 grade,
98 curvature,
99 },
100 local,
101 ) => Ok(intrinsic_reading(*height, *grade, curvature, local)?.height),
102 (closed, local) => closed
106 .height_at(local)
107 .ok_or_else(|| invalid("elevation law has no height at that distance")),
108 }
109}
110
111pub fn elevation_grade(law: &ElevationLaw, distance: Scalar) -> GeomResult<Scalar> {
117 match leaf(law, distance)? {
118 (
119 ElevationLaw::Intrinsic {
120 height,
121 grade,
122 curvature,
123 },
124 local,
125 ) => Ok(intrinsic_reading(*height, *grade, curvature, local)?.grade),
126 (closed, local) => closed
127 .grade_at(local)
128 .ok_or_else(|| invalid("elevation law has no grade at that distance")),
129 }
130}
131
132fn leaf(law: &ElevationLaw, distance: Scalar) -> GeomResult<(&ElevationLaw, Scalar)> {
134 if !distance.is_finite() {
135 return Err(invalid("plan distance must be finite"));
136 }
137 law.piece_at(distance)
138 .ok_or_else(|| invalid("elevation law has no piece at that distance"))
139}
140
141struct Reading {
143 height: Scalar,
144 grade: Scalar,
145 arc: Scalar,
147}
148
149fn profile_curve(
152 height: Scalar,
153 grade: Scalar,
154 curvature: &CurvatureLaw,
155 span: Scalar,
156) -> Intrinsic2 {
157 let norm = grade.hypot(1.0);
158 let (sin0, cos0) = (grade / norm, 1.0 / norm);
159 Intrinsic2::new(
160 Frame2 {
161 origin: Point2::new(0.0, height),
162 x: Vec2::new(cos0, sin0),
163 y: Vec2::new(-sin0, cos0),
164 },
165 curvature.clone(),
166 span,
167 )
168}
169
170fn checked(height: Scalar, grade: Scalar, curvature: &CurvatureLaw) -> GeomResult<()> {
172 if !(height.is_finite() && grade.is_finite()) {
173 return Err(invalid(
174 "intrinsic profile needs a finite start height and grade",
175 ));
176 }
177 if !curvature.is_well_formed() {
178 return Err(invalid("intrinsic profile curvature law is malformed"));
179 }
180 Ok(())
181}
182
183fn intrinsic_reading(
185 height: Scalar,
186 grade: Scalar,
187 curvature: &CurvatureLaw,
188 d: Scalar,
189) -> GeomResult<Reading> {
190 checked(height, grade, curvature)?;
191 if d < 0.0 {
192 return Err(invalid(
193 "an intrinsic profile is defined forward from its start only",
194 ));
195 }
196 if d == 0.0 {
197 return Ok(Reading {
198 height,
199 grade,
200 arc: 0.0,
201 });
202 }
203 let theta0 = grade.atan();
204 let curve = profile_curve(height, grade, curvature, d);
205 let upper = bracket(&curve, theta0, d)?;
206
207 let (mut lo, mut hi) = (d, upper);
209 let mut s = d;
210 let tolerance = INVERSION_TOLERANCE * d.max(1.0);
211 for _ in 0..MAX_INVERSION_STEPS {
212 let point = intrinsic_point(&curve, s)?;
213 let theta = theta0 + heading(&curve, s)?;
214 let residual = point.x - d;
215 if residual.abs() <= tolerance {
216 let slope = theta.tan();
217 return Ok(Reading {
219 height: point.y - slope * residual,
220 grade: slope,
221 arc: s,
222 });
223 }
224 if residual < 0.0 {
225 lo = s;
226 } else {
227 hi = s;
228 }
229 let mut next = s - residual / theta.cos();
231 if !(next > lo && next < hi) {
232 next = 0.5 * (lo + hi);
233 }
234 if next == s {
235 break;
236 }
237 s = next;
238 }
239 Err(GeomError::BudgetExceeded {
240 resource: "intrinsic profile plan-distance inversion",
241 })
242}
243
244fn heading(curve: &Intrinsic2, s: Scalar) -> GeomResult<Scalar> {
246 curve
247 .heading_at(s)
248 .ok_or_else(|| invalid("curvature law does not integrate to that arc length"))
249}
250
251fn bracket(curve: &Intrinsic2, theta0: Scalar, d: Scalar) -> GeomResult<Scalar> {
262 cos_floor(curve, theta0, 0.0, d)?;
263 let tolerance = INVERSION_TOLERANCE * d.max(1.0);
264 let mut upper = d;
265 for _ in 0..MAX_BRACKET_ROUNDS {
266 let reached = intrinsic_point(curve, upper)?.x;
267 if reached >= d - tolerance {
268 return Ok(upper);
269 }
270 let cos = (theta0 + heading(curve, upper)?).cos();
271 let mut step = (d - reached) / cos * (1.0 + 1e-6) + Scalar::EPSILON * upper.max(1.0);
274 loop {
275 match cos_floor(curve, theta0, upper, upper + step) {
276 Ok(_) => break,
277 Err(error) => {
278 step *= 0.5;
279 if step <= Scalar::EPSILON * upper.max(1.0) {
280 return Err(error);
281 }
282 }
283 }
284 }
285 upper += step;
286 }
287 Err(GeomError::BudgetExceeded {
288 resource: "intrinsic profile plan-distance bracket",
289 })
290}
291
292fn cos_floor(curve: &Intrinsic2, theta0: Scalar, lo: Scalar, hi: Scalar) -> GeomResult<Scalar> {
306 let vertical = || invalid("the profile turns vertical before reaching that plan distance");
307 let mut floor: Scalar = 1.0;
308 let mut stack = vec![(lo, hi, 0_u32)];
309 let mut inspected = 0_usize;
310 while let Some((a, b, depth)) = stack.pop() {
311 inspected += 1;
312 if inspected > MAX_CERTIFY_PANELS {
313 return Err(GeomError::BudgetExceeded {
314 resource: "intrinsic profile below-vertical certificate",
315 });
316 }
317 let theta_a = theta0 + heading(curve, a)?;
318 if theta_a.is_nan() || theta_a.abs() >= FRAC_PI_2 {
319 return Err(vertical());
320 }
321 let shifted = curve
322 .curvature
323 .shifted(a)
324 .ok_or_else(|| invalid("curvature law cannot be read from that arc length"))?;
325 let variation = Intrinsic2::new(curve.start, shifted, b - a)
326 .turning_variation_bound(b - a)
327 .ok_or_else(|| invalid("curvature law does not integrate over the span"))?;
328 let reach = theta_a.abs() + variation;
329 let panel_floor = reach.cos();
330 let below_vertical = reach < FRAC_PI_2 && panel_floor > 0.0;
331 if below_vertical && (panel_floor >= ACCEPT * theta_a.cos() || depth >= MAX_CERTIFY_DEPTH) {
332 floor = floor.min(panel_floor);
333 continue;
334 }
335 if depth >= MAX_CERTIFY_DEPTH {
336 return Err(vertical());
337 }
338 let mid = 0.5 * (a + b);
339 stack.push((mid, b, depth + 1));
340 stack.push((a, mid, depth + 1));
341 }
342 Ok(floor)
343}
344
345#[must_use]
366pub fn elevation_chord_bound(law: &ElevationLaw, a: Scalar, b: Scalar) -> Option<Scalar> {
367 if !(a.is_finite() && b.is_finite()) {
368 return None;
369 }
370 let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
371 if lo == hi {
372 return Some(0.0);
373 }
374 let second = second_derivative_bound(law, lo, hi)?;
375 let h = hi - lo;
376 let bound = h * h * 0.125 * second;
377 Some(bound * (1.0 + 1e-12)).filter(|b| b.is_finite())
379}
380
381fn second_derivative_bound(law: &ElevationLaw, lo: Scalar, hi: Scalar) -> Option<Scalar> {
383 match law {
384 ElevationLaw::Polynomial { coefficients } => {
385 let m = lo.abs().max(hi.abs());
386 Some(
387 coefficients
388 .iter()
389 .enumerate()
390 .skip(2)
391 .map(|(i, c)| {
392 let i_f = i as Scalar;
393 i_f * (i_f - 1.0) * c.abs() * m.powi(i as i32 - 2)
394 })
395 .sum(),
396 )
397 .filter(|s: &Scalar| s.is_finite())
398 }
399 ElevationLaw::Piecewise { breaks, laws } => {
400 if laws.len() != breaks.len() + 1 {
401 return None;
402 }
403 let mid = 0.5 * (lo + hi);
405 let index = breaks.partition_point(|b| *b <= mid);
406 let start = if index == 0 { 0.0 } else { breaks[index - 1] };
407 let lower = if index == 0 {
408 Scalar::NEG_INFINITY
409 } else {
410 start
411 };
412 let upper = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
413 if lo < lower || hi > upper {
414 return None;
415 }
416 second_derivative_bound(laws.get(index)?, lo - start, hi - start)
417 }
418 ElevationLaw::CircularArc { grade, radius, .. } => {
419 if !(grade.is_finite() && radius.is_finite() && *radius != 0.0) {
420 return None;
421 }
422 let sin0 = grade / grade.hypot(1.0);
423 let reach = (sin0 + lo / radius).abs().max((sin0 + hi / radius).abs());
424 if reach.is_nan() || reach >= 1.0 {
425 return None;
426 }
427 let cos = ((1.0 - reach) * (1.0 + reach)).sqrt();
428 Some(1.0 / (radius.abs() * cos * cos * cos)).filter(|s| s.is_finite())
429 }
430 ElevationLaw::Intrinsic {
431 height,
432 grade,
433 curvature,
434 } => {
435 checked(*height, *grade, curvature).ok()?;
436 if lo < 0.0 {
437 return None;
438 }
439 let theta0 = grade.atan();
443 let curve = profile_curve(*height, *grade, curvature, hi);
444 let past = hi + 2.0 * INVERSION_TOLERANCE * hi.max(1.0);
447 let upper = bracket(&curve, theta0, past).ok()?;
448 let floor = cos_floor(&curve, theta0, lo, upper).ok()?;
449 let kappa = curvature_sup(&curvature.shifted(lo)?, upper - lo)?;
450 Some(kappa / (floor * floor * floor)).filter(|s| s.is_finite())
451 }
452 _ => None,
454 }
455}
456
457fn curvature_sup(law: &CurvatureLaw, span: Scalar) -> Option<Scalar> {
459 let polynomial = |coefficients: &[Scalar]| -> Scalar {
460 coefficients
461 .iter()
462 .enumerate()
463 .map(|(i, c)| c.abs() * span.powi(i as i32))
464 .sum()
465 };
466 let sup = match law {
467 CurvatureLaw::Constant { curvature } => curvature.abs(),
468 CurvatureLaw::Polynomial { coefficients } if coefficients.len() <= 2 => {
470 let start = coefficients.first().copied().unwrap_or(0.0);
471 let rate = coefficients.get(1).copied().unwrap_or(0.0);
472 start.abs().max((start + rate * span).abs())
473 }
474 CurvatureLaw::Polynomial { coefficients } => polynomial(coefficients),
475 CurvatureLaw::Sinusoid {
476 mean, amplitude, ..
477 } => mean.abs() + amplitude.abs(),
478 CurvatureLaw::Composite {
479 polynomial: terms,
480 harmonics,
481 } => polynomial(terms) + harmonics.iter().map(|h| h.amplitude.abs()).sum::<Scalar>(),
482 CurvatureLaw::Piecewise { breaks, laws } => {
483 if !law.is_well_formed() {
484 return None;
485 }
486 let mut sup: Scalar = 0.0;
487 let mut start = 0.0;
488 for (index, piece) in laws.iter().enumerate() {
489 let end = breaks.get(index).copied().unwrap_or(span).min(span);
490 sup = sup.max(curvature_sup(piece, (end - start).max(0.0))?);
491 if end >= span {
492 break;
493 }
494 start = end;
495 }
496 sup
497 }
498 _ => return None,
500 };
501 Some(sup).filter(|s| s.is_finite())
502}
503
504pub(crate) fn elevation_second(law: &ElevationLaw, distance: Scalar) -> GeomResult<Scalar> {
515 let value = match leaf(law, distance)? {
516 (ElevationLaw::Polynomial { coefficients }, d) => coefficients
517 .iter()
518 .enumerate()
519 .skip(2)
520 .rev()
521 .fold(0.0, |accumulated, (i, c)| {
522 accumulated * d + c * (i * (i - 1)) as Scalar
523 }),
524 (ElevationLaw::CircularArc { grade, radius, .. }, d) => {
525 let sin = arc_sin(*grade, *radius, d)
526 .ok_or_else(|| invalid("elevation law has no height at that distance"))?;
527 let cos = ((1.0 - sin) * (1.0 + sin)).sqrt();
528 1.0 / (radius * cos * cos * cos)
529 }
530 (
531 ElevationLaw::Intrinsic {
532 height,
533 grade,
534 curvature,
535 },
536 d,
537 ) => {
538 let reading = intrinsic_reading(*height, *grade, curvature, d)?;
539 let kappa = crate::frenet::law_value(curvature, reading.arc)
540 .ok_or_else(|| invalid("intrinsic profile curvature is not defined there"))?;
541 let secant = reading.grade.hypot(1.0);
542 kappa * secant * secant * secant
543 }
544 _ => {
545 return Err(invalid(
546 "elevation law family has no second derivative here",
547 ))
548 }
549 };
550 if value.is_finite() {
551 Ok(value)
552 } else {
553 Err(invalid("elevation second derivative is not finite"))
554 }
555}
556
557fn arc_sin(grade: Scalar, radius: Scalar, d: Scalar) -> Option<Scalar> {
559 if !(grade.is_finite() && radius.is_finite() && radius != 0.0 && d.is_finite()) {
560 return None;
561 }
562 let sin = grade / grade.hypot(1.0) + d / radius;
563 (sin.abs() < 1.0).then_some(sin)
564}
565
566pub(crate) fn profile_bounds(law: &ElevationLaw, lo: Scalar, hi: Scalar) -> Option<[Scalar; 3]> {
583 if !(lo.is_finite() && hi.is_finite()) || lo > hi {
584 return None;
585 }
586 let out = match law {
587 ElevationLaw::Polynomial { coefficients } => {
588 let m = lo.abs().max(hi.abs());
589 let term = |k: usize| -> Scalar {
590 coefficients
591 .iter()
592 .enumerate()
593 .skip(k)
594 .map(|(i, c)| {
595 let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
596 falling * c.abs() * m.powi((i - k) as i32)
597 })
598 .sum()
599 };
600 [term(1), term(2), term(3)]
601 }
602 ElevationLaw::Piecewise { breaks, laws } => {
603 if laws.len() != breaks.len() + 1 {
604 return None;
605 }
606 let mid = 0.5 * (lo + hi);
607 let index = breaks.partition_point(|b| *b <= mid);
608 let start = if index == 0 { 0.0 } else { breaks[index - 1] };
609 let lower = if index == 0 {
610 Scalar::NEG_INFINITY
611 } else {
612 start
613 };
614 let upper = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
615 if lo < lower || hi > upper {
616 return None;
617 }
618 return profile_bounds(laws.get(index)?, lo - start, hi - start);
619 }
620 ElevationLaw::CircularArc { grade, radius, .. } => {
621 if !(grade.is_finite() && radius.is_finite() && *radius != 0.0) {
622 return None;
623 }
624 let sin0 = grade / grade.hypot(1.0);
625 let reach = (sin0 + lo / radius).abs().max((sin0 + hi / radius).abs());
626 if reach.is_nan() || reach >= 1.0 {
627 return None;
628 }
629 let cos = ((1.0 - reach) * (1.0 + reach)).sqrt();
630 let r = radius.abs();
631 [
632 reach / cos,
633 1.0 / (r * cos.powi(3)),
634 3.0 * reach / (r * r * cos.powi(5)),
635 ]
636 }
637 ElevationLaw::Intrinsic {
638 height,
639 grade,
640 curvature,
641 } => {
642 checked(*height, *grade, curvature).ok()?;
643 if lo < 0.0 {
644 return None;
645 }
646 let theta0 = grade.atan();
647 let profile = profile_curve(*height, *grade, curvature, hi);
648 let past = hi + 2.0 * INVERSION_TOLERANCE * hi.max(1.0);
649 let upper = bracket(&profile, theta0, past).ok()?;
650 if curvature
653 .seams_within(upper)
654 .iter()
655 .any(|&seam| seam > lo && seam < upper)
656 {
657 return None;
658 }
659 let floor = cos_floor(&profile, theta0, lo, upper).ok()?;
660 let kappa = curvature_sup(&curvature.shifted(lo)?, upper - lo)?;
661 let rate = curvature_sup(&curvature.derivative().shifted(lo)?, upper - lo)?;
662 let sin = ((1.0 - floor) * (1.0 + floor)).max(0.0).sqrt();
663 [
664 sin / floor,
665 kappa / floor.powi(3),
666 rate / floor.powi(4) + 3.0 * kappa * kappa * sin / floor.powi(5),
667 ]
668 }
669 _ => return None,
670 };
671 let out = out.map(|v| v * (1.0 + 1e-12));
672 out.iter().all(|v| v.is_finite()).then_some(out)
673}
674
675pub(crate) fn profile_bounds4(law: &ElevationLaw, lo: Scalar, hi: Scalar) -> Option<[Scalar; 4]> {
684 if !(lo.is_finite() && hi.is_finite()) || lo > hi {
685 return None;
686 }
687 let out = match law {
688 ElevationLaw::Polynomial { coefficients } => {
689 let m = lo.abs().max(hi.abs());
690 let term = |k: usize| -> Scalar {
691 coefficients
692 .iter()
693 .enumerate()
694 .skip(k)
695 .map(|(i, c)| {
696 let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
697 falling * c.abs() * m.powi((i - k) as i32)
698 })
699 .sum()
700 };
701 [term(1), term(2), term(3), term(4)]
702 }
703 ElevationLaw::Piecewise { breaks, laws } => {
704 if laws.len() != breaks.len() + 1 {
705 return None;
706 }
707 let mid = 0.5 * (lo + hi);
708 let index = breaks.partition_point(|b| *b <= mid);
709 let start = if index == 0 { 0.0 } else { breaks[index - 1] };
710 let lower = if index == 0 {
711 Scalar::NEG_INFINITY
712 } else {
713 start
714 };
715 let upper = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
716 if lo < lower || hi > upper {
717 return None;
718 }
719 return profile_bounds4(laws.get(index)?, lo - start, hi - start);
720 }
721 ElevationLaw::CircularArc { grade, radius, .. } => {
722 if !(grade.is_finite() && radius.is_finite() && *radius != 0.0) {
723 return None;
724 }
725 let sin0 = grade / grade.hypot(1.0);
726 let reach = (sin0 + lo / radius).abs().max((sin0 + hi / radius).abs());
727 if reach.is_nan() || reach >= 1.0 {
728 return None;
729 }
730 let cos = ((1.0 - reach) * (1.0 + reach)).sqrt();
731 let r = radius.abs();
732 [
733 reach / cos,
734 1.0 / (r * cos.powi(3)),
735 3.0 * reach / (r * r * cos.powi(5)),
736 3.0 * (1.0 + 4.0 * reach * reach) / (r * r * r * cos.powi(7)),
737 ]
738 }
739 _ => return None,
740 };
741 let out = out.map(|v| v * (1.0 + 1e-12));
742 out.iter().all(|v| v.is_finite()).then_some(out)
743}
744
745pub(crate) fn elevation_seams(law: &ElevationLaw) -> Vec<Scalar> {
748 fn collect(law: &ElevationLaw, offset: Scalar, out: &mut Vec<Scalar>) {
749 if let ElevationLaw::Piecewise { breaks, laws } = law {
750 for (index, piece) in laws.iter().enumerate() {
751 let start = if index == 0 { 0.0 } else { breaks[index - 1] };
752 collect(piece, offset + start, out);
753 }
754 out.extend(breaks.iter().map(|b| offset + b));
755 }
756 }
757 let mut out = Vec::new();
758 if law.is_well_formed() {
759 collect(law, 0.0, &mut out);
760 }
761 out.sort_by(Scalar::total_cmp);
762 out.dedup();
763 out
764}
765
766pub(crate) fn elevation_grade_before(law: &ElevationLaw, distance: Scalar) -> GeomResult<Scalar> {
773 match law {
774 ElevationLaw::Piecewise { breaks, laws } => {
775 if laws.len() != breaks.len() + 1 {
776 return Err(invalid("elevation law piece list is malformed"));
777 }
778 let index = breaks.partition_point(|b| *b < distance);
779 let start = if index == 0 { 0.0 } else { breaks[index - 1] };
780 elevation_grade_before(&laws[index], distance - start)
781 }
782 other => elevation_grade(other, distance),
783 }
784}