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