Skip to main content

axioval_engine/
free_space.rs

1//! Source-neutral free-area and clearance host-service contracts.
2//!
3//! Geometry algorithms and native shapes remain in Axiolid or another trusted
4//! backend. This module carries canonical metric requests and reviewable evidence.
5
6use crate::{MetricPoint, MobilityProfile, ThresholdVerdict};
7use axioval_ir::{Evidence, ObjectId};
8use std::sync::Arc;
9use thiserror::Error;
10
11#[derive(Clone, Debug, Error, PartialEq, Eq)]
12pub enum FreeSpaceError {
13    #[error("free-area interval is invalid")]
14    InvalidAreaInterval,
15    #[error("metric direction is zero or non-finite")]
16    InvalidMetricDirection,
17    #[error("metric frame axes are not mutually perpendicular")]
18    InvalidMetricFrame,
19    #[error("clearance shape dimensions must be positive and finite")]
20    InvalidClearanceShape,
21    #[error("placement offset interval is non-finite or reversed")]
22    InvalidOffsetInterval,
23    #[error("placement support gap must be finite and non-negative")]
24    InvalidSupportGap,
25    #[error("clearance evidence is incomplete")]
26    IncompleteClearanceEvidence,
27    #[error("obstruction evidence has no blocking objects")]
28    EmptyObstructionEvidence,
29    #[error("obstruction evidence names an object outside the request candidate set")]
30    UnexpectedObstacleEvidence,
31    #[error("obstruction provenance is not exact and reviewable")]
32    InexactObstructionEvidence,
33    #[error("free-area evidence is not exact and reviewable")]
34    InexactAreaEvidence,
35    #[error("placement evidence is not exact and reviewable")]
36    InexactPlacementEvidence,
37    #[error("support evidence is not exact and reviewable")]
38    InexactSupportEvidence,
39    #[error("supported placement has no complete support evidence")]
40    MissingSupportEvidence,
41    #[error("support evidence does not match the requested support or found frame")]
42    SupportEvidenceMismatch,
43    #[error("placement frame is not grounded in the requested scope")]
44    PlacementScopeMismatch,
45    #[error("placement witness falls outside its requested search domain")]
46    PlacementDomainMismatch,
47    #[error("free-space backend returned evidence for another request")]
48    ResponseRequestMismatch,
49    #[error("free-space geometry is unavailable for `{0}`")]
50    MissingGeometry(Box<ObjectId>),
51    #[error("free-space query unavailable: {0}")]
52    Unavailable(String),
53}
54
55#[derive(Clone, Copy, Debug, PartialEq)]
56pub struct AreaInterval {
57    lower_square_metres: f64,
58    upper_square_metres: f64,
59}
60impl AreaInterval {
61    pub fn try_new(lower: f64, upper: f64) -> Result<Self, FreeSpaceError> {
62        if !valid_non_negative(lower) || !valid_non_negative(upper) || lower > upper {
63            return Err(FreeSpaceError::InvalidAreaInterval);
64        }
65        Ok(Self {
66            lower_square_metres: lower,
67            upper_square_metres: upper,
68        })
69    }
70    pub fn exact(square_metres: f64) -> Result<Self, FreeSpaceError> {
71        Self::try_new(square_metres, square_metres)
72    }
73    pub fn lower_square_metres(&self) -> f64 {
74        self.lower_square_metres
75    }
76    pub fn upper_square_metres(&self) -> f64 {
77        self.upper_square_metres
78    }
79    pub fn compare_minimum(&self, minimum: f64) -> Result<ThresholdVerdict, FreeSpaceError> {
80        if !valid_non_negative(minimum) {
81            return Err(FreeSpaceError::InvalidAreaInterval);
82        }
83        if self.lower_square_metres >= minimum {
84            Ok(ThresholdVerdict::Satisfied)
85        } else if self.upper_square_metres < minimum {
86            Ok(ThresholdVerdict::Violated)
87        } else {
88            Ok(ThresholdVerdict::Indeterminate)
89        }
90    }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq)]
94pub struct MetricDirection([f64; 3]);
95impl MetricDirection {
96    pub fn try_new(vector: [f64; 3]) -> Result<Self, FreeSpaceError> {
97        if !vector.iter().all(|v| v.is_finite()) {
98            return Err(FreeSpaceError::InvalidMetricDirection);
99        }
100        let norm = vector.iter().map(|v| v * v).sum::<f64>().sqrt();
101        if norm <= f64::EPSILON {
102            return Err(FreeSpaceError::InvalidMetricDirection);
103        }
104        Ok(Self([vector[0] / norm, vector[1] / norm, vector[2] / norm]))
105    }
106    pub fn components(&self) -> [f64; 3] {
107        self.0
108    }
109    fn dot(self, other: Self) -> f64 {
110        self.0[0] * other.0[0] + self.0[1] * other.0[1] + self.0[2] * other.0[2]
111    }
112}
113
114#[derive(Clone, Debug, PartialEq)]
115pub struct MetricFrame {
116    origin: MetricPoint,
117    right: MetricDirection,
118    forward: MetricDirection,
119    up: MetricDirection,
120}
121impl MetricFrame {
122    pub fn try_new(
123        origin: MetricPoint,
124        right: MetricDirection,
125        forward: MetricDirection,
126        up: MetricDirection,
127    ) -> Result<Self, FreeSpaceError> {
128        const ORTHOGONAL_TOLERANCE: f64 = 1.0e-9;
129        let [rx, ry, rz] = right.components();
130        let [fx, fy, fz] = forward.components();
131        let [ux, uy, uz] = up.components();
132        let handedness =
133            (ry * fz - rz * fy) * ux + (rz * fx - rx * fz) * uy + (rx * fy - ry * fx) * uz;
134        if right.dot(forward).abs() > ORTHOGONAL_TOLERANCE
135            || right.dot(up).abs() > ORTHOGONAL_TOLERANCE
136            || forward.dot(up).abs() > ORTHOGONAL_TOLERANCE
137            || handedness < 1.0 - ORTHOGONAL_TOLERANCE
138        {
139            return Err(FreeSpaceError::InvalidMetricFrame);
140        }
141        Ok(Self {
142            origin,
143            right,
144            forward,
145            up,
146        })
147    }
148    pub fn origin(&self) -> &MetricPoint {
149        &self.origin
150    }
151    pub fn right(&self) -> MetricDirection {
152        self.right
153    }
154    pub fn forward(&self) -> MetricDirection {
155        self.forward
156    }
157    pub fn up(&self) -> MetricDirection {
158        self.up
159    }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq)]
163pub struct BoxClearance {
164    width: f64,
165    depth: f64,
166    height: f64,
167}
168impl BoxClearance {
169    pub fn try_new(width: f64, depth: f64, height: f64) -> Result<Self, FreeSpaceError> {
170        if !valid_positive(width) || !valid_positive(depth) || !valid_positive(height) {
171            return Err(FreeSpaceError::InvalidClearanceShape);
172        }
173        Ok(Self {
174            width,
175            depth,
176            height,
177        })
178    }
179    pub fn width_metres(&self) -> f64 {
180        self.width
181    }
182    pub fn depth_metres(&self) -> f64 {
183        self.depth
184    }
185    pub fn height_metres(&self) -> f64 {
186        self.height
187    }
188}
189
190#[derive(Clone, Copy, Debug, PartialEq)]
191pub struct CylinderClearance {
192    radius: f64,
193    height: f64,
194}
195impl CylinderClearance {
196    pub fn try_new(radius: f64, height: f64) -> Result<Self, FreeSpaceError> {
197        if !valid_positive(radius) || !valid_positive(height) {
198            return Err(FreeSpaceError::InvalidClearanceShape);
199        }
200        Ok(Self { radius, height })
201    }
202    pub fn radius_metres(&self) -> f64 {
203        self.radius
204    }
205    pub fn height_metres(&self) -> f64 {
206        self.height
207    }
208}
209
210#[derive(Clone, Copy, Debug, PartialEq)]
211pub enum ClearanceShape {
212    Box(BoxClearance),
213    Cylinder(CylinderClearance),
214}
215
216#[derive(Clone, Debug, PartialEq)]
217pub struct ClearanceRequest {
218    frame: MetricFrame,
219    shape: ClearanceShape,
220    obstacles: Vec<ObjectId>,
221}
222impl ClearanceRequest {
223    pub fn new(frame: MetricFrame, shape: ClearanceShape, mut obstacles: Vec<ObjectId>) -> Self {
224        obstacles.sort();
225        obstacles.dedup();
226        Self {
227            frame,
228            shape,
229            obstacles,
230        }
231    }
232    pub fn frame(&self) -> &MetricFrame {
233        &self.frame
234    }
235    pub fn shape(&self) -> ClearanceShape {
236        self.shape
237    }
238    pub fn obstacles(&self) -> &[ObjectId] {
239        &self.obstacles
240    }
241}
242
243#[derive(Clone, Debug, PartialEq)]
244pub struct FreeAreaRequest {
245    scope: ObjectId,
246    mobility: MobilityProfile,
247    obstacles: Vec<ObjectId>,
248}
249impl FreeAreaRequest {
250    pub fn new(scope: ObjectId, mobility: MobilityProfile, mut obstacles: Vec<ObjectId>) -> Self {
251        obstacles.sort();
252        obstacles.dedup();
253        Self {
254            scope,
255            mobility,
256            obstacles,
257        }
258    }
259    pub fn scope(&self) -> &ObjectId {
260        &self.scope
261    }
262    pub fn mobility(&self) -> MobilityProfile {
263        self.mobility
264    }
265    pub fn obstacles(&self) -> &[ObjectId] {
266        &self.obstacles
267    }
268}
269
270/// Inclusive signed offset bounds in canonical metres.
271#[derive(Clone, Copy, Debug, PartialEq)]
272pub struct SignedDistanceInterval {
273    lower_metres: f64,
274    upper_metres: f64,
275}
276impl SignedDistanceInterval {
277    pub fn try_new(lower_metres: f64, upper_metres: f64) -> Result<Self, FreeSpaceError> {
278        if !lower_metres.is_finite() || !upper_metres.is_finite() || lower_metres > upper_metres {
279            return Err(FreeSpaceError::InvalidOffsetInterval);
280        }
281        Ok(Self {
282            lower_metres,
283            upper_metres,
284        })
285    }
286    pub fn exact(metres: f64) -> Result<Self, FreeSpaceError> {
287        Self::try_new(metres, metres)
288    }
289    pub fn lower_metres(&self) -> f64 {
290        self.lower_metres
291    }
292    pub fn upper_metres(&self) -> f64 {
293        self.upper_metres
294    }
295    fn contains(self, value: f64) -> bool {
296        value >= self.lower_metres && value <= self.upper_metres
297    }
298}
299
300/// Requires the entire placement base to lie on an object's support surface.
301#[derive(Clone, Debug, PartialEq)]
302pub struct SupportedPlacement {
303    support: ObjectId,
304    maximum_gap_metres: f64,
305}
306impl SupportedPlacement {
307    pub fn try_new(support: ObjectId, maximum_gap_metres: f64) -> Result<Self, FreeSpaceError> {
308        if !valid_non_negative(maximum_gap_metres) {
309            return Err(FreeSpaceError::InvalidSupportGap);
310        }
311        Ok(Self {
312            support,
313            maximum_gap_metres,
314        })
315    }
316    pub fn support(&self) -> &ObjectId {
317        &self.support
318    }
319    pub fn maximum_gap_metres(&self) -> f64 {
320        self.maximum_gap_metres
321    }
322}
323
324/// Restricts candidate-frame origins to offsets in an anchor frame.
325#[derive(Clone, Debug, PartialEq)]
326pub struct FrameOffsetPlacement {
327    anchor: MetricFrame,
328    right: SignedDistanceInterval,
329    forward: SignedDistanceInterval,
330    up: SignedDistanceInterval,
331}
332impl FrameOffsetPlacement {
333    pub fn new(
334        anchor: MetricFrame,
335        right: SignedDistanceInterval,
336        forward: SignedDistanceInterval,
337        up: SignedDistanceInterval,
338    ) -> Self {
339        Self {
340            anchor,
341            right,
342            forward,
343            up,
344        }
345    }
346    pub fn anchor(&self) -> &MetricFrame {
347        &self.anchor
348    }
349    pub fn right(&self) -> SignedDistanceInterval {
350        self.right
351    }
352    pub fn forward(&self) -> SignedDistanceInterval {
353        self.forward
354    }
355    pub fn up(&self) -> SignedDistanceInterval {
356        self.up
357    }
358    fn contains_frame(&self, frame: &MetricFrame) -> bool {
359        let aligned = self.anchor.right() == frame.right()
360            && self.anchor.forward() == frame.forward()
361            && self.anchor.up() == frame.up();
362        if !aligned {
363            return false;
364        }
365        let anchor = self.anchor.origin().coordinates_metres();
366        let found = frame.origin().coordinates_metres();
367        let delta = [
368            found[0] - anchor[0],
369            found[1] - anchor[1],
370            found[2] - anchor[2],
371        ];
372        let project = |axis: MetricDirection| {
373            axis.components()
374                .into_iter()
375                .zip(delta)
376                .map(|(a, b)| a * b)
377                .sum()
378        };
379        self.right.contains(project(self.anchor.right()))
380            && self.forward.contains(project(self.anchor.forward()))
381            && self.up.contains(project(self.anchor.up()))
382    }
383}
384
385/// Geometric predicate limiting where a backend may search for placements.
386#[derive(Clone, Debug, PartialEq)]
387pub enum PlacementDomain {
388    Unconstrained,
389    Supported(SupportedPlacement),
390    FrameOffsets(FrameOffsetPlacement),
391    SupportedFrameOffsets {
392        support: SupportedPlacement,
393        offsets: FrameOffsetPlacement,
394    },
395}
396
397fn requested_support(domain: &PlacementDomain) -> Option<&SupportedPlacement> {
398    match domain {
399        PlacementDomain::Supported(support)
400        | PlacementDomain::SupportedFrameOffsets { support, .. } => Some(support),
401        _ => None,
402    }
403}
404
405/// Searches an object-grounded scope for any placement of a clearance shape.
406#[derive(Clone, Debug, PartialEq)]
407pub struct PlacementRequest {
408    scope: ObjectId,
409    shape: ClearanceShape,
410    obstacles: Vec<ObjectId>,
411    domain: PlacementDomain,
412}
413impl PlacementRequest {
414    pub fn new(scope: ObjectId, shape: ClearanceShape, mut obstacles: Vec<ObjectId>) -> Self {
415        obstacles.sort();
416        obstacles.dedup();
417        Self {
418            scope,
419            shape,
420            obstacles,
421            domain: PlacementDomain::Unconstrained,
422        }
423    }
424    pub fn new_in_domain(
425        scope: ObjectId,
426        shape: ClearanceShape,
427        mut obstacles: Vec<ObjectId>,
428        domain: PlacementDomain,
429    ) -> Result<Self, FreeSpaceError> {
430        let offsets = match &domain {
431            PlacementDomain::FrameOffsets(offsets)
432            | PlacementDomain::SupportedFrameOffsets { offsets, .. } => Some(offsets),
433            _ => None,
434        };
435        if offsets.is_some_and(|offsets| offsets.anchor().origin().subject() != &scope) {
436            return Err(FreeSpaceError::PlacementScopeMismatch);
437        }
438        obstacles.sort();
439        obstacles.dedup();
440        Ok(Self {
441            scope,
442            shape,
443            obstacles,
444            domain,
445        })
446    }
447    pub fn scope(&self) -> &ObjectId {
448        &self.scope
449    }
450    pub fn shape(&self) -> ClearanceShape {
451        self.shape
452    }
453    pub fn obstacles(&self) -> &[ObjectId] {
454        &self.obstacles
455    }
456    pub fn domain(&self) -> &PlacementDomain {
457        &self.domain
458    }
459}
460
461/// Exact proof that the entire candidate base is supported at a found frame.
462#[derive(Clone, Debug, PartialEq)]
463pub struct CompleteSupportEvidence {
464    support: ObjectId,
465    frame: MetricFrame,
466    maximum_gap_metres: f64,
467    evidence: Evidence,
468}
469
470impl CompleteSupportEvidence {
471    pub fn try_new(
472        support: ObjectId,
473        frame: MetricFrame,
474        maximum_gap_metres: f64,
475        evidence: Evidence,
476    ) -> Result<Self, FreeSpaceError> {
477        if !valid_non_negative(maximum_gap_metres) {
478            return Err(FreeSpaceError::InvalidSupportGap);
479        }
480        if !reviewable_exact_evidence(&evidence) {
481            return Err(FreeSpaceError::InexactSupportEvidence);
482        }
483        Ok(Self {
484            support,
485            frame,
486            maximum_gap_metres,
487            evidence,
488        })
489    }
490    pub fn support(&self) -> &ObjectId {
491        &self.support
492    }
493    pub fn frame(&self) -> &MetricFrame {
494        &self.frame
495    }
496    pub fn maximum_gap_metres(&self) -> f64 {
497        self.maximum_gap_metres
498    }
499    pub fn evidence(&self) -> &Evidence {
500        &self.evidence
501    }
502}
503
504#[derive(Clone, Debug, PartialEq)]
505pub struct CompleteClearanceEvidence {
506    request: ClearanceRequest,
507    evidence: Evidence,
508}
509impl CompleteClearanceEvidence {
510    pub fn try_new(request: ClearanceRequest, evidence: Evidence) -> Result<Self, FreeSpaceError> {
511        if !reviewable_exact_evidence(&evidence) {
512            return Err(FreeSpaceError::IncompleteClearanceEvidence);
513        }
514        Ok(Self { request, evidence })
515    }
516    pub fn request(&self) -> &ClearanceRequest {
517        &self.request
518    }
519    pub fn evidence(&self) -> &Evidence {
520        &self.evidence
521    }
522}
523
524#[derive(Clone, Debug, PartialEq)]
525pub struct ObstructionEvidence {
526    request: ClearanceRequest,
527    blockers: Vec<ObjectId>,
528    evidence: Evidence,
529}
530impl ObstructionEvidence {
531    pub fn try_new(
532        request: ClearanceRequest,
533        mut blockers: Vec<ObjectId>,
534        evidence: Evidence,
535    ) -> Result<Self, FreeSpaceError> {
536        if blockers.is_empty() {
537            return Err(FreeSpaceError::EmptyObstructionEvidence);
538        }
539        if !reviewable_exact_evidence(&evidence) {
540            return Err(FreeSpaceError::InexactObstructionEvidence);
541        }
542        blockers.sort();
543        blockers.dedup();
544        if blockers
545            .iter()
546            .any(|blocker| request.obstacles().binary_search(blocker).is_err())
547        {
548            return Err(FreeSpaceError::UnexpectedObstacleEvidence);
549        }
550        Ok(Self {
551            request,
552            blockers,
553            evidence,
554        })
555    }
556    pub fn request(&self) -> &ClearanceRequest {
557        &self.request
558    }
559    pub fn blockers(&self) -> &[ObjectId] {
560        &self.blockers
561    }
562    pub fn evidence(&self) -> &Evidence {
563        &self.evidence
564    }
565}
566
567#[derive(Clone, Debug, PartialEq)]
568pub enum ClearanceOutcome {
569    Clear(CompleteClearanceEvidence),
570    Obstructed(ObstructionEvidence),
571}
572
573/// One exact placement witness. It does not claim exhaustive search coverage.
574#[derive(Clone, Debug, PartialEq)]
575pub struct ClearancePlacementEvidence {
576    request: PlacementRequest,
577    frame: MetricFrame,
578    support_evidence: Option<Box<CompleteSupportEvidence>>,
579    evidence: Evidence,
580}
581fn validate_placement_witness(
582    request: &PlacementRequest,
583    frame: &MetricFrame,
584    evidence: &Evidence,
585) -> Result<(), FreeSpaceError> {
586    if frame.origin().subject() != request.scope() {
587        return Err(FreeSpaceError::PlacementScopeMismatch);
588    }
589    let offsets = match request.domain() {
590        PlacementDomain::FrameOffsets(offsets)
591        | PlacementDomain::SupportedFrameOffsets { offsets, .. } => Some(offsets),
592        _ => None,
593    };
594    if offsets.is_some_and(|offsets| !offsets.contains_frame(frame)) {
595        return Err(FreeSpaceError::PlacementDomainMismatch);
596    }
597    if !reviewable_exact_evidence(evidence) {
598        return Err(FreeSpaceError::InexactPlacementEvidence);
599    }
600    Ok(())
601}
602
603impl ClearancePlacementEvidence {
604    pub fn try_new(
605        request: PlacementRequest,
606        frame: MetricFrame,
607        evidence: Evidence,
608    ) -> Result<Self, FreeSpaceError> {
609        if requested_support(request.domain()).is_some() {
610            return Err(FreeSpaceError::MissingSupportEvidence);
611        }
612        validate_placement_witness(&request, &frame, &evidence)?;
613        Ok(Self {
614            request,
615            frame,
616            support_evidence: None,
617            evidence,
618        })
619    }
620    pub fn try_new_supported(
621        request: PlacementRequest,
622        frame: MetricFrame,
623        support_evidence: CompleteSupportEvidence,
624        evidence: Evidence,
625    ) -> Result<Self, FreeSpaceError> {
626        validate_placement_witness(&request, &frame, &evidence)?;
627        let required =
628            requested_support(request.domain()).ok_or(FreeSpaceError::SupportEvidenceMismatch)?;
629        if support_evidence.support() != required.support()
630            || support_evidence.frame() != &frame
631            || support_evidence.maximum_gap_metres() > required.maximum_gap_metres()
632        {
633            return Err(FreeSpaceError::SupportEvidenceMismatch);
634        }
635        Ok(Self {
636            request,
637            frame,
638            support_evidence: Some(Box::new(support_evidence)),
639            evidence,
640        })
641    }
642    pub fn request(&self) -> &PlacementRequest {
643        &self.request
644    }
645    pub fn frame(&self) -> &MetricFrame {
646        &self.frame
647    }
648    pub fn support_evidence(&self) -> Option<&CompleteSupportEvidence> {
649        self.support_evidence.as_deref()
650    }
651    pub fn evidence(&self) -> &Evidence {
652        &self.evidence
653    }
654}
655
656/// Exact, complete evidence that no valid placement exists.
657#[derive(Clone, Debug, PartialEq)]
658pub struct CompletePlacementEvidence {
659    request: PlacementRequest,
660    evidence: Evidence,
661}
662impl CompletePlacementEvidence {
663    pub fn try_new(request: PlacementRequest, evidence: Evidence) -> Result<Self, FreeSpaceError> {
664        if !reviewable_exact_evidence(&evidence) {
665            return Err(FreeSpaceError::IncompleteClearanceEvidence);
666        }
667        Ok(Self { request, evidence })
668    }
669    pub fn request(&self) -> &PlacementRequest {
670        &self.request
671    }
672    pub fn evidence(&self) -> &Evidence {
673        &self.evidence
674    }
675}
676
677#[derive(Clone, Debug, PartialEq)]
678pub enum PlacementOutcome {
679    Found(ClearancePlacementEvidence),
680    NoPlacement(CompletePlacementEvidence),
681}
682
683#[derive(Clone, Debug, PartialEq)]
684pub struct FreeAreaEvidence {
685    request: FreeAreaRequest,
686    available_area: AreaInterval,
687    evidence: Evidence,
688}
689impl FreeAreaEvidence {
690    pub fn try_new(
691        request: FreeAreaRequest,
692        available_area: AreaInterval,
693        evidence: Evidence,
694    ) -> Result<Self, FreeSpaceError> {
695        if !reviewable_exact_evidence(&evidence) {
696            return Err(FreeSpaceError::InexactAreaEvidence);
697        }
698        Ok(Self {
699            request,
700            available_area,
701            evidence,
702        })
703    }
704    pub fn request(&self) -> &FreeAreaRequest {
705        &self.request
706    }
707    pub fn available_area(&self) -> &AreaInterval {
708        &self.available_area
709    }
710    pub fn evidence(&self) -> &Evidence {
711        &self.evidence
712    }
713}
714
715pub trait FreeSpaceService: Send + Sync + 'static {
716    fn assess_clearance(
717        &self,
718        request: &ClearanceRequest,
719    ) -> Result<ClearanceOutcome, FreeSpaceError>;
720    fn find_placement(
721        &self,
722        request: &PlacementRequest,
723    ) -> Result<PlacementOutcome, FreeSpaceError>;
724    fn measure_free_area(
725        &self,
726        request: &FreeAreaRequest,
727    ) -> Result<FreeAreaEvidence, FreeSpaceError>;
728}
729
730#[derive(Clone)]
731pub struct FreeSpaceServiceHandle(Arc<dyn FreeSpaceService>);
732impl FreeSpaceServiceHandle {
733    pub fn new(service: Arc<dyn FreeSpaceService>) -> Self {
734        Self(service)
735    }
736    pub fn assess_clearance(
737        &self,
738        request: &ClearanceRequest,
739    ) -> Result<ClearanceOutcome, FreeSpaceError> {
740        let outcome = self.0.assess_clearance(request)?;
741        let actual = match &outcome {
742            ClearanceOutcome::Clear(value) => value.request(),
743            ClearanceOutcome::Obstructed(value) => value.request(),
744        };
745        if actual != request {
746            return Err(FreeSpaceError::ResponseRequestMismatch);
747        }
748        Ok(outcome)
749    }
750    pub fn find_placement(
751        &self,
752        request: &PlacementRequest,
753    ) -> Result<PlacementOutcome, FreeSpaceError> {
754        let outcome = self.0.find_placement(request)?;
755        let actual = match &outcome {
756            PlacementOutcome::Found(value) => value.request(),
757            PlacementOutcome::NoPlacement(value) => value.request(),
758        };
759        if actual != request {
760            return Err(FreeSpaceError::ResponseRequestMismatch);
761        }
762        Ok(outcome)
763    }
764    pub fn measure_free_area(
765        &self,
766        request: &FreeAreaRequest,
767    ) -> Result<FreeAreaEvidence, FreeSpaceError> {
768        let evidence = self.0.measure_free_area(request)?;
769        if evidence.request() != request {
770            return Err(FreeSpaceError::ResponseRequestMismatch);
771        }
772        Ok(evidence)
773    }
774}
775
776fn valid_non_negative(value: f64) -> bool {
777    value.is_finite() && value >= 0.0
778}
779fn valid_positive(value: f64) -> bool {
780    value.is_finite() && value > 0.0
781}
782fn reviewable_exact_evidence(evidence: &Evidence) -> bool {
783    evidence.exact && !evidence.locator.trim().is_empty()
784}