use std::sync::Arc;
use axioval_ir::{Evidence, ObjectId};
use thiserror::Error;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum BoundaryCoverageError {
#[error("no space boundaries registered for `{0}`")]
UnknownSpace(ObjectId),
#[error("space `{0}` has no body")]
NoBody(ObjectId),
#[error("boundary coverage unavailable: {0}")]
Unavailable(String),
#[error("invalid boundary coverage request: {0}")]
InvalidRequest(String),
#[error("the geometry service does not measure space-boundary coverage")]
Unsupported,
#[error("boundary coverage measurement is invalid")]
InvalidMeasurement,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BoundaryCoverageRequest {
space: ObjectId,
plane_tolerance: f64,
}
impl BoundaryCoverageRequest {
pub fn try_new(
space: ObjectId,
plane_tolerance_metres: f64,
) -> Result<Self, BoundaryCoverageError> {
if !plane_tolerance_metres.is_finite() || plane_tolerance_metres < 0.0 {
return Err(BoundaryCoverageError::InvalidRequest(format!(
"plane tolerance must be a finite length of at least zero, not \
{plane_tolerance_metres}"
)));
}
Ok(Self {
space,
plane_tolerance: plane_tolerance_metres,
})
}
#[must_use]
pub fn space(&self) -> &ObjectId {
&self.space
}
#[must_use]
pub fn plane_tolerance_metres(&self) -> f64 {
self.plane_tolerance
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SurfaceAreaInterval {
lower: f64,
upper: f64,
}
impl SurfaceAreaInterval {
pub fn try_new(lower: f64, upper: f64) -> Result<Self, BoundaryCoverageError> {
if !lower.is_finite() || !upper.is_finite() || lower < 0.0 || lower > upper {
return Err(BoundaryCoverageError::InvalidMeasurement);
}
Ok(Self { lower, upper })
}
pub fn exact(square_metres: f64) -> Result<Self, BoundaryCoverageError> {
Self::try_new(square_metres, square_metres)
}
#[must_use]
pub fn lower_square_metres(&self) -> f64 {
self.lower
}
#[must_use]
pub fn upper_square_metres(&self) -> f64 {
self.upper
}
#[must_use]
#[allow(clippy::float_cmp)]
pub fn is_point(&self) -> bool {
self.lower == self.upper
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ShareInterval {
lower: f64,
upper: f64,
}
impl ShareInterval {
#[must_use]
pub fn lower(&self) -> f64 {
self.lower
}
#[must_use]
pub fn upper(&self) -> f64 {
self.upper
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BoundaryPlacement {
OnSurface {
area: SurfaceAreaInterval,
},
OffSurface,
}
#[derive(Clone, Debug, PartialEq)]
pub struct MeasuredBoundary {
boundary: ObjectId,
element: Option<ObjectId>,
placement: BoundaryPlacement,
}
impl MeasuredBoundary {
#[must_use]
pub fn new(
boundary: ObjectId,
element: Option<ObjectId>,
placement: BoundaryPlacement,
) -> Self {
Self {
boundary,
element,
placement,
}
}
#[must_use]
pub fn boundary(&self) -> &ObjectId {
&self.boundary
}
#[must_use]
pub fn element(&self) -> Option<&ObjectId> {
self.element.as_ref()
}
#[must_use]
pub fn placement(&self) -> BoundaryPlacement {
self.placement
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BoundaryOverlap {
first: ObjectId,
second: ObjectId,
area: SurfaceAreaInterval,
}
impl BoundaryOverlap {
pub fn try_new(
first: ObjectId,
second: ObjectId,
area: SurfaceAreaInterval,
) -> Result<Self, BoundaryCoverageError> {
if first == second {
return Err(BoundaryCoverageError::InvalidMeasurement);
}
let (first, second) = if first < second {
(first, second)
} else {
(second, first)
};
Ok(Self {
first,
second,
area,
})
}
#[must_use]
pub fn first(&self) -> &ObjectId {
&self.first
}
#[must_use]
pub fn second(&self) -> &ObjectId {
&self.second
}
#[must_use]
pub fn area(&self) -> SurfaceAreaInterval {
self.area
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CoverageAreas {
pub surface: SurfaceAreaInterval,
pub covered: SurfaceAreaInterval,
pub uncovered: SurfaceAreaInterval,
pub overlap: SurfaceAreaInterval,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BoundaryCoverage {
request: BoundaryCoverageRequest,
areas: CoverageAreas,
share: ShareInterval,
boundaries: Vec<MeasuredBoundary>,
overlaps: Vec<BoundaryOverlap>,
evidence: Evidence,
}
impl BoundaryCoverage {
pub fn try_new(
request: BoundaryCoverageRequest,
areas: CoverageAreas,
mut boundaries: Vec<MeasuredBoundary>,
mut overlaps: Vec<BoundaryOverlap>,
evidence: Evidence,
) -> Result<Self, BoundaryCoverageError> {
let invalid = || BoundaryCoverageError::InvalidMeasurement;
if evidence.locator.trim().is_empty() {
return Err(invalid());
}
let CoverageAreas {
surface,
covered,
uncovered,
overlap,
} = areas;
if surface.lower <= 0.0 {
return Err(invalid());
}
let slack = 1e-9 * surface.upper.max(1.0);
if covered.lower + uncovered.lower > surface.upper + slack
|| covered.upper + uncovered.upper < surface.lower - slack
|| covered.lower > surface.upper
|| uncovered.lower > surface.upper
|| overlap.lower > covered.upper + slack
{
return Err(invalid());
}
boundaries.sort_by(|a, b| a.boundary.cmp(&b.boundary));
if boundaries
.windows(2)
.any(|pair| pair[0].boundary == pair[1].boundary)
{
return Err(invalid());
}
let on_surface = |id: &ObjectId| {
boundaries
.binary_search_by(|boundary| boundary.boundary.cmp(id))
.is_ok_and(|index| {
matches!(
boundaries[index].placement,
BoundaryPlacement::OnSurface { .. }
)
})
};
if overlaps
.iter()
.any(|pair| !on_surface(&pair.first) || !on_surface(&pair.second))
{
return Err(invalid());
}
overlaps.sort_by(|a, b| (&a.first, &a.second).cmp(&(&b.first, &b.second)));
if overlaps
.windows(2)
.any(|pair| (&pair[0].first, &pair[0].second) == (&pair[1].first, &pair[1].second))
{
return Err(invalid());
}
if evidence.exact {
let points = [surface, covered, uncovered, overlap]
.iter()
.chain(overlaps.iter().map(|pair| &pair.area))
.all(SurfaceAreaInterval::is_point)
&& boundaries.iter().all(|boundary| match boundary.placement {
BoundaryPlacement::OnSurface { area } => area.is_point(),
BoundaryPlacement::OffSurface => true,
});
if !points {
return Err(invalid());
}
}
let share = ShareInterval {
lower: (covered.lower / surface.upper).clamp(0.0, 1.0),
upper: (covered.upper / surface.lower).clamp(0.0, 1.0),
};
Ok(Self {
request,
areas,
share,
boundaries,
overlaps,
evidence,
})
}
#[must_use]
pub fn request(&self) -> &BoundaryCoverageRequest {
&self.request
}
#[must_use]
pub fn space(&self) -> &ObjectId {
&self.request.space
}
#[must_use]
pub fn surface_area(&self) -> SurfaceAreaInterval {
self.areas.surface
}
#[must_use]
pub fn covered_area(&self) -> SurfaceAreaInterval {
self.areas.covered
}
#[must_use]
pub fn uncovered_area(&self) -> SurfaceAreaInterval {
self.areas.uncovered
}
#[must_use]
pub fn overlap_area(&self) -> SurfaceAreaInterval {
self.areas.overlap
}
#[must_use]
pub fn covered_share(&self) -> ShareInterval {
self.share
}
#[must_use]
pub fn boundaries(&self) -> &[MeasuredBoundary] {
&self.boundaries
}
pub fn off_surface(&self) -> impl Iterator<Item = &MeasuredBoundary> {
self.boundaries
.iter()
.filter(|boundary| boundary.placement == BoundaryPlacement::OffSurface)
}
#[must_use]
pub fn overlaps(&self) -> &[BoundaryOverlap] {
&self.overlaps
}
#[must_use]
pub fn is_exact(&self) -> bool {
self.evidence.exact
}
#[must_use]
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}
pub trait BoundaryCoverageService: Send + Sync + 'static {
fn measure_boundary_coverage(
&self,
request: &BoundaryCoverageRequest,
) -> Result<BoundaryCoverage, BoundaryCoverageError> {
let _ = request;
Err(BoundaryCoverageError::Unsupported)
}
}
#[derive(Clone)]
pub struct BoundaryCoverageServiceHandle(Arc<dyn BoundaryCoverageService>);
impl BoundaryCoverageServiceHandle {
#[must_use]
pub fn new(service: Arc<dyn BoundaryCoverageService>) -> Self {
Self(service)
}
pub fn measure_boundary_coverage(
&self,
request: &BoundaryCoverageRequest,
) -> Result<BoundaryCoverage, BoundaryCoverageError> {
let answer = self.0.measure_boundary_coverage(request)?;
if answer.request() != request {
return Err(BoundaryCoverageError::InvalidMeasurement);
}
Ok(answer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use axioval_ir::SourceId;
fn source() -> SourceId {
SourceId::new("cad", "m").unwrap()
}
fn id(local: &str) -> ObjectId {
ObjectId::new(source(), local).unwrap()
}
fn area(lower: f64, upper: f64) -> SurfaceAreaInterval {
SurfaceAreaInterval::try_new(lower, upper).unwrap()
}
fn point(value: f64) -> SurfaceAreaInterval {
SurfaceAreaInterval::exact(value).unwrap()
}
fn request() -> BoundaryCoverageRequest {
BoundaryCoverageRequest::try_new(id("space"), 0.001).unwrap()
}
fn areas(surface: f64, covered: f64, overlap: f64) -> CoverageAreas {
CoverageAreas {
surface: point(surface),
covered: point(covered),
uncovered: point(surface - covered),
overlap: point(overlap),
}
}
fn on(boundary: &str, value: f64) -> MeasuredBoundary {
MeasuredBoundary::new(
id(boundary),
Some(id("wall")),
BoundaryPlacement::OnSurface { area: point(value) },
)
}
fn exact() -> Evidence {
Evidence::exact(source(), "boundary-coverage:space")
}
#[test]
fn a_request_needs_a_finite_non_negative_tolerance() {
assert!(BoundaryCoverageRequest::try_new(id("space"), 0.0).is_ok());
for tolerance in [-0.1, f64::NAN, f64::INFINITY] {
assert!(matches!(
BoundaryCoverageRequest::try_new(id("space"), tolerance),
Err(BoundaryCoverageError::InvalidRequest(_))
));
}
}
#[test]
fn intervals_refuse_reversed_negative_and_non_finite_bounds() {
assert!(SurfaceAreaInterval::try_new(1.0, 2.0).is_ok());
for (lower, upper) in [
(2.0, 1.0),
(-1.0, 1.0),
(0.0, f64::INFINITY),
(f64::NAN, 1.0),
] {
assert_eq!(
SurfaceAreaInterval::try_new(lower, upper),
Err(BoundaryCoverageError::InvalidMeasurement)
);
}
}
#[test]
fn the_share_is_derived_from_covered_and_surface_areas() {
let coverage = BoundaryCoverage::try_new(
request(),
areas(10.0, 7.5, 0.0),
vec![on("b", 5.0), on("a", 2.5)],
vec![],
exact(),
)
.unwrap();
assert!((coverage.covered_share().lower() - 0.75).abs() < 1e-12);
assert!((coverage.covered_share().upper() - 0.75).abs() < 1e-12);
assert_eq!(coverage.boundaries()[0].boundary(), &id("a"));
let approximate = BoundaryCoverage::try_new(
request(),
CoverageAreas {
surface: area(9.0, 10.0),
covered: area(4.5, 5.0),
uncovered: area(4.5, 5.5),
overlap: area(0.0, 0.1),
},
vec![],
vec![],
Evidence {
exact: false,
..exact()
},
)
.unwrap();
assert!((approximate.covered_share().lower() - 0.45).abs() < 1e-12);
assert!((approximate.covered_share().upper() - 5.0 / 9.0).abs() < 1e-12);
}
#[test]
fn exact_evidence_needs_point_intervals() {
let widened = CoverageAreas {
surface: area(10.0, 10.1),
..areas(10.0, 5.0, 0.0)
};
assert_eq!(
BoundaryCoverage::try_new(request(), widened, vec![], vec![], exact()),
Err(BoundaryCoverageError::InvalidMeasurement)
);
let widened_boundary = MeasuredBoundary::new(
id("a"),
None,
BoundaryPlacement::OnSurface {
area: area(1.0, 1.1),
},
);
assert_eq!(
BoundaryCoverage::try_new(
request(),
areas(10.0, 1.0, 0.0),
vec![widened_boundary],
vec![],
exact()
),
Err(BoundaryCoverageError::InvalidMeasurement)
);
}
#[test]
fn inconsistent_areas_are_refused() {
let over = CoverageAreas {
uncovered: point(6.0),
..areas(10.0, 5.0, 0.0)
};
let empty = areas(0.0, 0.0, 0.0);
let overlapping = areas(10.0, 1.0, 2.0);
for areas in [over, empty, overlapping] {
assert_eq!(
BoundaryCoverage::try_new(request(), areas, vec![], vec![], exact()),
Err(BoundaryCoverageError::InvalidMeasurement)
);
}
}
#[test]
fn boundaries_are_listed_once_and_overlaps_name_boundaries_on_the_surface() {
assert_eq!(
BoundaryCoverage::try_new(
request(),
areas(10.0, 5.0, 0.0),
vec![on("a", 5.0), on("a", 5.0)],
vec![],
exact()
),
Err(BoundaryCoverageError::InvalidMeasurement)
);
let off = MeasuredBoundary::new(id("c"), None, BoundaryPlacement::OffSurface);
let with = |pair: BoundaryOverlap| {
BoundaryCoverage::try_new(
request(),
areas(10.0, 5.0, 1.0),
vec![on("a", 3.0), on("b", 3.0), off.clone()],
vec![pair],
exact(),
)
};
let coverage =
with(BoundaryOverlap::try_new(id("b"), id("a"), point(1.0)).unwrap()).unwrap();
assert_eq!(coverage.overlaps()[0].first(), &id("a"));
assert_eq!(coverage.off_surface().count(), 1);
for pair in [
BoundaryOverlap::try_new(id("a"), id("c"), point(1.0)).unwrap(),
BoundaryOverlap::try_new(id("a"), id("z"), point(1.0)).unwrap(),
] {
assert_eq!(with(pair), Err(BoundaryCoverageError::InvalidMeasurement));
}
assert_eq!(
BoundaryOverlap::try_new(id("a"), id("a"), point(1.0)),
Err(BoundaryCoverageError::InvalidMeasurement)
);
}
struct Refusing;
impl BoundaryCoverageService for Refusing {}
struct Other;
impl BoundaryCoverageService for Other {
fn measure_boundary_coverage(
&self,
_: &BoundaryCoverageRequest,
) -> Result<BoundaryCoverage, BoundaryCoverageError> {
BoundaryCoverage::try_new(
BoundaryCoverageRequest::try_new(id("space"), 0.5).unwrap(),
areas(10.0, 10.0, 0.0),
vec![],
vec![],
exact(),
)
}
}
#[test]
fn the_default_refuses_and_the_handle_binds_answers_to_the_request() {
let refusing = BoundaryCoverageServiceHandle::new(Arc::new(Refusing));
assert_eq!(
refusing.measure_boundary_coverage(&request()),
Err(BoundaryCoverageError::Unsupported)
);
let other = BoundaryCoverageServiceHandle::new(Arc::new(Other));
assert_eq!(
other.measure_boundary_coverage(&request()),
Err(BoundaryCoverageError::InvalidMeasurement)
);
let same = BoundaryCoverageRequest::try_new(id("space"), 0.5).unwrap();
assert!(other.measure_boundary_coverage(&same).is_ok());
}
}