Skip to main content

axioval_engine/
metric_routing.rs

1//! Source-neutral metric-routing evidence and host-service contracts.
2//!
3//! Geometry algorithms do not live here. A trusted Axiolid or alternate backend
4//! supplies this interface after adapting its native geometry into validated,
5//! source-qualified evidence.
6
7use std::sync::Arc;
8
9use crate::services::reviewable_exact_evidence;
10use crate::walkability::VerticalConnector;
11use axioval_ir::{Evidence, ObjectId};
12use thiserror::Error;
13
14/// Fail-closed metric routing errors.
15#[derive(Clone, Debug, Error, PartialEq, Eq)]
16pub enum MetricRoutingError {
17    /// A coordinate was NaN or infinite.
18    #[error("metric point coordinates must be finite")]
19    InvalidCoordinate,
20    /// A scalar length was negative, non-finite, or had reversed bounds.
21    #[error("metric length interval is invalid")]
22    InvalidLengthInterval,
23    /// A mobility dimension was negative or non-finite.
24    #[error("mobility profile contains an invalid dimension")]
25    InvalidMobilityProfile,
26    /// A route response omitted its path or traversed-object evidence.
27    #[error("metric route evidence is empty")]
28    EmptyRouteEvidence,
29    /// Route provenance was approximate or blank.
30    #[error("metric route provenance is not exact and reviewable")]
31    InexactRouteEvidence,
32    /// A blocked verdict did not prove complete obstacle/topology coverage.
33    #[error("metric evidence is incomplete")]
34    IncompleteMetricEvidence,
35    /// A backend returned a route for different endpoints than requested.
36    #[error("metric routing backend returned mismatched endpoints")]
37    ResponseEndpointMismatch,
38    /// Required geometry was not available for the named object.
39    #[error("metric geometry is unavailable for `{0}`")]
40    MissingGeometry(Box<ObjectId>),
41    /// The backend deliberately refused an unsupported or partial query.
42    #[error("metric routing query unavailable: {0}")]
43    Unavailable(String),
44    /// A many-target query named no target.
45    #[error("a metric routing query needs at least one target")]
46    NoTargets,
47    /// A farthest-point tolerance was negative or non-finite.
48    #[error("metric routing tolerance must be finite and non-negative")]
49    InvalidTolerance,
50    /// A backend answered with a target the request does not have, or
51    /// claimed convergence for an interval wider than the tolerance.
52    #[error("metric routing backend answered inconsistently with the request")]
53    InconsistentResponse,
54    /// A climb's vertical factor was negative or non-finite.
55    #[error("a climb's vertical factor must be finite and non-negative")]
56    InvalidClimb,
57    /// One connector was given twice with different kinds.
58    #[error("a vertical connector is given twice with different kinds")]
59    ConflictingConnector,
60    /// A travel cost factor was below one or non-finite.
61    #[error("a travel cost factor must be finite and at least one")]
62    InvalidCostFactor,
63}
64
65/// Three-valued result for comparing bounded evidence with a policy threshold.
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub enum ThresholdVerdict {
68    /// Every value in the interval meets the maximum.
69    Satisfied,
70    /// Every value in the interval exceeds the maximum.
71    Violated,
72    /// Bounds straddle the maximum, so policy evaluation must not guess.
73    Indeterminate,
74}
75
76/// Conservative bounds for a non-negative metric length in metres.
77#[derive(Clone, Copy, Debug, PartialEq)]
78pub struct LengthInterval {
79    lower_metres: f64,
80    upper_metres: f64,
81}
82
83impl LengthInterval {
84    /// Validates inclusive lower and upper distance bounds.
85    pub fn try_new(lower_metres: f64, upper_metres: f64) -> Result<Self, MetricRoutingError> {
86        if !valid_non_negative(lower_metres)
87            || !valid_non_negative(upper_metres)
88            || lower_metres > upper_metres
89        {
90            return Err(MetricRoutingError::InvalidLengthInterval);
91        }
92        Ok(Self {
93            lower_metres,
94            upper_metres,
95        })
96    }
97
98    /// Creates a zero-error interval.
99    pub fn exact(metres: f64) -> Result<Self, MetricRoutingError> {
100        Self::try_new(metres, metres)
101    }
102
103    /// Inclusive lower bound in metres.
104    pub fn lower_metres(&self) -> f64 {
105        self.lower_metres
106    }
107
108    /// Inclusive upper bound in metres.
109    pub fn upper_metres(&self) -> f64 {
110        self.upper_metres
111    }
112
113    /// Whether the interval proves one exact value.
114    #[allow(clippy::float_cmp)]
115    pub fn is_exact(&self) -> bool {
116        // The exact constructor writes the same validated scalar to both fields;
117        // this tests evidence identity, not numerical convergence.
118        self.lower_metres == self.upper_metres
119    }
120
121    /// Compares this interval to an inclusive maximum without collapsing uncertainty.
122    pub fn compare_maximum(
123        &self,
124        maximum_metres: f64,
125    ) -> Result<ThresholdVerdict, MetricRoutingError> {
126        if !valid_non_negative(maximum_metres) {
127            return Err(MetricRoutingError::InvalidLengthInterval);
128        }
129        if self.upper_metres <= maximum_metres {
130            Ok(ThresholdVerdict::Satisfied)
131        } else if self.lower_metres > maximum_metres {
132            Ok(ThresholdVerdict::Violated)
133        } else {
134            Ok(ThresholdVerdict::Indeterminate)
135        }
136    }
137}
138
139/// A source-qualified object-grounded point expressed in canonical metres.
140#[derive(Clone, Debug, PartialEq)]
141pub struct MetricPoint {
142    subject: ObjectId,
143    coordinates_metres: [f64; 3],
144}
145
146impl MetricPoint {
147    /// Validates a model-grounded point.
148    pub fn try_new(
149        subject: ObjectId,
150        coordinates_metres: [f64; 3],
151    ) -> Result<Self, MetricRoutingError> {
152        if !coordinates_metres.iter().all(|value| value.is_finite()) {
153            return Err(MetricRoutingError::InvalidCoordinate);
154        }
155        Ok(Self {
156            subject,
157            coordinates_metres,
158        })
159    }
160
161    /// Object grounding this point.
162    pub fn subject(&self) -> &ObjectId {
163        &self.subject
164    }
165
166    /// Canonical coordinates in metres.
167    pub fn coordinates_metres(&self) -> [f64; 3] {
168        self.coordinates_metres
169    }
170}
171
172/// Geometry-independent mobility envelope used by route providers.
173#[derive(Clone, Copy, Debug, PartialEq)]
174pub struct MobilityProfile {
175    radius_metres: f64,
176    height_metres: f64,
177    maximum_step_metres: f64,
178    maximum_slope: f64,
179}
180
181impl MobilityProfile {
182    /// Validates non-negative finite mobility dimensions.
183    pub fn try_new(
184        radius_metres: f64,
185        height_metres: f64,
186        maximum_step_metres: f64,
187        maximum_slope: f64,
188    ) -> Result<Self, MetricRoutingError> {
189        if ![
190            radius_metres,
191            height_metres,
192            maximum_step_metres,
193            maximum_slope,
194        ]
195        .into_iter()
196        .all(valid_non_negative)
197        {
198            return Err(MetricRoutingError::InvalidMobilityProfile);
199        }
200        Ok(Self {
201            radius_metres,
202            height_metres,
203            maximum_step_metres,
204            maximum_slope,
205        })
206    }
207
208    /// Agent radius in metres.
209    pub fn radius_metres(&self) -> f64 {
210        self.radius_metres
211    }
212
213    /// Required clear height in metres.
214    pub fn height_metres(&self) -> f64 {
215        self.height_metres
216    }
217
218    /// Maximum traversable step in metres.
219    pub fn maximum_step_metres(&self) -> f64 {
220        self.maximum_step_metres
221    }
222
223    /// Maximum dimensionless slope ratio.
224    pub fn maximum_slope(&self) -> f64 {
225        self.maximum_slope
226    }
227}
228
229/// How a climb through a stair or ramp is measured.
230#[derive(Clone, Copy, Debug, PartialEq, Eq)]
231pub enum StairLength {
232    /// Along the slope: `sqrt(h² + (f·v)²)` for a climb `h` long in plan
233    /// and `v` high, with the vertical factor `f`.
234    Slope,
235    /// The horizontal length plus the rise times the vertical factor:
236    /// `h + f·v`.
237    HorizontalPlusVertical,
238}
239
240impl StairLength {
241    /// The measure's name in rule parameters: `slope` or
242    /// `horizontal-plus-vertical`.
243    #[must_use]
244    pub fn as_str(self) -> &'static str {
245        match self {
246            Self::Slope => "slope",
247            Self::HorizontalPlusVertical => "horizontal-plus-vertical",
248        }
249    }
250}
251
252/// How much a climb through a vertical connector adds to a route's length.
253#[derive(Clone, Copy, Debug, PartialEq)]
254pub struct ClimbLength {
255    measure: StairLength,
256    vertical_factor: f64,
257}
258
259impl ClimbLength {
260    /// Validates a finite, non-negative vertical factor.
261    ///
262    /// # Errors
263    ///
264    /// [`MetricRoutingError::InvalidClimb`] otherwise.
265    pub fn try_new(measure: StairLength, vertical_factor: f64) -> Result<Self, MetricRoutingError> {
266        if !valid_non_negative(vertical_factor) {
267            return Err(MetricRoutingError::InvalidClimb);
268        }
269        Ok(Self {
270            measure,
271            vertical_factor,
272        })
273    }
274
275    /// The true slope length: [`StairLength::Slope`] with a factor of one.
276    #[must_use]
277    pub fn slope() -> Self {
278        Self {
279            measure: StairLength::Slope,
280            vertical_factor: 1.0,
281        }
282    }
283
284    /// How the climb is measured.
285    #[must_use]
286    pub fn measure(&self) -> StairLength {
287        self.measure
288    }
289
290    /// What a metre of rise counts.
291    #[must_use]
292    pub fn vertical_factor(&self) -> f64 {
293        self.vertical_factor
294    }
295
296    /// Bounds the length of a climb whose plan length and rise lie in the
297    /// given intervals. Both measures grow with either, so the bounds come
298    /// from the ends, rounded outwards.
299    #[must_use]
300    pub fn length(&self, horizontal: LengthInterval, rise: LengthInterval) -> LengthInterval {
301        let at = |h: f64, v: f64| match self.measure {
302            StairLength::Slope => h.hypot(self.vertical_factor * v),
303            StairLength::HorizontalPlusVertical => self.vertical_factor.mul_add(v, h),
304        };
305        let lower = at(horizontal.lower_metres(), rise.lower_metres());
306        let upper = at(horizontal.upper_metres(), rise.upper_metres());
307        LengthInterval {
308            lower_metres: (lower * (1.0 - CLIMB_ROUNDING)).max(0.0),
309            upper_metres: upper * (1.0 + CLIMB_ROUNDING),
310        }
311    }
312}
313
314/// Relative allowance for the rounding of a climb's length.
315const CLIMB_ROUNDING: f64 = 4.0 * f64::EPSILON;
316
317/// The vertical connectors a route may climb through, and how a climb
318/// counts.
319///
320/// A route enters and leaves a connector at its **landings**, the two ends
321/// of its walking line, and counts the climb between them by
322/// [`ClimbLength`]. A request carrying this routes through these
323/// connectors only: any other connector is no way between levels for it.
324/// A backend that cannot prove a connector's length or passability leaves
325/// every route through it undecided, never shorter and never blocked.
326#[derive(Clone, Debug, PartialEq)]
327pub struct ConnectorRouting {
328    connectors: Vec<VerticalConnector>,
329    climb: ClimbLength,
330}
331
332impl ConnectorRouting {
333    /// The connectors (sorted, deduplicated) and the climb length.
334    ///
335    /// # Errors
336    ///
337    /// [`MetricRoutingError::ConflictingConnector`] when one object is
338    /// given two kinds.
339    pub fn try_new(
340        mut connectors: Vec<VerticalConnector>,
341        climb: ClimbLength,
342    ) -> Result<Self, MetricRoutingError> {
343        connectors.sort();
344        connectors.dedup();
345        if connectors
346            .windows(2)
347            .any(|pair| pair[0].object() == pair[1].object())
348        {
349            return Err(MetricRoutingError::ConflictingConnector);
350        }
351        Ok(Self { connectors, climb })
352    }
353
354    /// The connectors a route may climb through, sorted.
355    #[must_use]
356    pub fn connectors(&self) -> &[VerticalConnector] {
357        &self.connectors
358    }
359
360    /// How a climb counts.
361    #[must_use]
362    pub fn climb(&self) -> ClimbLength {
363        self.climb
364    }
365}
366
367/// Travel over an object counted by a factor: a metre walked over its
368/// plan footprint counts `factor` metres.
369///
370/// Where footprints overlap the greatest factor counts, and along a
371/// footprint's edge the cheaper side does. A walk under a stair lies over
372/// it, as for [`PathTraceRequest`].
373///
374/// A cost weighs only walks on its object's own level: the level it lies
375/// in, or every level it spans. A walk on a floor above or below the
376/// object is not weighed by it. Across levels ([`ConnectorRouting`]) a
377/// climb counts its length times a factor between one and the largest
378/// factor of a cost meeting its connector: the answer's lower bound takes
379/// one, its upper bound the largest. A backend that cannot tell an
380/// object's level refuses the request rather than weigh a level the
381/// object is not on.
382#[derive(Clone, Debug, PartialEq)]
383pub struct TravelCost {
384    object: ObjectId,
385    factor: f64,
386}
387
388impl TravelCost {
389    /// Validates a finite factor of at least one.
390    ///
391    /// # Errors
392    ///
393    /// [`MetricRoutingError::InvalidCostFactor`] otherwise.
394    pub fn try_new(object: ObjectId, factor: f64) -> Result<Self, MetricRoutingError> {
395        if !(factor.is_finite() && factor >= 1.0) {
396            return Err(MetricRoutingError::InvalidCostFactor);
397        }
398        Ok(Self { object, factor })
399    }
400
401    /// The object whose plan footprint costs more.
402    #[must_use]
403    pub fn object(&self) -> &ObjectId {
404        &self.object
405    }
406
407    /// What a metre over it counts.
408    #[must_use]
409    pub fn factor(&self) -> f64 {
410        self.factor
411    }
412}
413
414/// Sorts costs by object, keeps each object's greatest factor and drops a
415/// factor of one, which changes nothing.
416fn settled(mut costs: Vec<TravelCost>) -> Vec<TravelCost> {
417    costs.retain(|cost| cost.factor > 1.0);
418    costs.sort_by(|a, b| a.object.cmp(&b.object).then(b.factor.total_cmp(&a.factor)));
419    costs.dedup_by(|later, earlier| later.object == earlier.object);
420    costs
421}
422
423/// One source-neutral metric routing request.
424#[derive(Clone, Debug, PartialEq)]
425pub struct MetricRouteRequest {
426    origin: MetricPoint,
427    destination: MetricPoint,
428    profile: MobilityProfile,
429    connectors: Option<ConnectorRouting>,
430}
431
432impl MetricRouteRequest {
433    /// Creates a request from already validated values.
434    pub fn new(origin: MetricPoint, destination: MetricPoint, profile: MobilityProfile) -> Self {
435        Self {
436            origin,
437            destination,
438            profile,
439            connectors: None,
440        }
441    }
442
443    /// The same request, climbing through `connectors` between levels
444    /// (see [`ConnectorRouting`]). Only a backend that [climbs
445    /// connectors](MetricRoutingService::climbs_connectors) is asked.
446    #[must_use]
447    pub fn with_connectors(mut self, connectors: ConnectorRouting) -> Self {
448        self.connectors = Some(connectors);
449        self
450    }
451
452    /// The connectors a route may climb through; `None` for a request that
453    /// leaves them to the backend.
454    pub fn connectors(&self) -> Option<&ConnectorRouting> {
455        self.connectors.as_ref()
456    }
457
458    /// Route origin.
459    pub fn origin(&self) -> &MetricPoint {
460        &self.origin
461    }
462
463    /// Route destination.
464    pub fn destination(&self) -> &MetricPoint {
465        &self.destination
466    }
467
468    /// Mobility envelope.
469    pub fn profile(&self) -> MobilityProfile {
470        self.profile
471    }
472}
473
474/// Provenance proving complete topology and obstacle coverage for a negative verdict.
475#[derive(Clone, Debug, PartialEq, Eq)]
476pub struct CompleteMetricEvidence(Evidence);
477
478impl CompleteMetricEvidence {
479    /// Promotes only exact, reviewable completeness evidence.
480    pub fn try_new(evidence: Evidence) -> Result<Self, MetricRoutingError> {
481        if !reviewable_exact_evidence(&evidence) {
482            return Err(MetricRoutingError::IncompleteMetricEvidence);
483        }
484        Ok(Self(evidence))
485    }
486
487    /// Completeness provenance.
488    pub fn evidence(&self) -> &Evidence {
489        &self.0
490    }
491}
492
493/// A negative route verdict bound to the exact request and complete evidence.
494#[derive(Clone, Debug, PartialEq)]
495pub struct BlockedMetricRouteEvidence {
496    request: MetricRouteRequest,
497    completeness: CompleteMetricEvidence,
498}
499
500impl BlockedMetricRouteEvidence {
501    /// Binds complete topology and obstacle evidence to one request.
502    pub fn new(request: MetricRouteRequest, completeness: CompleteMetricEvidence) -> Self {
503        Self {
504            request,
505            completeness,
506        }
507    }
508
509    /// Request proven blocked.
510    pub fn request(&self) -> &MetricRouteRequest {
511        &self.request
512    }
513
514    /// Exact completeness provenance.
515    pub fn completeness(&self) -> &CompleteMetricEvidence {
516        &self.completeness
517    }
518}
519
520/// A known route and conservative shortest-distance bounds.
521#[derive(Clone, Debug, PartialEq)]
522pub struct MetricRouteEvidence {
523    shortest_distance: LengthInterval,
524    waypoints: Vec<MetricPoint>,
525    traversed_objects: Vec<ObjectId>,
526    evidence: Evidence,
527}
528
529impl MetricRouteEvidence {
530    /// Validates known-route evidence without upgrading bounded distance to exact.
531    pub fn try_new(
532        shortest_distance: LengthInterval,
533        waypoints: Vec<MetricPoint>,
534        traversed_objects: Vec<ObjectId>,
535        evidence: Evidence,
536    ) -> Result<Self, MetricRoutingError> {
537        if waypoints.is_empty() || traversed_objects.is_empty() {
538            return Err(MetricRoutingError::EmptyRouteEvidence);
539        }
540        if !reviewable_exact_evidence(&evidence) {
541            return Err(MetricRoutingError::InexactRouteEvidence);
542        }
543        Ok(Self {
544            shortest_distance,
545            waypoints,
546            traversed_objects,
547            evidence,
548        })
549    }
550
551    /// Conservative shortest-distance bounds.
552    pub fn shortest_distance(&self) -> &LengthInterval {
553        &self.shortest_distance
554    }
555
556    /// Object-grounded route points in traversal order.
557    pub fn waypoints(&self) -> &[MetricPoint] {
558        &self.waypoints
559    }
560
561    /// Source-qualified objects traversed by the route.
562    pub fn traversed_objects(&self) -> &[ObjectId] {
563        &self.traversed_objects
564    }
565
566    /// Route computation provenance.
567    pub fn evidence(&self) -> &Evidence {
568        &self.evidence
569    }
570}
571
572/// Evaluated route result. Backend incompleteness is an error, not a third verdict.
573#[derive(Clone, Debug, PartialEq)]
574pub enum MetricRouteOutcome {
575    /// At least one route exists; the distance may remain conservatively bounded.
576    Reachable(MetricRouteEvidence),
577    /// No route exists under exact, complete topology and obstacle evidence.
578    Blocked(BlockedMetricRouteEvidence),
579}
580
581/// The distance from one point to the nearest of several targets.
582///
583/// With [`Self::with_avoided`], every walk keeps out of the named objects:
584/// each is an obstacle wherever its body stands in the walking band, even a
585/// surface or portal the backend would otherwise walk on or through. The
586/// answer then bounds the shortest walk avoiding them all, so a lower bound
587/// beyond the plain walk's upper bound proves that every shortest walk
588/// enters one of them. Only a backend that [avoids
589/// objects](MetricRoutingService::avoids_objects) is asked.
590///
591/// With [`Self::with_costs`], the distance is the least weighted cost of a
592/// walk (see [`TravelCost`]), and the answer's route is a walk whose
593/// weighted cost is at most its upper bound. Only a backend that [weighs
594/// travel](MetricRoutingService::weighs_travel) is asked.
595#[derive(Clone, Debug, PartialEq)]
596pub struct NearestTargetRequest {
597    origin: MetricPoint,
598    targets: Vec<MetricPoint>,
599    profile: MobilityProfile,
600    avoided: Vec<ObjectId>,
601    connectors: Option<ConnectorRouting>,
602    costs: Vec<TravelCost>,
603}
604
605impl NearestTargetRequest {
606    /// Creates a request; targets keep their order, which answers index.
607    pub fn try_new(
608        origin: MetricPoint,
609        targets: Vec<MetricPoint>,
610        profile: MobilityProfile,
611    ) -> Result<Self, MetricRoutingError> {
612        if targets.is_empty() {
613            return Err(MetricRoutingError::NoTargets);
614        }
615        Ok(Self {
616            origin,
617            targets,
618            profile,
619            avoided: Vec::new(),
620            connectors: None,
621            costs: Vec::new(),
622        })
623    }
624
625    /// The same request, counting travel over objects by their factors
626    /// (sorted by object, each object's greatest factor kept, factors of
627    /// one dropped).
628    #[must_use]
629    pub fn with_costs(mut self, costs: Vec<TravelCost>) -> Self {
630        self.costs = settled(costs);
631        self
632    }
633
634    /// The costs travel counts by, sorted by object; empty for plain
635    /// length.
636    pub fn costs(&self) -> &[TravelCost] {
637        &self.costs
638    }
639
640    /// The same request, walking around `avoided` (sorted, deduplicated).
641    /// An avoided connector is not climbed.
642    #[must_use]
643    pub fn with_avoided(mut self, mut avoided: Vec<ObjectId>) -> Self {
644        avoided.sort();
645        avoided.dedup();
646        self.avoided = avoided;
647        self
648    }
649
650    /// The same request, climbing through `connectors` between levels
651    /// (see [`ConnectorRouting`]).
652    #[must_use]
653    pub fn with_connectors(mut self, connectors: ConnectorRouting) -> Self {
654        self.connectors = Some(connectors);
655        self
656    }
657
658    /// The connectors a route may climb through; `None` for a request that
659    /// leaves them to the backend.
660    pub fn connectors(&self) -> Option<&ConnectorRouting> {
661        self.connectors.as_ref()
662    }
663
664    /// Where every route starts.
665    pub fn origin(&self) -> &MetricPoint {
666        &self.origin
667    }
668
669    /// The targets, in request order.
670    pub fn targets(&self) -> &[MetricPoint] {
671        &self.targets
672    }
673
674    /// Mobility envelope.
675    pub fn profile(&self) -> MobilityProfile {
676        self.profile
677    }
678
679    /// The objects every walk keeps out of, sorted; empty for a plain walk.
680    pub fn avoided(&self) -> &[ObjectId] {
681        &self.avoided
682    }
683}
684
685/// Bounds on the distance to the nearest target, and a route that realises
686/// the upper bound.
687#[derive(Clone, Debug, PartialEq)]
688pub struct NearestTargetEvidence {
689    target: usize,
690    shortest_distance: LengthInterval,
691    waypoints: Vec<MetricPoint>,
692    evidence: Evidence,
693}
694
695impl NearestTargetEvidence {
696    /// Validates the answer: `target` indexes the request's targets and is
697    /// the one the waypoints reach; `shortest_distance` bounds the distance
698    /// to the nearest of all targets, which may be another one.
699    pub fn try_new(
700        target: usize,
701        shortest_distance: LengthInterval,
702        waypoints: Vec<MetricPoint>,
703        evidence: Evidence,
704    ) -> Result<Self, MetricRoutingError> {
705        if waypoints.is_empty() {
706            return Err(MetricRoutingError::EmptyRouteEvidence);
707        }
708        if !reviewable_exact_evidence(&evidence) {
709            return Err(MetricRoutingError::InexactRouteEvidence);
710        }
711        Ok(Self {
712            target,
713            shortest_distance,
714            waypoints,
715            evidence,
716        })
717    }
718
719    /// Index of the target the route reaches.
720    pub fn target(&self) -> usize {
721        self.target
722    }
723
724    /// Conservative bounds on the distance to the nearest target.
725    pub fn shortest_distance(&self) -> &LengthInterval {
726        &self.shortest_distance
727    }
728
729    /// The route, from the origin to [`Self::target`].
730    pub fn waypoints(&self) -> &[MetricPoint] {
731        &self.waypoints
732    }
733
734    /// Measurement provenance.
735    pub fn evidence(&self) -> &Evidence {
736        &self.evidence
737    }
738}
739
740/// No target is reachable from the origin, under complete exact evidence.
741#[derive(Clone, Debug, PartialEq)]
742pub struct UnreachableTargetsEvidence {
743    request: NearestTargetRequest,
744    completeness: CompleteMetricEvidence,
745}
746
747impl UnreachableTargetsEvidence {
748    /// Binds complete evidence to one request.
749    pub fn new(request: NearestTargetRequest, completeness: CompleteMetricEvidence) -> Self {
750        Self {
751            request,
752            completeness,
753        }
754    }
755
756    /// Request proven unreachable.
757    pub fn request(&self) -> &NearestTargetRequest {
758        &self.request
759    }
760
761    /// Exact completeness provenance.
762    pub fn completeness(&self) -> &CompleteMetricEvidence {
763        &self.completeness
764    }
765}
766
767/// Answer to a [`NearestTargetRequest`].
768#[derive(Clone, Debug, PartialEq)]
769pub enum NearestTargetOutcome {
770    /// Some target is reachable; the nearest distance is bounded.
771    Reached(NearestTargetEvidence),
772    /// No target is reachable, with complete evidence.
773    Unreachable(UnreachableTargetsEvidence),
774}
775
776/// The largest distance from any point of a region to the nearest of
777/// several targets.
778///
779/// The region is an object's walkable area: the points of its plan, on its
780/// floor, that the mobility profile leaves free. Only those points count;
781/// a point inside an obstacle is none of the region's.
782///
783/// With [`Self::with_costs`], distances are weighted as for a
784/// [`NearestTargetRequest`]; only a backend that [weighs
785/// travel](MetricRoutingService::weighs_travel) is asked.
786#[derive(Clone, Debug, PartialEq)]
787pub struct FarthestPointRequest {
788    region: ObjectId,
789    targets: Vec<MetricPoint>,
790    profile: MobilityProfile,
791    tolerance_metres: f64,
792    connectors: Option<ConnectorRouting>,
793    costs: Vec<TravelCost>,
794}
795
796impl FarthestPointRequest {
797    /// Creates a request. `tolerance_metres` is how narrow the interval
798    /// should become; a backend may answer wider without claiming
799    /// convergence, never narrower than the truth.
800    pub fn try_new(
801        region: ObjectId,
802        targets: Vec<MetricPoint>,
803        profile: MobilityProfile,
804        tolerance_metres: f64,
805    ) -> Result<Self, MetricRoutingError> {
806        if targets.is_empty() {
807            return Err(MetricRoutingError::NoTargets);
808        }
809        if !valid_non_negative(tolerance_metres) {
810            return Err(MetricRoutingError::InvalidTolerance);
811        }
812        Ok(Self {
813            region,
814            targets,
815            profile,
816            tolerance_metres,
817            connectors: None,
818            costs: Vec::new(),
819        })
820    }
821
822    /// The same request, counting travel over objects by their factors
823    /// (sorted by object, each object's greatest factor kept, factors of
824    /// one dropped).
825    #[must_use]
826    pub fn with_costs(mut self, costs: Vec<TravelCost>) -> Self {
827        self.costs = settled(costs);
828        self
829    }
830
831    /// The costs travel counts by, sorted by object; empty for plain
832    /// length.
833    pub fn costs(&self) -> &[TravelCost] {
834        &self.costs
835    }
836
837    /// The same request, climbing through `connectors` between levels
838    /// (see [`ConnectorRouting`]).
839    #[must_use]
840    pub fn with_connectors(mut self, connectors: ConnectorRouting) -> Self {
841        self.connectors = Some(connectors);
842        self
843    }
844
845    /// The connectors a route may climb through; `None` for a request that
846    /// leaves them to the backend.
847    pub fn connectors(&self) -> Option<&ConnectorRouting> {
848        self.connectors.as_ref()
849    }
850
851    /// The object whose walkable area is measured.
852    pub fn region(&self) -> &ObjectId {
853        &self.region
854    }
855
856    /// The targets, in request order.
857    pub fn targets(&self) -> &[MetricPoint] {
858        &self.targets
859    }
860
861    /// Mobility envelope.
862    pub fn profile(&self) -> MobilityProfile {
863        self.profile
864    }
865
866    /// Requested interval width in metres.
867    pub fn tolerance_metres(&self) -> f64 {
868        self.tolerance_metres
869    }
870}
871
872/// A certified bracket on the largest distance to the nearest target.
873#[derive(Clone, Debug, PartialEq)]
874pub struct FarthestPointEvidence {
875    distance: LengthInterval,
876    witness: MetricPoint,
877    converged: bool,
878    evidence: Evidence,
879}
880
881impl FarthestPointEvidence {
882    /// Validates the bracket.
883    ///
884    /// `distance` contains the largest distance over the region; `witness`
885    /// is a point of the region whose distance to every target is at least
886    /// `distance`'s lower bound. `converged` claims the interval is no wider
887    /// than the requested tolerance; the handle checks the claim.
888    pub fn try_new(
889        distance: LengthInterval,
890        witness: MetricPoint,
891        converged: bool,
892        evidence: Evidence,
893    ) -> Result<Self, MetricRoutingError> {
894        if !reviewable_exact_evidence(&evidence) {
895            return Err(MetricRoutingError::InexactRouteEvidence);
896        }
897        Ok(Self {
898            distance,
899            witness,
900            converged,
901            evidence,
902        })
903    }
904
905    /// Bounds on the largest distance to the nearest target.
906    pub fn distance(&self) -> &LengthInterval {
907        &self.distance
908    }
909
910    /// A point of the region at least the lower bound from every target.
911    pub fn witness(&self) -> &MetricPoint {
912        &self.witness
913    }
914
915    /// Whether the interval is no wider than the requested tolerance.
916    pub fn converged(&self) -> bool {
917        self.converged
918    }
919
920    /// Measurement provenance.
921    pub fn evidence(&self) -> &Evidence {
922        &self.evidence
923    }
924}
925
926/// Part of the region reaches no target, under complete exact evidence, so
927/// the largest distance is unbounded.
928#[derive(Clone, Debug, PartialEq)]
929pub struct UnreachableRegionEvidence {
930    request: FarthestPointRequest,
931    witness: MetricPoint,
932    completeness: CompleteMetricEvidence,
933}
934
935impl UnreachableRegionEvidence {
936    /// Binds a point of the region no target reaches, and complete
937    /// evidence, to one request.
938    pub fn new(
939        request: FarthestPointRequest,
940        witness: MetricPoint,
941        completeness: CompleteMetricEvidence,
942    ) -> Self {
943        Self {
944            request,
945            witness,
946            completeness,
947        }
948    }
949
950    /// Request proven to have an unreachable part.
951    pub fn request(&self) -> &FarthestPointRequest {
952        &self.request
953    }
954
955    /// A point of the region from which no target is reachable.
956    pub fn witness(&self) -> &MetricPoint {
957        &self.witness
958    }
959
960    /// Exact completeness provenance.
961    pub fn completeness(&self) -> &CompleteMetricEvidence {
962        &self.completeness
963    }
964}
965
966/// Answer to a [`FarthestPointRequest`].
967#[derive(Clone, Debug, PartialEq)]
968pub enum FarthestPointOutcome {
969    /// Every point of the region reaches a target; the largest distance is
970    /// bracketed.
971    Bounded(FarthestPointEvidence),
972    /// Some point of the region reaches no target.
973    Unreachable(UnreachableRegionEvidence),
974}
975
976/// How much of a walked polyline lies over each of several objects.
977///
978/// The polyline is a route's waypoints, such as a nearest-target answer's;
979/// its length and every part of it are measured in plan, as routes are. A
980/// part lies over an object where it lies inside the object's plan
981/// footprint, whatever the heights: a walk under a stair lies over it.
982#[derive(Clone, Debug, PartialEq)]
983pub struct PathTraceRequest {
984    waypoints: Vec<MetricPoint>,
985    objects: Vec<ObjectId>,
986}
987
988impl PathTraceRequest {
989    /// Creates a request; the objects are sorted and deduplicated, and the
990    /// answer follows that order.
991    pub fn try_new(
992        waypoints: Vec<MetricPoint>,
993        mut objects: Vec<ObjectId>,
994    ) -> Result<Self, MetricRoutingError> {
995        if waypoints.is_empty() {
996            return Err(MetricRoutingError::EmptyRouteEvidence);
997        }
998        objects.sort();
999        objects.dedup();
1000        Ok(Self { waypoints, objects })
1001    }
1002
1003    /// The polyline, in walking order.
1004    pub fn waypoints(&self) -> &[MetricPoint] {
1005        &self.waypoints
1006    }
1007
1008    /// The objects measured, sorted.
1009    pub fn objects(&self) -> &[ObjectId] {
1010        &self.objects
1011    }
1012
1013    /// The polyline's length in plan, in metres.
1014    pub fn plan_length_metres(&self) -> f64 {
1015        self.waypoints
1016            .windows(2)
1017            .map(|pair| {
1018                let ([ax, ay, _], [bx, by, _]) =
1019                    (pair[0].coordinates_metres(), pair[1].coordinates_metres());
1020                (bx - ax).hypot(by - ay)
1021            })
1022            .sum()
1023    }
1024}
1025
1026/// The length of a polyline over each requested object, in request order.
1027///
1028/// Each length is an interval: its upper bound counts every part that may
1029/// lie over the object, along the boundary of its footprint included; its
1030/// lower bound only the parts surely inside. An object whose footprint is
1031/// unknown answers why instead.
1032#[derive(Clone, Debug, PartialEq)]
1033pub struct PathTrace {
1034    lengths: Vec<Result<LengthInterval, String>>,
1035    evidence: Evidence,
1036}
1037
1038impl PathTrace {
1039    /// Validates exact, reviewable provenance.
1040    pub fn try_new(
1041        lengths: Vec<Result<LengthInterval, String>>,
1042        evidence: Evidence,
1043    ) -> Result<Self, MetricRoutingError> {
1044        if !reviewable_exact_evidence(&evidence) {
1045            return Err(MetricRoutingError::InexactRouteEvidence);
1046        }
1047        Ok(Self { lengths, evidence })
1048    }
1049
1050    /// The length over each object, in request order, or why it is unknown.
1051    pub fn lengths(&self) -> &[Result<LengthInterval, String>] {
1052        &self.lengths
1053    }
1054
1055    /// Measurement provenance.
1056    pub fn evidence(&self) -> &Evidence {
1057        &self.evidence
1058    }
1059}
1060
1061/// The shortest walk from a point to the nearest of several targets that
1062/// enters an object's plan footprint (touching it counts).
1063///
1064/// A lower bound beyond the upper bound of the plain walk's length proves
1065/// that no shortest walk enters the object. With [`Self::with_avoided`]
1066/// every walk keeps out of the named objects, as for a
1067/// [`NearestTargetRequest`].
1068#[derive(Clone, Debug, PartialEq)]
1069pub struct ForcedWalkRequest {
1070    origin: MetricPoint,
1071    targets: Vec<MetricPoint>,
1072    through: ObjectId,
1073    profile: MobilityProfile,
1074    tolerance_metres: f64,
1075    avoided: Vec<ObjectId>,
1076    connectors: Option<ConnectorRouting>,
1077}
1078
1079impl ForcedWalkRequest {
1080    /// Creates a request; `tolerance_metres` is how narrow the bracket
1081    /// should become.
1082    ///
1083    /// # Errors
1084    ///
1085    /// [`MetricRoutingError::NoTargets`] for no target,
1086    /// [`MetricRoutingError::InvalidTolerance`] for a negative or
1087    /// non-finite tolerance.
1088    pub fn try_new(
1089        origin: MetricPoint,
1090        targets: Vec<MetricPoint>,
1091        through: ObjectId,
1092        profile: MobilityProfile,
1093        tolerance_metres: f64,
1094    ) -> Result<Self, MetricRoutingError> {
1095        if targets.is_empty() {
1096            return Err(MetricRoutingError::NoTargets);
1097        }
1098        if !valid_non_negative(tolerance_metres) {
1099            return Err(MetricRoutingError::InvalidTolerance);
1100        }
1101        Ok(Self {
1102            origin,
1103            targets,
1104            through,
1105            profile,
1106            tolerance_metres,
1107            avoided: Vec::new(),
1108            connectors: None,
1109        })
1110    }
1111
1112    /// The same request, walking around `avoided` (sorted, deduplicated).
1113    #[must_use]
1114    pub fn with_avoided(mut self, mut avoided: Vec<ObjectId>) -> Self {
1115        avoided.sort();
1116        avoided.dedup();
1117        self.avoided = avoided;
1118        self
1119    }
1120
1121    /// The same request, climbing through `connectors` between levels.
1122    #[must_use]
1123    pub fn with_connectors(mut self, connectors: ConnectorRouting) -> Self {
1124        self.connectors = Some(connectors);
1125        self
1126    }
1127
1128    /// Where every walk starts.
1129    pub fn origin(&self) -> &MetricPoint {
1130        &self.origin
1131    }
1132
1133    /// The targets, in request order.
1134    pub fn targets(&self) -> &[MetricPoint] {
1135        &self.targets
1136    }
1137
1138    /// The object every walk measured enters.
1139    pub fn through(&self) -> &ObjectId {
1140        &self.through
1141    }
1142
1143    /// Mobility envelope.
1144    pub fn profile(&self) -> MobilityProfile {
1145        self.profile
1146    }
1147
1148    /// Requested bracket width in metres.
1149    pub fn tolerance_metres(&self) -> f64 {
1150        self.tolerance_metres
1151    }
1152
1153    /// The objects every walk keeps out of, sorted.
1154    pub fn avoided(&self) -> &[ObjectId] {
1155        &self.avoided
1156    }
1157
1158    /// The connectors a walk may climb through.
1159    pub fn connectors(&self) -> Option<&ConnectorRouting> {
1160        self.connectors.as_ref()
1161    }
1162}
1163
1164/// Bounds on the shortest walk entering the object.
1165#[derive(Clone, Debug, PartialEq)]
1166pub struct ForcedWalkEvidence {
1167    lower_metres: f64,
1168    upper_metres: f64,
1169    converged: bool,
1170    evidence: Evidence,
1171}
1172
1173impl ForcedWalkEvidence {
1174    /// Validates the bracket: `lower_metres` finite and non-negative,
1175    /// `upper_metres` no less, and infinite when no walk entering the
1176    /// object is known.
1177    ///
1178    /// # Errors
1179    ///
1180    /// [`MetricRoutingError::InvalidLengthInterval`] for bounds out of
1181    /// order, [`MetricRoutingError::InexactRouteEvidence`] for provenance
1182    /// that is not exact.
1183    pub fn try_new(
1184        lower_metres: f64,
1185        upper_metres: f64,
1186        converged: bool,
1187        evidence: Evidence,
1188    ) -> Result<Self, MetricRoutingError> {
1189        if !valid_non_negative(lower_metres) || upper_metres.is_nan() || upper_metres < lower_metres
1190        {
1191            return Err(MetricRoutingError::InvalidLengthInterval);
1192        }
1193        if !reviewable_exact_evidence(&evidence) {
1194            return Err(MetricRoutingError::InexactRouteEvidence);
1195        }
1196        Ok(Self {
1197            lower_metres,
1198            upper_metres,
1199            converged,
1200            evidence,
1201        })
1202    }
1203
1204    /// No walk entering the object is shorter.
1205    pub fn lower_metres(&self) -> f64 {
1206        self.lower_metres
1207    }
1208
1209    /// Some walk entering the object is no longer; infinite when none is
1210    /// known.
1211    pub fn upper_metres(&self) -> f64 {
1212        self.upper_metres
1213    }
1214
1215    /// Whether the bracket is no wider than the requested tolerance.
1216    pub fn converged(&self) -> bool {
1217        self.converged
1218    }
1219
1220    /// Measurement provenance.
1221    pub fn evidence(&self) -> &Evidence {
1222        &self.evidence
1223    }
1224}
1225
1226/// No walk from the origin to a target enters the object, under complete
1227/// exact evidence.
1228#[derive(Clone, Debug, PartialEq)]
1229pub struct NeverEnteredEvidence {
1230    request: ForcedWalkRequest,
1231    completeness: CompleteMetricEvidence,
1232}
1233
1234impl NeverEnteredEvidence {
1235    /// Binds complete evidence to one request.
1236    pub fn new(request: ForcedWalkRequest, completeness: CompleteMetricEvidence) -> Self {
1237        Self {
1238            request,
1239            completeness,
1240        }
1241    }
1242
1243    /// Request proven never entered.
1244    pub fn request(&self) -> &ForcedWalkRequest {
1245        &self.request
1246    }
1247
1248    /// Exact completeness provenance.
1249    pub fn completeness(&self) -> &CompleteMetricEvidence {
1250        &self.completeness
1251    }
1252}
1253
1254/// Answer to a [`ForcedWalkRequest`].
1255#[derive(Clone, Debug, PartialEq)]
1256pub enum ForcedWalkOutcome {
1257    /// The shortest walk entering the object is bracketed.
1258    Bounded(ForcedWalkEvidence),
1259    /// No walk to a target enters the object.
1260    NeverEntered(Box<NeverEnteredEvidence>),
1261}
1262
1263/// Relative slack for a backend's rounding when a traced length is checked
1264/// against the polyline's own length.
1265const TRACE_ROUNDING: f64 = 1e-9;
1266
1267/// Backend-neutral metric routing interface implemented by trusted host code.
1268pub trait MetricRoutingService: Send + Sync + 'static {
1269    /// Evaluates one route request or explicitly refuses unavailable evidence.
1270    fn route(&self, request: &MetricRouteRequest)
1271    -> Result<MetricRouteOutcome, MetricRoutingError>;
1272
1273    /// Bounds the distance from the origin to the nearest target. The
1274    /// default refuses: a backend that cannot search many targets at once
1275    /// must not answer with a single pair.
1276    fn nearest_target(
1277        &self,
1278        request: &NearestTargetRequest,
1279    ) -> Result<NearestTargetOutcome, MetricRoutingError> {
1280        let _ = request;
1281        Err(MetricRoutingError::Unavailable(
1282            "this backend does not measure nearest targets".into(),
1283        ))
1284    }
1285
1286    /// Brackets the largest distance from any point of the region to the
1287    /// nearest target. The default refuses rather than sampling points.
1288    fn farthest_point(
1289        &self,
1290        request: &FarthestPointRequest,
1291    ) -> Result<FarthestPointOutcome, MetricRoutingError> {
1292        let _ = request;
1293        Err(MetricRoutingError::Unavailable(
1294            "this backend does not measure farthest points".into(),
1295        ))
1296    }
1297
1298    /// Whether [`Self::nearest_target`] honours
1299    /// [`NearestTargetRequest::avoided`]. The default is `false`, and the
1300    /// handle then refuses a request avoiding anything rather than let the
1301    /// backend answer the plain walk.
1302    fn avoids_objects(&self) -> bool {
1303        false
1304    }
1305
1306    /// Whether [`Self::nearest_target`] and [`Self::farthest_point`] honour
1307    /// a request's [`TravelCost`]s. The default is `false`, and the handle
1308    /// then refuses a weighted request rather than let the backend answer
1309    /// the plain length.
1310    fn weighs_travel(&self) -> bool {
1311        false
1312    }
1313
1314    /// Brackets the shortest walk to a target that enters an object. The
1315    /// default refuses.
1316    fn forced_walk(
1317        &self,
1318        request: &ForcedWalkRequest,
1319    ) -> Result<ForcedWalkOutcome, MetricRoutingError> {
1320        let _ = request;
1321        Err(MetricRoutingError::Unavailable(
1322            "this backend does not measure walks forced through objects".into(),
1323        ))
1324    }
1325
1326    /// Whether the queries honour a request's [`ConnectorRouting`]. The
1327    /// default is `false`, and the handle then refuses a request carrying
1328    /// connectors rather than let the backend answer a walk on one level.
1329    fn climbs_connectors(&self) -> bool {
1330        false
1331    }
1332
1333    /// Measures how much of a polyline lies over each requested object. The
1334    /// default refuses.
1335    fn trace_path(&self, request: &PathTraceRequest) -> Result<PathTrace, MetricRoutingError> {
1336        let _ = request;
1337        Err(MetricRoutingError::Unavailable(
1338            "this backend does not trace paths over objects".into(),
1339        ))
1340    }
1341}
1342
1343/// Concrete type-indexable wrapper around a metric routing service.
1344#[derive(Clone)]
1345pub struct MetricRoutingServiceHandle(Arc<dyn MetricRoutingService>);
1346
1347impl MetricRoutingServiceHandle {
1348    /// Wraps an Axiolid or alternate backend implementation for service registration.
1349    pub fn new(service: Arc<dyn MetricRoutingService>) -> Self {
1350        Self(service)
1351    }
1352
1353    /// Executes and validates endpoint identity in the backend response.
1354    pub fn route(
1355        &self,
1356        request: &MetricRouteRequest,
1357    ) -> Result<MetricRouteOutcome, MetricRoutingError> {
1358        self.climbing(request.connectors())?;
1359        let outcome = self.0.route(request)?;
1360        if let MetricRouteOutcome::Reachable(route) = &outcome {
1361            let (Some(first), Some(last)) = (route.waypoints.first(), route.waypoints.last())
1362            else {
1363                return Err(MetricRoutingError::EmptyRouteEvidence);
1364            };
1365            if first != request.origin() || last != request.destination() {
1366                return Err(MetricRoutingError::ResponseEndpointMismatch);
1367            }
1368        } else if let MetricRouteOutcome::Blocked(blocked) = &outcome
1369            && blocked.request() != request
1370        {
1371            return Err(MetricRoutingError::ResponseEndpointMismatch);
1372        }
1373        Ok(outcome)
1374    }
1375
1376    /// Executes a nearest-target query and checks the answer is bound to it:
1377    /// the target exists, the route starts at the origin and ends at it, and
1378    /// an unreachable verdict names this request.
1379    ///
1380    /// A request avoiding objects is refused unless the backend [avoids
1381    /// objects](MetricRoutingService::avoids_objects).
1382    pub fn nearest_target(
1383        &self,
1384        request: &NearestTargetRequest,
1385    ) -> Result<NearestTargetOutcome, MetricRoutingError> {
1386        if !request.avoided().is_empty() && !self.0.avoids_objects() {
1387            return Err(MetricRoutingError::Unavailable(
1388                "this backend does not walk around objects".into(),
1389            ));
1390        }
1391        self.weighing(request.costs())?;
1392        self.climbing(request.connectors())?;
1393        let outcome = self.0.nearest_target(request)?;
1394        match &outcome {
1395            NearestTargetOutcome::Reached(reached) => {
1396                let target = request
1397                    .targets()
1398                    .get(reached.target())
1399                    .ok_or(MetricRoutingError::InconsistentResponse)?;
1400                let (Some(first), Some(last)) =
1401                    (reached.waypoints().first(), reached.waypoints().last())
1402                else {
1403                    return Err(MetricRoutingError::EmptyRouteEvidence);
1404                };
1405                if first != request.origin() || last != target {
1406                    return Err(MetricRoutingError::ResponseEndpointMismatch);
1407                }
1408            }
1409            NearestTargetOutcome::Unreachable(unreachable) => {
1410                if unreachable.request() != request {
1411                    return Err(MetricRoutingError::ResponseEndpointMismatch);
1412                }
1413            }
1414        }
1415        Ok(outcome)
1416    }
1417
1418    /// Executes a farthest-point query and checks the answer is bound to it:
1419    /// the witness lies on the requested region, a claimed convergence holds
1420    /// for the requested tolerance, and an unreachable verdict names this
1421    /// request.
1422    pub fn farthest_point(
1423        &self,
1424        request: &FarthestPointRequest,
1425    ) -> Result<FarthestPointOutcome, MetricRoutingError> {
1426        self.weighing(request.costs())?;
1427        self.climbing(request.connectors())?;
1428        let outcome = self.0.farthest_point(request)?;
1429        match &outcome {
1430            FarthestPointOutcome::Bounded(bounded) => {
1431                if bounded.witness().subject() != request.region() {
1432                    return Err(MetricRoutingError::ResponseEndpointMismatch);
1433                }
1434                let width = bounded.distance().upper_metres() - bounded.distance().lower_metres();
1435                if bounded.converged() && width > request.tolerance_metres() {
1436                    return Err(MetricRoutingError::InconsistentResponse);
1437                }
1438            }
1439            FarthestPointOutcome::Unreachable(unreachable) => {
1440                if unreachable.request() != request
1441                    || unreachable.witness().subject() != request.region()
1442                {
1443                    return Err(MetricRoutingError::ResponseEndpointMismatch);
1444                }
1445            }
1446        }
1447        Ok(outcome)
1448    }
1449}
1450
1451impl MetricRoutingServiceHandle {
1452    /// Refuses a weighted request unless the backend [weighs
1453    /// travel](MetricRoutingService::weighs_travel).
1454    fn weighing(&self, costs: &[TravelCost]) -> Result<(), MetricRoutingError> {
1455        if !costs.is_empty() && !self.0.weighs_travel() {
1456            return Err(MetricRoutingError::Unavailable(
1457                "this backend does not weigh travel over objects".into(),
1458            ));
1459        }
1460        Ok(())
1461    }
1462
1463    /// Executes a forced-walk query and checks the answer is bound to it: a
1464    /// claimed convergence holds for the requested tolerance, and a
1465    /// never-entered verdict names this request. A request avoiding
1466    /// objects or climbing connectors is refused as for
1467    /// [`Self::nearest_target`].
1468    pub fn forced_walk(
1469        &self,
1470        request: &ForcedWalkRequest,
1471    ) -> Result<ForcedWalkOutcome, MetricRoutingError> {
1472        if !request.avoided().is_empty() && !self.0.avoids_objects() {
1473            return Err(MetricRoutingError::Unavailable(
1474                "this backend does not walk around objects".into(),
1475            ));
1476        }
1477        self.climbing(request.connectors())?;
1478        let outcome = self.0.forced_walk(request)?;
1479        match &outcome {
1480            ForcedWalkOutcome::Bounded(bounded) => {
1481                if bounded.converged()
1482                    && bounded.upper_metres() - bounded.lower_metres() > request.tolerance_metres()
1483                {
1484                    return Err(MetricRoutingError::InconsistentResponse);
1485                }
1486            }
1487            ForcedWalkOutcome::NeverEntered(never) => {
1488                if never.request() != request {
1489                    return Err(MetricRoutingError::ResponseEndpointMismatch);
1490                }
1491            }
1492        }
1493        Ok(outcome)
1494    }
1495
1496    /// Refuses a request carrying connectors unless the backend [climbs
1497    /// connectors](MetricRoutingService::climbs_connectors).
1498    fn climbing(&self, connectors: Option<&ConnectorRouting>) -> Result<(), MetricRoutingError> {
1499        if connectors.is_some() && !self.0.climbs_connectors() {
1500            return Err(MetricRoutingError::Unavailable(
1501                "this backend does not route through vertical connectors".into(),
1502            ));
1503        }
1504        Ok(())
1505    }
1506
1507    /// Traces a polyline over objects and checks the answer is bound to it:
1508    /// one length per requested object, none surely longer than the
1509    /// polyline itself.
1510    pub fn trace_path(&self, request: &PathTraceRequest) -> Result<PathTrace, MetricRoutingError> {
1511        let trace = self.0.trace_path(request)?;
1512        if trace.lengths().len() != request.objects().len() {
1513            return Err(MetricRoutingError::InconsistentResponse);
1514        }
1515        let most = request.plan_length_metres() * (1.0 + TRACE_ROUNDING) + TRACE_ROUNDING;
1516        if trace
1517            .lengths()
1518            .iter()
1519            .flatten()
1520            .any(|length| length.lower_metres() > most)
1521        {
1522            return Err(MetricRoutingError::InconsistentResponse);
1523        }
1524        Ok(trace)
1525    }
1526}
1527
1528fn valid_non_negative(value: f64) -> bool {
1529    value.is_finite() && value >= 0.0
1530}