1use std::sync::Arc;
24
25use axioval_ir::{Evidence, ObjectId};
26use thiserror::Error;
27
28#[derive(Clone, Debug, Error, PartialEq, Eq)]
30pub enum BoundaryCoverageError {
31 #[error("no space boundaries registered for `{0}`")]
34 UnknownSpace(ObjectId),
35 #[error("space `{0}` has no body")]
37 NoBody(ObjectId),
38 #[error("boundary coverage unavailable: {0}")]
42 Unavailable(String),
43 #[error("invalid boundary coverage request: {0}")]
45 InvalidRequest(String),
46 #[error("the geometry service does not measure space-boundary coverage")]
48 Unsupported,
49 #[error("boundary coverage measurement is invalid")]
51 InvalidMeasurement,
52}
53
54#[derive(Clone, Debug, PartialEq)]
56pub struct BoundaryCoverageRequest {
57 space: ObjectId,
58 plane_tolerance: f64,
59}
60
61impl BoundaryCoverageRequest {
62 pub fn try_new(
69 space: ObjectId,
70 plane_tolerance_metres: f64,
71 ) -> Result<Self, BoundaryCoverageError> {
72 if !plane_tolerance_metres.is_finite() || plane_tolerance_metres < 0.0 {
73 return Err(BoundaryCoverageError::InvalidRequest(format!(
74 "plane tolerance must be a finite length of at least zero, not \
75 {plane_tolerance_metres}"
76 )));
77 }
78 Ok(Self {
79 space,
80 plane_tolerance: plane_tolerance_metres,
81 })
82 }
83
84 #[must_use]
86 pub fn space(&self) -> &ObjectId {
87 &self.space
88 }
89
90 #[must_use]
93 pub fn plane_tolerance_metres(&self) -> f64 {
94 self.plane_tolerance
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
100pub struct SurfaceAreaInterval {
101 lower: f64,
102 upper: f64,
103}
104
105impl SurfaceAreaInterval {
106 pub fn try_new(lower: f64, upper: f64) -> Result<Self, BoundaryCoverageError> {
112 if !lower.is_finite() || !upper.is_finite() || lower < 0.0 || lower > upper {
113 return Err(BoundaryCoverageError::InvalidMeasurement);
114 }
115 Ok(Self { lower, upper })
116 }
117
118 pub fn exact(square_metres: f64) -> Result<Self, BoundaryCoverageError> {
124 Self::try_new(square_metres, square_metres)
125 }
126
127 #[must_use]
129 pub fn lower_square_metres(&self) -> f64 {
130 self.lower
131 }
132
133 #[must_use]
135 pub fn upper_square_metres(&self) -> f64 {
136 self.upper
137 }
138
139 #[must_use]
141 #[allow(clippy::float_cmp)]
142 pub fn is_point(&self) -> bool {
143 self.lower == self.upper
144 }
145}
146
147#[derive(Clone, Copy, Debug, PartialEq)]
149pub struct ShareInterval {
150 lower: f64,
151 upper: f64,
152}
153
154impl ShareInterval {
155 #[must_use]
157 pub fn lower(&self) -> f64 {
158 self.lower
159 }
160
161 #[must_use]
163 pub fn upper(&self) -> f64 {
164 self.upper
165 }
166}
167
168#[derive(Clone, Copy, Debug, PartialEq)]
170pub enum BoundaryPlacement {
171 OnSurface {
175 area: SurfaceAreaInterval,
177 },
178 OffSurface,
180}
181
182#[derive(Clone, Debug, PartialEq)]
184pub struct MeasuredBoundary {
185 boundary: ObjectId,
186 element: Option<ObjectId>,
187 placement: BoundaryPlacement,
188}
189
190impl MeasuredBoundary {
191 #[must_use]
194 pub fn new(
195 boundary: ObjectId,
196 element: Option<ObjectId>,
197 placement: BoundaryPlacement,
198 ) -> Self {
199 Self {
200 boundary,
201 element,
202 placement,
203 }
204 }
205
206 #[must_use]
208 pub fn boundary(&self) -> &ObjectId {
209 &self.boundary
210 }
211
212 #[must_use]
214 pub fn element(&self) -> Option<&ObjectId> {
215 self.element.as_ref()
216 }
217
218 #[must_use]
220 pub fn placement(&self) -> BoundaryPlacement {
221 self.placement
222 }
223}
224
225#[derive(Clone, Debug, PartialEq)]
227pub struct BoundaryOverlap {
228 first: ObjectId,
229 second: ObjectId,
230 area: SurfaceAreaInterval,
231}
232
233impl BoundaryOverlap {
234 pub fn try_new(
241 first: ObjectId,
242 second: ObjectId,
243 area: SurfaceAreaInterval,
244 ) -> Result<Self, BoundaryCoverageError> {
245 if first == second {
246 return Err(BoundaryCoverageError::InvalidMeasurement);
247 }
248 let (first, second) = if first < second {
249 (first, second)
250 } else {
251 (second, first)
252 };
253 Ok(Self {
254 first,
255 second,
256 area,
257 })
258 }
259
260 #[must_use]
262 pub fn first(&self) -> &ObjectId {
263 &self.first
264 }
265
266 #[must_use]
268 pub fn second(&self) -> &ObjectId {
269 &self.second
270 }
271
272 #[must_use]
274 pub fn area(&self) -> SurfaceAreaInterval {
275 self.area
276 }
277}
278
279#[derive(Clone, Copy, Debug, PartialEq)]
281pub struct CoverageAreas {
282 pub surface: SurfaceAreaInterval,
284 pub covered: SurfaceAreaInterval,
286 pub uncovered: SurfaceAreaInterval,
288 pub overlap: SurfaceAreaInterval,
290}
291
292#[derive(Clone, Debug, PartialEq)]
295pub struct BoundaryCoverage {
296 request: BoundaryCoverageRequest,
297 areas: CoverageAreas,
298 share: ShareInterval,
299 boundaries: Vec<MeasuredBoundary>,
300 overlaps: Vec<BoundaryOverlap>,
301 evidence: Evidence,
302}
303
304impl BoundaryCoverage {
305 pub fn try_new(
317 request: BoundaryCoverageRequest,
318 areas: CoverageAreas,
319 mut boundaries: Vec<MeasuredBoundary>,
320 mut overlaps: Vec<BoundaryOverlap>,
321 evidence: Evidence,
322 ) -> Result<Self, BoundaryCoverageError> {
323 let invalid = || BoundaryCoverageError::InvalidMeasurement;
324 if evidence.locator.trim().is_empty() {
325 return Err(invalid());
326 }
327 let CoverageAreas {
328 surface,
329 covered,
330 uncovered,
331 overlap,
332 } = areas;
333 if surface.lower <= 0.0 {
334 return Err(invalid());
335 }
336 let slack = 1e-9 * surface.upper.max(1.0);
339 if covered.lower + uncovered.lower > surface.upper + slack
340 || covered.upper + uncovered.upper < surface.lower - slack
341 || covered.lower > surface.upper
342 || uncovered.lower > surface.upper
343 || overlap.lower > covered.upper + slack
344 {
345 return Err(invalid());
346 }
347 boundaries.sort_by(|a, b| a.boundary.cmp(&b.boundary));
348 if boundaries
349 .windows(2)
350 .any(|pair| pair[0].boundary == pair[1].boundary)
351 {
352 return Err(invalid());
353 }
354 let on_surface = |id: &ObjectId| {
355 boundaries
356 .binary_search_by(|boundary| boundary.boundary.cmp(id))
357 .is_ok_and(|index| {
358 matches!(
359 boundaries[index].placement,
360 BoundaryPlacement::OnSurface { .. }
361 )
362 })
363 };
364 if overlaps
365 .iter()
366 .any(|pair| !on_surface(&pair.first) || !on_surface(&pair.second))
367 {
368 return Err(invalid());
369 }
370 overlaps.sort_by(|a, b| (&a.first, &a.second).cmp(&(&b.first, &b.second)));
371 if overlaps
372 .windows(2)
373 .any(|pair| (&pair[0].first, &pair[0].second) == (&pair[1].first, &pair[1].second))
374 {
375 return Err(invalid());
376 }
377 if evidence.exact {
378 let points = [surface, covered, uncovered, overlap]
379 .iter()
380 .chain(overlaps.iter().map(|pair| &pair.area))
381 .all(SurfaceAreaInterval::is_point)
382 && boundaries.iter().all(|boundary| match boundary.placement {
383 BoundaryPlacement::OnSurface { area } => area.is_point(),
384 BoundaryPlacement::OffSurface => true,
385 });
386 if !points {
387 return Err(invalid());
388 }
389 }
390 let share = ShareInterval {
391 lower: (covered.lower / surface.upper).clamp(0.0, 1.0),
392 upper: (covered.upper / surface.lower).clamp(0.0, 1.0),
393 };
394 Ok(Self {
395 request,
396 areas,
397 share,
398 boundaries,
399 overlaps,
400 evidence,
401 })
402 }
403
404 #[must_use]
406 pub fn request(&self) -> &BoundaryCoverageRequest {
407 &self.request
408 }
409
410 #[must_use]
412 pub fn space(&self) -> &ObjectId {
413 &self.request.space
414 }
415
416 #[must_use]
418 pub fn surface_area(&self) -> SurfaceAreaInterval {
419 self.areas.surface
420 }
421
422 #[must_use]
424 pub fn covered_area(&self) -> SurfaceAreaInterval {
425 self.areas.covered
426 }
427
428 #[must_use]
430 pub fn uncovered_area(&self) -> SurfaceAreaInterval {
431 self.areas.uncovered
432 }
433
434 #[must_use]
436 pub fn overlap_area(&self) -> SurfaceAreaInterval {
437 self.areas.overlap
438 }
439
440 #[must_use]
444 pub fn covered_share(&self) -> ShareInterval {
445 self.share
446 }
447
448 #[must_use]
450 pub fn boundaries(&self) -> &[MeasuredBoundary] {
451 &self.boundaries
452 }
453
454 pub fn off_surface(&self) -> impl Iterator<Item = &MeasuredBoundary> {
456 self.boundaries
457 .iter()
458 .filter(|boundary| boundary.placement == BoundaryPlacement::OffSurface)
459 }
460
461 #[must_use]
464 pub fn overlaps(&self) -> &[BoundaryOverlap] {
465 &self.overlaps
466 }
467
468 #[must_use]
470 pub fn is_exact(&self) -> bool {
471 self.evidence.exact
472 }
473
474 #[must_use]
476 pub fn evidence(&self) -> &Evidence {
477 &self.evidence
478 }
479}
480
481pub trait BoundaryCoverageService: Send + Sync + 'static {
483 fn measure_boundary_coverage(
490 &self,
491 request: &BoundaryCoverageRequest,
492 ) -> Result<BoundaryCoverage, BoundaryCoverageError> {
493 let _ = request;
494 Err(BoundaryCoverageError::Unsupported)
495 }
496}
497
498#[derive(Clone)]
500pub struct BoundaryCoverageServiceHandle(Arc<dyn BoundaryCoverageService>);
501
502impl BoundaryCoverageServiceHandle {
503 #[must_use]
505 pub fn new(service: Arc<dyn BoundaryCoverageService>) -> Self {
506 Self(service)
507 }
508
509 pub fn measure_boundary_coverage(
518 &self,
519 request: &BoundaryCoverageRequest,
520 ) -> Result<BoundaryCoverage, BoundaryCoverageError> {
521 let answer = self.0.measure_boundary_coverage(request)?;
522 if answer.request() != request {
523 return Err(BoundaryCoverageError::InvalidMeasurement);
524 }
525 Ok(answer)
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use axioval_ir::SourceId;
533
534 fn source() -> SourceId {
535 SourceId::new("cad", "m").unwrap()
536 }
537
538 fn id(local: &str) -> ObjectId {
539 ObjectId::new(source(), local).unwrap()
540 }
541
542 fn area(lower: f64, upper: f64) -> SurfaceAreaInterval {
543 SurfaceAreaInterval::try_new(lower, upper).unwrap()
544 }
545
546 fn point(value: f64) -> SurfaceAreaInterval {
547 SurfaceAreaInterval::exact(value).unwrap()
548 }
549
550 fn request() -> BoundaryCoverageRequest {
551 BoundaryCoverageRequest::try_new(id("space"), 0.001).unwrap()
552 }
553
554 fn areas(surface: f64, covered: f64, overlap: f64) -> CoverageAreas {
555 CoverageAreas {
556 surface: point(surface),
557 covered: point(covered),
558 uncovered: point(surface - covered),
559 overlap: point(overlap),
560 }
561 }
562
563 fn on(boundary: &str, value: f64) -> MeasuredBoundary {
564 MeasuredBoundary::new(
565 id(boundary),
566 Some(id("wall")),
567 BoundaryPlacement::OnSurface { area: point(value) },
568 )
569 }
570
571 fn exact() -> Evidence {
572 Evidence::exact(source(), "boundary-coverage:space")
573 }
574
575 #[test]
576 fn a_request_needs_a_finite_non_negative_tolerance() {
577 assert!(BoundaryCoverageRequest::try_new(id("space"), 0.0).is_ok());
578 for tolerance in [-0.1, f64::NAN, f64::INFINITY] {
579 assert!(matches!(
580 BoundaryCoverageRequest::try_new(id("space"), tolerance),
581 Err(BoundaryCoverageError::InvalidRequest(_))
582 ));
583 }
584 }
585
586 #[test]
587 fn intervals_refuse_reversed_negative_and_non_finite_bounds() {
588 assert!(SurfaceAreaInterval::try_new(1.0, 2.0).is_ok());
589 for (lower, upper) in [
590 (2.0, 1.0),
591 (-1.0, 1.0),
592 (0.0, f64::INFINITY),
593 (f64::NAN, 1.0),
594 ] {
595 assert_eq!(
596 SurfaceAreaInterval::try_new(lower, upper),
597 Err(BoundaryCoverageError::InvalidMeasurement)
598 );
599 }
600 }
601
602 #[test]
603 fn the_share_is_derived_from_covered_and_surface_areas() {
604 let coverage = BoundaryCoverage::try_new(
605 request(),
606 areas(10.0, 7.5, 0.0),
607 vec![on("b", 5.0), on("a", 2.5)],
608 vec![],
609 exact(),
610 )
611 .unwrap();
612 assert!((coverage.covered_share().lower() - 0.75).abs() < 1e-12);
613 assert!((coverage.covered_share().upper() - 0.75).abs() < 1e-12);
614 assert_eq!(coverage.boundaries()[0].boundary(), &id("a"));
615
616 let approximate = BoundaryCoverage::try_new(
617 request(),
618 CoverageAreas {
619 surface: area(9.0, 10.0),
620 covered: area(4.5, 5.0),
621 uncovered: area(4.5, 5.5),
622 overlap: area(0.0, 0.1),
623 },
624 vec![],
625 vec![],
626 Evidence {
627 exact: false,
628 ..exact()
629 },
630 )
631 .unwrap();
632 assert!((approximate.covered_share().lower() - 0.45).abs() < 1e-12);
633 assert!((approximate.covered_share().upper() - 5.0 / 9.0).abs() < 1e-12);
634 }
635
636 #[test]
637 fn exact_evidence_needs_point_intervals() {
638 let widened = CoverageAreas {
639 surface: area(10.0, 10.1),
640 ..areas(10.0, 5.0, 0.0)
641 };
642 assert_eq!(
643 BoundaryCoverage::try_new(request(), widened, vec![], vec![], exact()),
644 Err(BoundaryCoverageError::InvalidMeasurement)
645 );
646 let widened_boundary = MeasuredBoundary::new(
647 id("a"),
648 None,
649 BoundaryPlacement::OnSurface {
650 area: area(1.0, 1.1),
651 },
652 );
653 assert_eq!(
654 BoundaryCoverage::try_new(
655 request(),
656 areas(10.0, 1.0, 0.0),
657 vec![widened_boundary],
658 vec![],
659 exact()
660 ),
661 Err(BoundaryCoverageError::InvalidMeasurement)
662 );
663 }
664
665 #[test]
666 fn inconsistent_areas_are_refused() {
667 let over = CoverageAreas {
669 uncovered: point(6.0),
670 ..areas(10.0, 5.0, 0.0)
671 };
672 let empty = areas(0.0, 0.0, 0.0);
674 let overlapping = areas(10.0, 1.0, 2.0);
676 for areas in [over, empty, overlapping] {
677 assert_eq!(
678 BoundaryCoverage::try_new(request(), areas, vec![], vec![], exact()),
679 Err(BoundaryCoverageError::InvalidMeasurement)
680 );
681 }
682 }
683
684 #[test]
685 fn boundaries_are_listed_once_and_overlaps_name_boundaries_on_the_surface() {
686 assert_eq!(
687 BoundaryCoverage::try_new(
688 request(),
689 areas(10.0, 5.0, 0.0),
690 vec![on("a", 5.0), on("a", 5.0)],
691 vec![],
692 exact()
693 ),
694 Err(BoundaryCoverageError::InvalidMeasurement)
695 );
696 let off = MeasuredBoundary::new(id("c"), None, BoundaryPlacement::OffSurface);
697 let with = |pair: BoundaryOverlap| {
698 BoundaryCoverage::try_new(
699 request(),
700 areas(10.0, 5.0, 1.0),
701 vec![on("a", 3.0), on("b", 3.0), off.clone()],
702 vec![pair],
703 exact(),
704 )
705 };
706 let coverage =
707 with(BoundaryOverlap::try_new(id("b"), id("a"), point(1.0)).unwrap()).unwrap();
708 assert_eq!(coverage.overlaps()[0].first(), &id("a"));
709 assert_eq!(coverage.off_surface().count(), 1);
710 for pair in [
711 BoundaryOverlap::try_new(id("a"), id("c"), point(1.0)).unwrap(),
712 BoundaryOverlap::try_new(id("a"), id("z"), point(1.0)).unwrap(),
713 ] {
714 assert_eq!(with(pair), Err(BoundaryCoverageError::InvalidMeasurement));
715 }
716 assert_eq!(
717 BoundaryOverlap::try_new(id("a"), id("a"), point(1.0)),
718 Err(BoundaryCoverageError::InvalidMeasurement)
719 );
720 }
721
722 struct Refusing;
723 impl BoundaryCoverageService for Refusing {}
724
725 struct Other;
726 impl BoundaryCoverageService for Other {
727 fn measure_boundary_coverage(
728 &self,
729 _: &BoundaryCoverageRequest,
730 ) -> Result<BoundaryCoverage, BoundaryCoverageError> {
731 BoundaryCoverage::try_new(
732 BoundaryCoverageRequest::try_new(id("space"), 0.5).unwrap(),
733 areas(10.0, 10.0, 0.0),
734 vec![],
735 vec![],
736 exact(),
737 )
738 }
739 }
740
741 #[test]
742 fn the_default_refuses_and_the_handle_binds_answers_to_the_request() {
743 let refusing = BoundaryCoverageServiceHandle::new(Arc::new(Refusing));
744 assert_eq!(
745 refusing.measure_boundary_coverage(&request()),
746 Err(BoundaryCoverageError::Unsupported)
747 );
748 let other = BoundaryCoverageServiceHandle::new(Arc::new(Other));
749 assert_eq!(
750 other.measure_boundary_coverage(&request()),
751 Err(BoundaryCoverageError::InvalidMeasurement)
752 );
753 let same = BoundaryCoverageRequest::try_new(id("space"), 0.5).unwrap();
754 assert!(other.measure_boundary_coverage(&same).is_ok());
755 }
756}