use std::f64::consts::PI;
use crate::error::DiagramError;
use crate::geometry::diagram::IntersectionPoint;
use crate::geometry::primitives::point;
use crate::geometry::primitives::Point;
use crate::geometry::shapes::{Polygon, Rectangle};
use crate::geometry::traits::{
Area, BoundingBox, Centroid, Closed, DiagramShape, Distance, Perimeter, Polygonize,
};
use argmin::core::{CostFunction, Error, Executor, State};
use argmin::solver::brent::BrentOpt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Circle {
center: Point,
radius: f64,
}
impl Area for Circle {
fn area(&self) -> f64 {
PI * self.radius * self.radius
}
}
impl Centroid for Circle {
fn centroid(&self) -> Point {
self.center
}
}
impl Distance for Circle {
fn distance(&self, other: &Self) -> f64 {
let center_distance = self.center.distance(&other.center);
let radius_sum = self.radius + other.radius;
if center_distance > radius_sum {
center_distance - radius_sum
} else {
0.0
}
}
}
impl Perimeter for Circle {
fn perimeter(&self) -> f64 {
2.0 * PI * self.radius
}
}
impl BoundingBox for Circle {
fn bounding_box(&self) -> Rectangle {
let width = 2.0 * self.radius;
let height = 2.0 * self.radius;
Rectangle::new(self.center, width, height)
}
}
impl Closed for Circle {
fn contains(&self, other: &Self) -> bool {
let center_distance = self.center.distance(&other.center);
center_distance + other.radius <= self.radius
}
fn contains_point(&self, point: &Point) -> bool {
let dist = self.center.distance(point);
dist <= self.radius
}
fn intersects(&self, other: &Self) -> bool {
let center_distance = self.center.distance(&other.center);
center_distance < self.radius + other.radius
&& center_distance > (self.radius - other.radius).abs()
}
fn intersection_area(&self, other: &Self) -> f64 {
let d = self.center.distance(&other.center);
if d >= self.radius + other.radius {
return 0.0; }
if d <= (self.radius - other.radius).abs() {
let smaller_radius = self.radius.min(other.radius);
return PI * smaller_radius * smaller_radius;
}
let r1 = self.radius;
let r2 = other.radius;
let part1 = r1 * r1 * (((d * d + r1 * r1 - r2 * r2) / (2.0 * d * r1)).acos());
let part2 = r2 * r2 * (((d * d + r2 * r2 - r1 * r1) / (2.0 * d * r2)).acos());
let part3 = 0.5 * ((r1 + r2 - d) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2)).sqrt();
part1 + part2 - part3
}
fn intersection_points(&self, other: &Self) -> Vec<Point> {
let d = self.center.distance(&other.center);
if d > self.radius + other.radius || d < (self.radius - other.radius).abs() {
return vec![]; }
let a = (self.radius * self.radius - other.radius * other.radius + d * d) / (2.0 * d);
let h = (self.radius * self.radius - a * a).sqrt();
let p2_x = self.center.x() + a * (other.center.x() - self.center.x()) / d;
let p2_y = self.center.y() + a * (other.center.y() - self.center.y()) / d;
let rx = -(other.center.y() - self.center.y()) * (h / d);
let ry = (other.center.x() - self.center.x()) * (h / d);
let intersection1 = Point::new(p2_x + rx, p2_y + ry);
let intersection2 = Point::new(p2_x - rx, p2_y - ry);
if intersection1 == intersection2 {
vec![intersection1]
} else {
vec![intersection1, intersection2]
}
}
}
impl DiagramShape for Circle {
fn compute_exclusive_regions(
shapes: &[Self],
) -> std::collections::HashMap<crate::geometry::diagram::RegionMask, f64> {
crate::geometry::diagram::compute_exclusive_regions(shapes)
}
fn optimizer_params_from_circle(x: f64, y: f64, radius: f64) -> Vec<f64> {
vec![x, y, radius]
}
fn mds_target_distance(
area_i: f64,
area_j: f64,
target_overlap: f64,
) -> Result<f64, crate::error::DiagramError> {
let r1 = (area_i / PI).sqrt();
let r2 = (area_j / PI).sqrt();
distance_for_overlap(r1, r2, target_overlap, None, None).map_err(|_| {
crate::error::DiagramError::InvalidCombination(format!(
"Could not compute target distance for areas {area_i} / {area_j} \
and target overlap {target_overlap}"
))
})
}
fn n_params() -> usize {
3 }
fn from_params(params: &[f64]) -> Self {
debug_assert_eq!(
params.len(),
3,
"Circle requires 3 parameters: x, y, radius"
);
Circle::new(
Point::new(params[0], params[1]),
params[2].max(f64::MIN_POSITIVE),
)
}
fn to_params(&self) -> Vec<f64> {
vec![self.center.x(), self.center.y(), self.radius]
}
fn compute_exclusive_regions_with_gradient(
shapes: &[Self],
) -> Option<crate::geometry::traits::ExclusiveRegionsAndGradient> {
Some(crate::geometry::diagram::compute_exclusive_regions_with_gradient_circles(shapes))
}
fn compute_exclusive_regions_clipped(
shapes: &[Self],
container: &Rectangle,
) -> Option<std::collections::HashMap<crate::geometry::diagram::RegionMask, f64>> {
Some(compute_exclusive_regions_clipped(shapes, container))
}
fn compute_exclusive_regions_clipped_with_gradient(
shapes: &[Self],
container: &Rectangle,
) -> Option<crate::geometry::traits::ExclusiveRegionsAndGradient> {
Some(compute_exclusive_regions_clipped_with_gradient(
shapes, container,
))
}
}
impl Polygonize for Circle {
fn polygonize(&self, n_vertices: usize) -> Polygon {
use std::f64::consts::PI;
let n_vertices = n_vertices.max(3);
let mut vertices = Vec::with_capacity(n_vertices);
for i in 0..n_vertices {
let angle = 2.0 * PI * (i as f64) / (n_vertices as f64);
let x = self.center.x() + self.radius * angle.cos();
let y = self.center.y() + self.radius * angle.sin();
vertices.push(Point::new(x, y));
}
Polygon::new(vertices)
}
}
struct SeparationCost {
r1: f64,
r2: f64,
target_overlap: f64,
}
impl CostFunction for SeparationCost {
type Param = f64;
type Output = f64;
fn cost(&self, distance: &Self::Param) -> Result<Self::Output, Error> {
let c1 = Circle::new(Point::new(0.0, 0.0), self.r1);
let c2 = Circle::new(Point::new(*distance, 0.0), self.r2);
let current_overlap = c1.intersection_area(&c2);
let cost = (current_overlap - self.target_overlap).powi(2);
Ok(cost)
}
}
impl Circle {
pub fn new(center: Point, radius: f64) -> Self {
assert!(radius > 0.0, "Circle radius must be > 0, got {}", radius);
Circle { center, radius }
}
pub fn try_new(center: Point, radius: f64) -> Result<Self, DiagramError> {
if radius > 0.0 {
Ok(Circle { center, radius })
} else {
Err(DiagramError::InvalidShapeParameter {
shape: "Circle",
param: "radius",
value: radius,
})
}
}
pub fn center(&self) -> &Point {
&self.center
}
pub fn radius(&self) -> f64 {
self.radius
}
pub fn set_center(&mut self, center: Point) {
self.center = center;
}
pub fn sector_area(&self, angle_rad: f64) -> f64 {
0.5 * self.radius * self.radius * angle_rad
}
pub fn segment_area_from_angle(&self, angle_rad: f64) -> f64 {
0.5 * self.radius * self.radius * (angle_rad - angle_rad.sin())
}
pub fn segment_area_from_chord(&self, chord_length: f64) -> f64 {
let r = self.radius;
debug_assert!(
chord_length <= 2.0 * r,
"Chord length {} cannot exceed diameter {}",
chord_length,
2.0 * r
);
let theta = 2.0 * (chord_length / (2.0 * r)).asin();
self.segment_area_from_angle(theta)
}
pub fn segment_area_from_points(&self, p1: &Point, p2: &Point) -> f64 {
let chord_length = p1.distance(p2);
self.segment_area_from_chord(chord_length)
}
}
pub(crate) fn distance_for_overlap(
r1: f64,
r2: f64,
overlap: f64,
tol: Option<f64>,
max_iter: Option<u64>,
) -> Result<f64, Error> {
let min_distance = (r1 - r2).abs();
let max_distance = r1 + r2;
if overlap <= 0.0 {
return Ok(max_distance);
}
let cost_fun = SeparationCost {
r1,
r2,
target_overlap: overlap,
};
let solver = BrentOpt::new(min_distance, max_distance);
let result = Executor::new(cost_fun, solver)
.configure(|state| {
state
.max_iters(max_iter.unwrap_or(1000))
.target_cost(tol.unwrap_or(f64::EPSILON.sqrt())) })
.run()?;
Ok(*result.state.get_best_param().unwrap())
}
#[deprecated(
since = "0.3.1",
note = "Returns wrong area when one circle in the mask contains the others' lens. Use `crate::geometry::diagram::compute_exclusive_regions` or the boundary-arc helpers (`region_boundary_arcs` + `area_from_boundary_arcs`) instead."
)]
pub fn multiple_overlap_areas(circles: &[Circle], points: &[IntersectionPoint]) -> f64 {
let n_circles = circles.len();
let full_intersection_points: Vec<&IntersectionPoint> = points
.iter()
.filter(|ip| ip.adopters().len() == n_circles)
.collect();
if full_intersection_points.is_empty() {
return 0.0;
}
let n_points = full_intersection_points.len();
let centroid = point::centroid(
&full_intersection_points
.iter()
.map(|ip| *ip.point())
.collect::<Vec<Point>>(),
);
let mut indices: Vec<usize> = (0..n_points).collect();
indices.sort_by(|&i, &j| {
full_intersection_points[i]
.point()
.angle_to(¢roid)
.partial_cmp(&full_intersection_points[j].point().angle_to(¢roid))
.unwrap_or(std::cmp::Ordering::Less)
});
let mut area = 0.0;
let mut l = n_points - 1;
for k in 0..n_points {
let i = indices[k];
let j = indices[l];
let p1 = &full_intersection_points[i].point();
let p2 = &full_intersection_points[j].point();
let parents1 = &full_intersection_points[i].parents();
let parents2 = &full_intersection_points[j].parents();
let common_parents: Vec<usize> = vec![parents1.0, parents1.1]
.into_iter()
.filter(|p| *p == parents2.0 || *p == parents2.1)
.collect();
let mut segment_areas = Vec::with_capacity(common_parents.len());
if common_parents.is_empty() {
panic!("No common parent circles found for intersection points");
}
for &circle_index in &common_parents {
let circle = &circles[circle_index];
let seg_area = circle.segment_area_from_points(p1, p2);
debug_assert!(seg_area >= 0.0, "Segment area should be non-negative");
segment_areas.push(seg_area);
}
let triangle_area = 0.5 * ((p1.x() + p2.x()) * (p1.y() - p2.y()));
let min_segment = segment_areas
.into_iter()
.fold(f64::INFINITY, |a, b| a.min(b));
area += triangle_area;
area += min_segment;
l = k;
}
area.abs()
}
#[deprecated(
since = "0.3.1",
note = "Returns wrong area when one circle in the mask contains the others' lens. Use `crate::geometry::diagram::compute_exclusive_regions` or the boundary-arc helpers (`region_boundary_arcs` + `area_from_boundary_arcs`) instead."
)]
pub fn multiple_overlap_areas_with_mask(
circles: &[Circle],
points: &[IntersectionPoint],
circle_indices: &[usize],
) -> f64 {
let region_points: Vec<&IntersectionPoint> = points
.iter()
.filter(|ip| {
circle_indices
.iter()
.all(|&idx| ip.adopters().contains(&idx))
})
.collect();
if region_points.is_empty() {
return 0.0;
}
let n_points = region_points.len();
let centroid = point::centroid(
®ion_points
.iter()
.map(|ip| *ip.point())
.collect::<Vec<Point>>(),
);
let mut indices: Vec<usize> = (0..n_points).collect();
indices.sort_by(|&i, &j| {
region_points[i]
.point()
.angle_to(¢roid)
.partial_cmp(®ion_points[j].point().angle_to(¢roid))
.unwrap_or(std::cmp::Ordering::Less)
});
let mut area = 0.0;
let mut l = n_points - 1;
for k in 0..n_points {
let i = indices[k];
let j = indices[l];
let p1 = ®ion_points[i].point();
let p2 = ®ion_points[j].point();
let parents1 = ®ion_points[i].parents();
let parents2 = ®ion_points[j].parents();
let common_parents: Vec<usize> = vec![parents1.0, parents1.1]
.into_iter()
.filter(|p| *p == parents2.0 || *p == parents2.1)
.filter(|p| circle_indices.contains(p)) .collect();
let mut segment_areas = Vec::with_capacity(common_parents.len());
if common_parents.is_empty() {
for &circle_idx in circle_indices {
let circle = &circles[circle_idx];
if circle.contains_point(p1) && circle.contains_point(p2) {
let seg_area = circle.segment_area_from_points(p1, p2);
segment_areas.push(seg_area);
}
}
if segment_areas.is_empty() {
l = k;
continue;
}
} else {
for &circle_index in &common_parents {
let circle = &circles[circle_index];
let seg_area = circle.segment_area_from_points(p1, p2);
debug_assert!(seg_area >= 0.0, "Segment area should be non-negative");
segment_areas.push(seg_area);
}
}
let triangle_area = 0.5 * ((p1.x() + p2.x()) * (p1.y() - p2.y()));
let min_segment = segment_areas
.into_iter()
.fold(f64::INFINITY, |a, b| a.min(b));
area += triangle_area;
area += min_segment;
l = k;
}
area.abs()
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct BoundaryArc {
pub circle: usize,
pub phi_start: f64,
pub phi_end: f64,
pub delta_phi: f64,
}
#[inline]
fn circle_implicit_value(p: &Point, c: &Circle) -> f64 {
let dx = p.x() - c.center().x();
let dy = p.y() - c.center().y();
(dx * dx + dy * dy) / (c.radius() * c.radius())
}
fn arc_midpoint_owned_by_j(j: usize, mid: &Point, indices: &[usize], circles: &[Circle]) -> bool {
for &l in indices {
if l == j {
continue;
}
let cl = &circles[l];
let eps = (2.0 * BOUNDARY_COINCIDENCE_GEOM_TOL / cl.radius()).max(IMPLICIT_VALUE_FP_TOL);
let v = circle_implicit_value(mid, cl);
if v > 1.0 + eps {
return false;
}
if (v - 1.0).abs() <= eps && l < j {
return false;
}
}
true
}
const BOUNDARY_COINCIDENCE_GEOM_TOL: f64 = 1e-7;
const IMPLICIT_VALUE_FP_TOL: f64 = 1e-12;
pub(crate) fn region_boundary_arcs(
mask: crate::geometry::diagram::RegionMask,
circles: &[Circle],
intersections: &[IntersectionPoint],
n_sets: usize,
) -> Vec<BoundaryArc> {
use crate::geometry::diagram::{adopters_to_mask, mask_to_indices};
let circle_count = mask.count_ones();
if circle_count == 0 {
return Vec::new();
}
if circle_count == 1 {
let idx = mask.trailing_zeros() as usize;
return vec![BoundaryArc {
circle: idx,
phi_start: 0.0,
phi_end: 0.0,
delta_phi: 2.0 * PI,
}];
}
let indices = mask_to_indices(mask, n_sets);
let mut arcs = Vec::new();
let two_pi = 2.0 * PI;
let arc_eps = 1e-9;
for &j in &indices {
let cj = &circles[j];
let mut j_phis: Vec<f64> = intersections
.iter()
.filter(|ip| {
let (p1, p2) = ip.parents();
if p1 != j && p2 != j {
return false;
}
let am = adopters_to_mask(ip.adopters());
(mask & am) == mask
})
.map(|ip| {
let p = ip.point();
(p.y() - cj.center().y()).atan2(p.x() - cj.center().x())
})
.collect();
if j_phis.is_empty() {
let probe = Point::new(cj.center().x() + cj.radius(), cj.center().y());
if arc_midpoint_owned_by_j(j, &probe, &indices, circles) {
arcs.push(BoundaryArc {
circle: j,
phi_start: 0.0,
phi_end: 0.0,
delta_phi: two_pi,
});
}
continue;
}
j_phis.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let m = j_phis.len();
for k in 0..m {
let phi_a = j_phis[k];
let phi_b = j_phis[(k + 1) % m];
let mut delta = phi_b - phi_a;
if delta <= 0.0 {
delta += two_pi;
}
if delta < arc_eps || delta > two_pi - arc_eps {
continue;
}
let phi_mid = phi_a + delta * 0.5;
let mid = Point::new(
cj.center().x() + cj.radius() * phi_mid.cos(),
cj.center().y() + cj.radius() * phi_mid.sin(),
);
if !arc_midpoint_owned_by_j(j, &mid, &indices, circles) {
continue;
}
let phi_end_canonical = wrap_angle(phi_a + delta);
arcs.push(BoundaryArc {
circle: j,
phi_start: phi_a,
phi_end: phi_end_canonical,
delta_phi: delta,
});
}
}
arcs
}
pub(crate) fn area_from_boundary_arcs(arcs: &[BoundaryArc], circles: &[Circle]) -> f64 {
let mut total = 0.0;
for arc in arcs {
let cj = &circles[arc.circle];
let xk = cj.center().x();
let yk = cj.center().y();
let r = cj.radius();
let phi_a = arc.phi_start;
let phi_b = phi_a + arc.delta_phi;
total += xk * r * (phi_b.sin() - phi_a.sin());
total += yk * r * (phi_a.cos() - phi_b.cos());
total += r * r * arc.delta_phi;
}
0.5 * total
}
#[inline]
pub(crate) fn wrap_angle(x: f64) -> f64 {
let two_pi = 2.0 * PI;
let y = x.rem_euclid(two_pi);
if y > PI {
y - two_pi
} else {
y
}
}
pub(crate) fn accumulate_region_overlap_gradient(
arcs: &[BoundaryArc],
circles: &[Circle],
grad: &mut [f64],
) {
for arc in arcs {
let r = circles[arc.circle].radius();
let sign = arc.delta_phi.signum();
let off = arc.circle * 3;
grad[off] += sign * r * (arc.phi_end.sin() - arc.phi_start.sin());
grad[off + 1] -= sign * r * (arc.phi_end.cos() - arc.phi_start.cos());
grad[off + 2] += r * arc.delta_phi.abs();
}
}
pub fn compute_exclusive_regions_clipped(
circles: &[Circle],
container: &Rectangle,
) -> std::collections::HashMap<crate::geometry::diagram::RegionMask, f64> {
use crate::geometry::diagram::{collect_intersections, discover_regions, to_exclusive_areas};
let n_sets = circles.len();
let intersections = collect_intersections(circles, n_sets);
let regions = discover_regions(circles, &intersections, n_sets);
let mut overlapping_areas = std::collections::HashMap::new();
overlapping_areas.insert(0, container.area());
for &mask in ®ions {
let arcs = region_boundary_arcs(mask, circles, &intersections, n_sets);
let area = area_from_clipped_arcs(&arcs, circles, container, mask, n_sets);
overlapping_areas.insert(mask, area);
}
to_exclusive_areas(&overlapping_areas)
}
pub(crate) fn compute_exclusive_regions_clipped_with_gradient(
circles: &[Circle],
container: &Rectangle,
) -> (
std::collections::HashMap<crate::geometry::diagram::RegionMask, f64>,
std::collections::HashMap<crate::geometry::diagram::RegionMask, Vec<f64>>,
) {
use crate::geometry::diagram::{
collect_intersections, discover_regions, to_exclusive_areas_and_gradients,
};
let n_sets = circles.len();
let n_params = n_sets * 3 + 4;
let intersections = collect_intersections(circles, n_sets);
let regions = discover_regions(circles, &intersections, n_sets);
let mut overlapping_areas = std::collections::HashMap::new();
let mut overlapping_grads: std::collections::HashMap<
crate::geometry::diagram::RegionMask,
Vec<f64>,
> = std::collections::HashMap::new();
let container_area = container.area();
overlapping_areas.insert(0, container_area);
let mut zero_grad = vec![0.0; n_params];
zero_grad[n_sets * 3 + 2] = container_area;
overlapping_grads.insert(0, zero_grad);
for &mask in ®ions {
let arcs = region_boundary_arcs(mask, circles, &intersections, n_sets);
let mut grad = vec![0.0; n_params];
let area =
area_and_gradient_from_clipped_arcs(&arcs, circles, container, mask, n_sets, &mut grad);
overlapping_areas.insert(mask, area);
overlapping_grads.insert(mask, grad);
}
to_exclusive_areas_and_gradients(&overlapping_areas, &overlapping_grads, n_params)
}
pub(crate) fn area_from_clipped_arcs(
arcs: &[BoundaryArc],
circles: &[Circle],
container: &Rectangle,
mask: crate::geometry::diagram::RegionMask,
n_sets: usize,
) -> f64 {
use crate::geometry::diagram::mask_to_indices;
let (x_min, x_max, y_min, y_max) = container.bounds();
if x_max <= x_min || y_max <= y_min {
return 0.0;
}
let mut total = 0.0;
for arc in arcs {
total += clipped_arc_integral(arc, circles, x_min, x_max, y_min, y_max);
}
let indices = mask_to_indices(mask, n_sets);
if let Some((a, b)) = horizontal_edge_inside_interval(y_min, x_min, x_max, &indices, circles) {
total += -y_min * (b - a);
}
if let Some((a, b)) = vertical_edge_inside_interval(x_max, y_min, y_max, &indices, circles) {
total += x_max * (b - a);
}
if let Some((a, b)) = horizontal_edge_inside_interval(y_max, x_min, x_max, &indices, circles) {
total += y_max * (b - a);
}
if let Some((a, b)) = vertical_edge_inside_interval(x_min, y_min, y_max, &indices, circles) {
total += -x_min * (b - a);
}
0.5 * total
}
pub(crate) fn area_and_gradient_from_clipped_arcs(
arcs: &[BoundaryArc],
circles: &[Circle],
container: &Rectangle,
mask: crate::geometry::diagram::RegionMask,
n_sets: usize,
grad: &mut [f64],
) -> f64 {
use crate::geometry::diagram::mask_to_indices;
let (x_min, x_max, y_min, y_max) = container.bounds();
if x_max <= x_min || y_max <= y_min {
return 0.0;
}
let w = x_max - x_min;
let h = y_max - y_min;
let container_off = n_sets * 3;
let mut total = 0.0;
for arc in arcs {
total += clipped_arc_integral_with_gradient(arc, circles, x_min, x_max, y_min, y_max, grad);
}
let indices = mask_to_indices(mask, n_sets);
if let Some((a, b)) = horizontal_edge_inside_interval(y_min, x_min, x_max, &indices, circles) {
let l = b - a;
total += -y_min * l;
grad[container_off + 1] -= l;
grad[container_off + 2] += (h / 4.0) * l;
grad[container_off + 3] -= (h / 4.0) * l;
}
if let Some((a, b)) = vertical_edge_inside_interval(x_max, y_min, y_max, &indices, circles) {
let l = b - a;
total += x_max * l;
grad[container_off] += l;
grad[container_off + 2] += (w / 4.0) * l;
grad[container_off + 3] += (w / 4.0) * l;
}
if let Some((a, b)) = horizontal_edge_inside_interval(y_max, x_min, x_max, &indices, circles) {
let l = b - a;
total += y_max * l;
grad[container_off + 1] += l;
grad[container_off + 2] += (h / 4.0) * l;
grad[container_off + 3] -= (h / 4.0) * l;
}
if let Some((a, b)) = vertical_edge_inside_interval(x_min, y_min, y_max, &indices, circles) {
let l = b - a;
total += -x_min * l;
grad[container_off] -= l;
grad[container_off + 2] += (w / 4.0) * l;
grad[container_off + 3] += (w / 4.0) * l;
}
0.5 * total
}
fn clipped_arc_integral(
arc: &BoundaryArc,
circles: &[Circle],
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
) -> f64 {
let cj = &circles[arc.circle];
let xc = cj.center().x();
let yc = cj.center().y();
let r = cj.radius();
let phi_a = arc.phi_start;
let phi_b = phi_a + arc.delta_phi;
let mut crossings: Vec<f64> = Vec::with_capacity(8);
for &x_edge in &[x_min, x_max] {
let c = (x_edge - xc) / r;
if c.abs() <= 1.0 {
let phi0 = c.acos(); collect_periodic_in_range(&mut crossings, phi0, phi_a, phi_b);
collect_periodic_in_range(&mut crossings, -phi0, phi_a, phi_b);
}
}
for &y_edge in &[y_min, y_max] {
let s = (y_edge - yc) / r;
if s.abs() <= 1.0 {
let phi0 = s.asin(); let phi1 = std::f64::consts::PI - phi0;
collect_periodic_in_range(&mut crossings, phi0, phi_a, phi_b);
collect_periodic_in_range(&mut crossings, phi1, phi_a, phi_b);
}
}
let mut breaks: Vec<f64> = Vec::with_capacity(crossings.len() + 2);
breaks.push(phi_a);
breaks.extend(crossings);
breaks.push(phi_b);
breaks.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mut sum = 0.0;
for w in breaks.windows(2) {
let a = w[0];
let b = w[1];
let delta = b - a;
if delta <= 1e-12 {
continue;
}
let mid = 0.5 * (a + b);
let mx = xc + r * mid.cos();
let my = yc + r * mid.sin();
let tol = 1e-9;
let inside =
mx >= x_min - tol && mx <= x_max + tol && my >= y_min - tol && my <= y_max + tol;
if !inside {
continue;
}
sum += xc * r * (b.sin() - a.sin()) + yc * r * (a.cos() - b.cos()) + r * r * (b - a);
}
sum
}
fn clipped_arc_integral_with_gradient(
arc: &BoundaryArc,
circles: &[Circle],
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
grad: &mut [f64],
) -> f64 {
let cj = &circles[arc.circle];
let xc = cj.center().x();
let yc = cj.center().y();
let r = cj.radius();
let phi_a = arc.phi_start;
let phi_b = phi_a + arc.delta_phi;
let mut crossings: Vec<f64> = Vec::with_capacity(8);
for &x_edge in &[x_min, x_max] {
let c = (x_edge - xc) / r;
if c.abs() <= 1.0 {
let phi0 = c.acos();
collect_periodic_in_range(&mut crossings, phi0, phi_a, phi_b);
collect_periodic_in_range(&mut crossings, -phi0, phi_a, phi_b);
}
}
for &y_edge in &[y_min, y_max] {
let s = (y_edge - yc) / r;
if s.abs() <= 1.0 {
let phi0 = s.asin();
let phi1 = std::f64::consts::PI - phi0;
collect_periodic_in_range(&mut crossings, phi0, phi_a, phi_b);
collect_periodic_in_range(&mut crossings, phi1, phi_a, phi_b);
}
}
let mut breaks: Vec<f64> = Vec::with_capacity(crossings.len() + 2);
breaks.push(phi_a);
breaks.extend(crossings);
breaks.push(phi_b);
breaks.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let off = arc.circle * 3;
let mut sum = 0.0;
for w in breaks.windows(2) {
let a = w[0];
let b = w[1];
let delta = b - a;
if delta <= 1e-12 {
continue;
}
let mid = 0.5 * (a + b);
let mx = xc + r * mid.cos();
let my = yc + r * mid.sin();
let tol = 1e-9;
let inside =
mx >= x_min - tol && mx <= x_max + tol && my >= y_min - tol && my <= y_max + tol;
if !inside {
continue;
}
let s_diff = b.sin() - a.sin();
let c_diff = b.cos() - a.cos();
sum += xc * r * s_diff - yc * r * c_diff + r * r * delta;
grad[off] += r * s_diff;
grad[off + 1] -= r * c_diff;
grad[off + 2] += r * delta;
}
sum
}
fn collect_periodic_in_range(out: &mut Vec<f64>, cand: f64, phi_a: f64, phi_b: f64) {
let two_pi = 2.0 * std::f64::consts::PI;
let tol = 1e-12;
for k in -1..=1 {
let p = cand + (k as f64) * two_pi;
if p > phi_a + tol && p < phi_b - tol {
out.push(p);
}
}
}
fn horizontal_edge_inside_interval(
y: f64,
x_min: f64,
x_max: f64,
indices: &[usize],
circles: &[Circle],
) -> Option<(f64, f64)> {
let mut lo = x_min;
let mut hi = x_max;
for &i in indices {
let c = &circles[i];
let dy = y - c.center().y();
let r = c.radius();
let r2 = r * r;
if dy * dy > r2 {
return None;
}
let dx = (r2 - dy * dy).max(0.0).sqrt();
let disk_lo = c.center().x() - dx;
let disk_hi = c.center().x() + dx;
if disk_lo > lo {
lo = disk_lo;
}
if disk_hi < hi {
hi = disk_hi;
}
if hi <= lo {
return None;
}
}
if hi > lo {
Some((lo, hi))
} else {
None
}
}
fn vertical_edge_inside_interval(
x: f64,
y_min: f64,
y_max: f64,
indices: &[usize],
circles: &[Circle],
) -> Option<(f64, f64)> {
let mut lo = y_min;
let mut hi = y_max;
for &i in indices {
let c = &circles[i];
let dx = x - c.center().x();
let r = c.radius();
let r2 = r * r;
if dx * dx > r2 {
return None;
}
let dy = (r2 - dx * dx).max(0.0).sqrt();
let disk_lo = c.center().y() - dy;
let disk_hi = c.center().y() + dy;
if disk_lo > lo {
lo = disk_lo;
}
if disk_hi < hi {
hi = disk_hi;
}
if hi <= lo {
return None;
}
}
if hi > lo {
Some((lo, hi))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-10;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn test_circle_new() {
let center = Point::new(1.0, 2.0);
let circle = Circle::new(center, 5.0);
assert_eq!(circle.radius(), 5.0);
assert_eq!(circle.center().x(), 1.0);
assert_eq!(circle.center().y(), 2.0);
}
#[test]
fn test_circle_try_new_accepts_positive() {
let circle = Circle::try_new(Point::new(0.0, 0.0), 1.0).unwrap();
assert_eq!(circle.radius(), 1.0);
}
#[test]
fn test_circle_try_new_rejects_zero_and_negative() {
let err = Circle::try_new(Point::new(0.0, 0.0), 0.0).unwrap_err();
assert!(matches!(
err,
crate::error::DiagramError::InvalidShapeParameter {
shape: "Circle",
param: "radius",
..
}
));
let err = Circle::try_new(Point::new(0.0, 0.0), -1.5).unwrap_err();
assert!(matches!(
err,
crate::error::DiagramError::InvalidShapeParameter {
shape: "Circle",
param: "radius",
..
}
));
}
#[test]
#[should_panic(expected = "Circle radius must be > 0")]
fn test_circle_new_panics_on_zero_radius() {
let _ = Circle::new(Point::new(0.0, 0.0), 0.0);
}
#[test]
fn test_circle_area() {
let circle = Circle::new(Point::new(0.0, 0.0), 1.0);
assert!(approx_eq(circle.area(), PI));
let circle2 = Circle::new(Point::new(0.0, 0.0), 2.0);
assert!(approx_eq(circle2.area(), 4.0 * PI));
let circle3 = Circle::new(Point::new(5.0, 5.0), 3.0);
assert!(approx_eq(circle3.area(), 9.0 * PI));
}
#[test]
fn test_circle_distance_no_overlap() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let circle2 = Circle::new(Point::new(5.0, 0.0), 1.0);
assert_eq!(circle1.distance(&circle2), 3.0);
}
#[test]
fn test_circle_distance_touching() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let circle2 = Circle::new(Point::new(2.0, 0.0), 1.0);
assert_eq!(circle1.distance(&circle2), 0.0);
}
#[test]
fn test_circle_distance_overlapping() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 2.0);
let circle2 = Circle::new(Point::new(1.0, 0.0), 2.0);
assert_eq!(circle1.distance(&circle2), 0.0);
}
#[test]
fn test_circle_contains_smaller() {
let large = Circle::new(Point::new(0.0, 0.0), 5.0);
let small = Circle::new(Point::new(1.0, 1.0), 2.0);
assert!(large.contains(&small));
}
#[test]
fn test_circle_contains_self() {
let circle = Circle::new(Point::new(0.0, 0.0), 3.0);
assert!(circle.contains(&circle));
}
#[test]
fn test_circle_not_contains() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 2.0);
let circle2 = Circle::new(Point::new(5.0, 0.0), 2.0);
assert!(!circle1.contains(&circle2));
}
#[test]
fn test_circle_not_contains_partial_overlap() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 3.0);
let circle2 = Circle::new(Point::new(2.0, 0.0), 2.0);
assert!(!circle1.contains(&circle2));
}
#[test]
fn test_circle_intersects_separate() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let circle2 = Circle::new(Point::new(5.0, 0.0), 1.0);
assert!(!circle1.intersects(&circle2));
}
#[test]
fn test_circle_intersects_touching() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let circle2 = Circle::new(Point::new(2.0, 0.0), 1.0);
assert!(!circle1.intersects(&circle2));
}
#[test]
fn test_circle_intersects_overlapping() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 2.0);
let circle2 = Circle::new(Point::new(1.0, 0.0), 2.0);
assert!(circle1.intersects(&circle2));
}
#[test]
fn test_intersection_area_no_overlap() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let circle2 = Circle::new(Point::new(10.0, 0.0), 1.0);
assert_eq!(circle1.intersection_area(&circle2), 0.0);
}
#[test]
fn test_intersection_area_touching() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let circle2 = Circle::new(Point::new(2.0, 0.0), 1.0);
let area = circle1.intersection_area(&circle2);
assert!(approx_eq(area, 0.0));
}
#[test]
fn test_intersection_area_complete_overlap_same_size() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 2.0);
let circle2 = Circle::new(Point::new(0.0, 0.0), 2.0);
let expected = PI * 4.0;
assert!(approx_eq(circle1.intersection_area(&circle2), expected));
}
#[test]
fn test_intersection_area_one_inside_other() {
let large = Circle::new(Point::new(0.0, 0.0), 5.0);
let small = Circle::new(Point::new(1.0, 0.0), 2.0);
let expected = PI * 4.0; assert!(approx_eq(large.intersection_area(&small), expected));
assert!(approx_eq(small.intersection_area(&large), expected));
}
#[test]
fn test_intersection_area_partial_overlap() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let circle2 = Circle::new(Point::new(1.0, 0.0), 1.0);
let area = circle1.intersection_area(&circle2);
assert!(area > 0.0);
assert!(area < PI);
}
#[test]
fn test_intersection_area_symmetric() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 2.0);
let circle2 = Circle::new(Point::new(1.5, 0.0), 1.5);
let area1 = circle1.intersection_area(&circle2);
let area2 = circle2.intersection_area(&circle1);
assert!(approx_eq(area1, area2));
}
#[test]
fn test_intersection_area_different_sizes() {
let circle1 = Circle::new(Point::new(0.0, 0.0), 3.0);
let circle2 = Circle::new(Point::new(2.0, 0.0), 1.0);
let area = circle1.intersection_area(&circle2);
assert!(area > 0.0);
assert!(area <= PI * 1.0 * 1.0);
}
#[test]
fn test_distance_for_overlap_zero_overlap() {
let r1 = 2.0;
let r2 = 1.5;
let overlap = 0.0;
let distance = distance_for_overlap(r1, r2, overlap, None, None).unwrap();
assert!(approx_eq(distance, r1 + r2));
}
#[test]
fn test_distance_for_overlap_negative_overlap() {
let r1 = 2.0;
let r2 = 1.5;
let overlap = -1.0;
let distance = distance_for_overlap(r1, r2, overlap, None, None).unwrap();
assert!(approx_eq(distance, r1 + r2));
}
#[test]
fn test_distance_for_overlap_full_overlap() {
let r1 = 3.0;
let r2 = 2.0;
let overlap = PI * r2 * r2;
let distance = distance_for_overlap(r1, r2, overlap, None, None).unwrap();
assert!(distance <= (r1 - r2).abs() + 0.1); }
#[test]
fn test_distance_for_overlap_partial_overlap_equal_radii() {
let r1 = 2.0;
let r2 = 2.0;
let target_overlap = 2.0;
let distance = distance_for_overlap(r1, r2, target_overlap, None, None).unwrap();
let c1 = Circle::new(Point::new(0.0, 0.0), r1);
let c2 = Circle::new(Point::new(distance, 0.0), r2);
let actual_overlap = c1.intersection_area(&c2);
assert!((actual_overlap - target_overlap).abs() < 1e-2);
}
#[test]
fn test_distance_for_overlap_partial_overlap_different_radii() {
let r1 = 3.0;
let r2 = 1.5;
let target_overlap = 1.0;
let distance = distance_for_overlap(r1, r2, target_overlap, None, None).unwrap();
let c1 = Circle::new(Point::new(0.0, 0.0), r1);
let c2 = Circle::new(Point::new(distance, 0.0), r2);
let actual_overlap = c1.intersection_area(&c2);
assert!((actual_overlap - target_overlap).abs() < 1e-2);
}
#[test]
fn test_distance_for_overlap_custom_tolerance() {
let r1 = 2.0;
let r2 = 1.0;
let target_overlap = 0.5;
let custom_tol = 1e-8;
let distance =
distance_for_overlap(r1, r2, target_overlap, Some(custom_tol), None).unwrap();
let c1 = Circle::new(Point::new(0.0, 0.0), r1);
let c2 = Circle::new(Point::new(distance, 0.0), r2);
let actual_overlap = c1.intersection_area(&c2);
assert!((actual_overlap - target_overlap).abs() < 1e-3);
}
#[test]
fn test_distance_for_overlap_custom_max_iter() {
let r1 = 2.0;
let r2 = 1.5;
let target_overlap = 1.0;
let max_iter = 50;
let distance = distance_for_overlap(r1, r2, target_overlap, None, Some(max_iter)).unwrap();
let c1 = Circle::new(Point::new(0.0, 0.0), r1);
let c2 = Circle::new(Point::new(distance, 0.0), r2);
let actual_overlap = c1.intersection_area(&c2);
assert!((actual_overlap - target_overlap).abs() < 1e-2);
}
#[test]
fn test_distance_for_overlap_small_circles() {
let r1 = 0.5;
let r2 = 0.3;
let target_overlap = 0.1;
let distance = distance_for_overlap(r1, r2, target_overlap, None, None).unwrap();
let c1 = Circle::new(Point::new(0.0, 0.0), r1);
let c2 = Circle::new(Point::new(distance, 0.0), r2);
let actual_overlap = c1.intersection_area(&c2);
assert!((actual_overlap - target_overlap).abs() < 1e-4);
}
#[test]
fn test_distance_for_overlap_large_circles() {
let r1 = 100.0;
let r2 = 75.0;
let target_overlap = 500.0;
let distance = distance_for_overlap(r1, r2, target_overlap, None, None).unwrap();
let c1 = Circle::new(Point::new(0.0, 0.0), r1);
let c2 = Circle::new(Point::new(distance, 0.0), r2);
let actual_overlap = c1.intersection_area(&c2);
assert!((actual_overlap - target_overlap).abs() < 1.0); }
#[test]
fn test_distance_for_overlap_bounds() {
let r1 = 2.0;
let r2 = 1.5;
let target_overlap = 1.0;
let distance = distance_for_overlap(r1, r2, target_overlap, None, None).unwrap();
let min_distance = (r1 - r2).abs();
let max_distance = r1 + r2;
assert!(distance >= min_distance);
assert!(distance <= max_distance);
}
#[test]
fn test_intersection_points_no_intersection() {
let c1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let c2 = Circle::new(Point::new(5.0, 0.0), 1.0);
let points = c1.intersection_points(&c2);
assert_eq!(points.len(), 0);
}
#[test]
fn test_intersection_points_one_inside_other() {
let c1 = Circle::new(Point::new(0.0, 0.0), 3.0);
let c2 = Circle::new(Point::new(0.0, 0.0), 1.0);
let points = c1.intersection_points(&c2);
assert_eq!(points.len(), 0);
}
#[test]
fn test_intersection_points_touching_externally() {
let c1 = Circle::new(Point::new(0.0, 0.0), 2.0);
let c2 = Circle::new(Point::new(4.0, 0.0), 2.0);
let points = c1.intersection_points(&c2);
assert_eq!(points.len(), 1);
assert!(approx_eq(points[0].x(), 2.0));
assert!(approx_eq(points[0].y(), 0.0));
}
#[test]
fn test_intersection_points_two_points() {
let c1 = Circle::new(Point::new(0.0, 0.0), 2.0);
let c2 = Circle::new(Point::new(2.0, 0.0), 2.0);
let points = c1.intersection_points(&c2);
assert_eq!(points.len(), 2);
for point in &points {
let dist_to_c1 = c1.center.distance(point);
let dist_to_c2 = c2.center.distance(point);
assert!(approx_eq(dist_to_c1, c1.radius));
assert!(approx_eq(dist_to_c2, c2.radius));
}
let expected_x = 1.0;
let expected_y = 3.0_f64.sqrt();
let found_positive = points
.iter()
.any(|p| approx_eq(p.x(), expected_x) && approx_eq(p.y(), expected_y));
let found_negative = points
.iter()
.any(|p| approx_eq(p.x(), expected_x) && approx_eq(p.y(), -expected_y));
assert!(found_positive);
assert!(found_negative);
}
#[test]
fn test_intersection_points_vertical_alignment() {
let c1 = Circle::new(Point::new(0.0, 0.0), 1.5);
let c2 = Circle::new(Point::new(0.0, 2.0), 1.5);
let points = c1.intersection_points(&c2);
assert_eq!(points.len(), 2);
for point in &points {
let dist_to_c1 = c1.center.distance(point);
let dist_to_c2 = c2.center.distance(point);
assert!(approx_eq(dist_to_c1, c1.radius));
assert!(approx_eq(dist_to_c2, c2.radius));
}
}
#[test]
fn test_intersection_points_equal_radii_partial_overlap() {
let c1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let c2 = Circle::new(Point::new(1.0, 0.0), 1.0);
let points = c1.intersection_points(&c2);
assert_eq!(points.len(), 2);
for point in &points {
assert!(approx_eq(point.x(), 0.5));
}
}
#[test]
fn test_intersection_points_different_radii() {
let c1 = Circle::new(Point::new(0.0, 0.0), 3.0);
let c2 = Circle::new(Point::new(2.0, 0.0), 1.5);
let points = c1.intersection_points(&c2);
assert_eq!(points.len(), 2);
for point in &points {
let dist_to_c1 = c1.center.distance(point);
let dist_to_c2 = c2.center.distance(point);
assert!(approx_eq(dist_to_c1, c1.radius));
assert!(approx_eq(dist_to_c2, c2.radius));
}
}
#[test]
fn test_perimeter() {
let c = Circle::new(Point::new(0.0, 0.0), 1.0);
let perimeter = c.perimeter();
assert!(approx_eq(perimeter, 2.0 * PI));
}
#[test]
fn test_perimeter_various_radii() {
let test_cases = vec![(0.5, PI), (2.0, 4.0 * PI), (10.0, 20.0 * PI)];
for (radius, expected) in test_cases {
let c = Circle::new(Point::new(0.0, 0.0), radius);
assert!(approx_eq(c.perimeter(), expected));
}
}
#[test]
fn test_sector_area_full_circle() {
let c = Circle::new(Point::new(0.0, 0.0), 2.0);
let full_circle_angle = 2.0 * PI;
let sector = c.sector_area(full_circle_angle);
assert!(approx_eq(sector, c.area()));
}
#[test]
fn test_sector_area_half_circle() {
let c = Circle::new(Point::new(0.0, 0.0), 3.0);
let half_circle_angle = PI;
let sector = c.sector_area(half_circle_angle);
assert!(approx_eq(sector, c.area() / 2.0));
}
#[test]
fn test_sector_area_quarter_circle() {
let c = Circle::new(Point::new(0.0, 0.0), 4.0);
let quarter_circle_angle = PI / 2.0;
let sector = c.sector_area(quarter_circle_angle);
assert!(approx_eq(sector, c.area() / 4.0));
}
#[test]
fn test_sector_area_zero() {
let c = Circle::new(Point::new(0.0, 0.0), 5.0);
let sector = c.sector_area(0.0);
assert!(approx_eq(sector, 0.0));
}
#[test]
fn test_segment_area_from_angle_semicircle() {
let c = Circle::new(Point::new(0.0, 0.0), 2.0);
let angle = PI;
let segment = c.segment_area_from_angle(angle);
assert!(approx_eq(segment, c.area() / 2.0));
}
#[test]
fn test_segment_area_from_angle_zero() {
let c = Circle::new(Point::new(0.0, 0.0), 3.0);
let segment = c.segment_area_from_angle(0.0);
assert!(approx_eq(segment, 0.0));
}
#[test]
fn test_segment_area_from_angle_small() {
let c = Circle::new(Point::new(0.0, 0.0), 1.0);
let angle = PI / 6.0; let segment = c.segment_area_from_angle(angle);
let sector = c.sector_area(angle);
assert!(segment > 0.0);
assert!(segment < sector);
}
#[test]
fn test_segment_area_from_chord_diameter() {
let radius = 2.0;
let c = Circle::new(Point::new(0.0, 0.0), radius);
let chord_length = 2.0 * radius; let segment = c.segment_area_from_chord(chord_length);
assert!(approx_eq(segment, c.area() / 2.0));
}
#[test]
fn test_segment_area_from_chord_small() {
let c = Circle::new(Point::new(0.0, 0.0), 5.0);
let chord_length = 2.0;
let segment = c.segment_area_from_chord(chord_length);
assert!(segment > 0.0);
assert!(segment < c.area() / 4.0);
}
#[test]
fn test_segment_area_from_chord_vs_angle_consistency() {
let c = Circle::new(Point::new(0.0, 0.0), 3.0);
let angle = PI / 3.0;
let chord_length = 2.0 * c.radius() * (angle / 2.0).sin();
let segment_from_angle = c.segment_area_from_angle(angle);
let segment_from_chord = c.segment_area_from_chord(chord_length);
assert!(approx_eq(segment_from_angle, segment_from_chord));
}
#[test]
fn test_segment_area_relationships() {
let c = Circle::new(Point::new(0.0, 0.0), 1.0);
let angle = PI / 4.0;
let sector = c.sector_area(angle);
let segment = c.segment_area_from_angle(angle);
assert!(segment < sector);
let small_angle = 0.1;
let small_sector = c.sector_area(small_angle);
let small_segment = c.segment_area_from_angle(small_angle);
assert!(small_segment < small_sector / 2.0);
}
#[test]
#[should_panic(expected = "Chord length")]
fn test_segment_area_from_chord_invalid() {
let c = Circle::new(Point::new(0.0, 0.0), 2.0);
let chord_length = 5.0; c.segment_area_from_chord(chord_length);
}
#[test]
fn test_three_circle_complete_overlap() {
use crate::geometry::traits::DiagramShape;
let c = Circle::new(Point::new(0.0, 0.0), 1.0);
let areas = Circle::compute_exclusive_regions(&[c, c, c]);
let mask_all = 0b111;
let all_three = areas.get(&mask_all).copied().unwrap_or(0.0);
assert!(
(all_three - PI).abs() < 1e-6,
"Complete overlap should give area ~π, got {}",
all_three
);
}
#[test]
fn test_three_circle_two_coincident_one_smaller() {
use crate::geometry::traits::DiagramShape;
let big = Circle::new(Point::new(0.0, 0.0), 1.0);
let small = Circle::new(Point::new(0.0, 0.0), 0.5);
let areas = Circle::compute_exclusive_regions(&[big, big, small]);
let mask_all = 0b111;
let all_three = areas.get(&mask_all).copied().unwrap_or(0.0);
let expected = PI * 0.25;
assert!(
(all_three - expected).abs() < 1e-6,
"Expected area {}, got {}",
expected,
all_three
);
}
#[test]
fn clipped_single_circle_inside_container() {
let circles = vec![Circle::new(Point::new(0.0, 0.0), 1.0)];
let container = Rectangle::new(Point::new(0.0, 0.0), 6.0, 6.0); let areas = compute_exclusive_regions_clipped(&circles, &container);
let disk = areas.get(&0b1).copied().unwrap_or(0.0);
let complement = areas.get(&0).copied().unwrap_or(0.0);
assert!((disk - PI).abs() < 1e-9, "disk area {} ≠ π", disk);
assert!(
(complement - (36.0 - PI)).abs() < 1e-9,
"complement {} ≠ 36 − π",
complement
);
}
#[test]
fn clipped_single_circle_outside_container() {
let circles = vec![Circle::new(Point::new(100.0, 100.0), 1.0)];
let container = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0); let areas = compute_exclusive_regions_clipped(&circles, &container);
let disk = areas.get(&0b1).copied().unwrap_or(0.0);
let complement = areas.get(&0).copied().unwrap_or(0.0);
assert!(disk.abs() < 1e-9, "disk area {} ≠ 0", disk);
assert!(
(complement - 16.0).abs() < 1e-9,
"complement {} ≠ 16",
complement
);
}
#[test]
fn clipped_single_circle_one_edge_matches_segment_formula() {
let circles = vec![Circle::new(Point::new(0.0, 0.0), 1.0)];
let container = Rectangle::new(Point::new(0.0, -2.25), 100.0, 5.5);
let areas = compute_exclusive_regions_clipped(&circles, &container);
let disk_clipped = areas.get(&0b1).copied().unwrap_or(0.0);
let theta = 2.0 * (0.5_f64).acos(); let segment = 0.5 * (theta - theta.sin());
let expected = PI - segment;
assert!(
(disk_clipped - expected).abs() < 1e-9,
"clipped disk {} ≠ π − segment ({})",
disk_clipped,
expected
);
}
#[test]
fn clipped_container_fully_inside_circle() {
let circles = vec![Circle::new(Point::new(0.0, 0.0), 100.0)];
let container = Rectangle::new(Point::new(0.0, 0.0), 4.0, 6.0);
let areas = compute_exclusive_regions_clipped(&circles, &container);
let disk = areas.get(&0b1).copied().unwrap_or(0.0);
let complement = areas.get(&0).copied().unwrap_or(0.0);
assert!(
(disk - 24.0).abs() < 1e-9,
"disk-clipped {} ≠ container area",
disk
);
assert!(complement.abs() < 1e-9, "complement {} ≠ 0", complement);
}
#[test]
fn clipped_two_disks_inside_sum_to_union_area() {
let circles = vec![
Circle::new(Point::new(-0.6, 0.0), 1.0),
Circle::new(Point::new(0.6, 0.0), 1.0),
];
let container = Rectangle::new(Point::new(0.0, 0.0), 6.0, 6.0); let areas = compute_exclusive_regions_clipped(&circles, &container);
let a_only = areas.get(&0b01).copied().unwrap_or(0.0);
let b_only = areas.get(&0b10).copied().unwrap_or(0.0);
let a_and_b = areas.get(&0b11).copied().unwrap_or(0.0);
let complement = areas.get(&0).copied().unwrap_or(0.0);
let union = a_only + b_only + a_and_b;
let lens = circles[0].intersection_area(&circles[1]);
let expected_union = 2.0 * PI - lens;
assert!(
(union - expected_union).abs() < 1e-9,
"union via per-mask sum {} ≠ {}",
union,
expected_union
);
assert!(
(complement - (36.0 - expected_union)).abs() < 1e-9,
"complement {} ≠ box − union",
complement
);
}
#[test]
fn clipped_no_circles_complement_equals_container_area() {
let circles: Vec<Circle> = Vec::new();
let container = Rectangle::new(Point::new(1.0, 2.0), 3.0, 4.0);
let areas = compute_exclusive_regions_clipped(&circles, &container);
let complement = areas.get(&0).copied().unwrap_or(0.0);
assert!(
(complement - 12.0).abs() < 1e-9,
"complement {} ≠ 12",
complement
);
}
#[test]
fn clipped_single_circle_corner_matches_monte_carlo() {
let circles = vec![Circle::new(Point::new(0.0, 0.0), 1.0)];
let container = Rectangle::new(Point::new(-0.75, -0.75), 2.5, 2.5);
let areas = compute_exclusive_regions_clipped(&circles, &container);
let kept = areas.get(&0b1).copied().unwrap_or(0.0);
let n = 400_000;
let mut hits = 0;
let mut state: u64 = 0xCAFE_BABE_DEAD_BEEF;
let next_u01 = |s: &mut u64| -> f64 {
*s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((*s >> 11) as f64) / ((1u64 << 53) as f64)
};
for _ in 0..n {
let x = -1.0 + 2.0 * next_u01(&mut state);
let y = -1.0 + 2.0 * next_u01(&mut state);
let in_disk = x * x + y * y <= 1.0;
let in_box = (-2.0..=0.5).contains(&x) && (-2.0..=0.5).contains(&y);
if in_disk && in_box {
hits += 1;
}
}
let mc_area = 4.0 * (hits as f64) / (n as f64); assert!(
(kept - mc_area).abs() < 0.02,
"clipped disk {} vs MC {}",
kept,
mc_area
);
}
fn pack_clipped_params(circles: &[Circle], container: &Rectangle) -> Vec<f64> {
let mut p = Vec::with_capacity(circles.len() * 3 + 4);
for c in circles {
p.push(c.center().x());
p.push(c.center().y());
p.push(c.radius());
}
p.extend(container.to_optimizer_params());
p
}
fn unpack_clipped_params(p: &[f64], n_sets: usize) -> (Vec<Circle>, Rectangle) {
let circles: Vec<Circle> = (0..n_sets)
.map(|i| {
Circle::new(
Point::new(p[3 * i], p[3 * i + 1]),
p[3 * i + 2].max(f64::MIN_POSITIVE),
)
})
.collect();
let container = Rectangle::from_optimizer_params(&p[3 * n_sets..3 * n_sets + 4]);
(circles, container)
}
fn assert_clipped_gradient_matches_fd(
circles: &[Circle],
container: &Rectangle,
h: f64,
tol: f64,
label: &str,
) {
let (areas, grads) = compute_exclusive_regions_clipped_with_gradient(circles, container);
let n_sets = circles.len();
let n_params = n_sets * 3 + 4;
let p0 = pack_clipped_params(circles, container);
for (mask, analytic) in &grads {
assert_eq!(
analytic.len(),
n_params,
"{}: gradient length {} ≠ expected {}",
label,
analytic.len(),
n_params
);
let mut fd = vec![0.0; n_params];
for i in 0..n_params {
let mut plus = p0.clone();
let mut minus = p0.clone();
plus[i] += h;
minus[i] -= h;
let (cp, kp) = unpack_clipped_params(&plus, n_sets);
let (cm, km) = unpack_clipped_params(&minus, n_sets);
let ap = compute_exclusive_regions_clipped(&cp, &kp)
.get(mask)
.copied()
.unwrap_or(0.0);
let am = compute_exclusive_regions_clipped(&cm, &km)
.get(mask)
.copied()
.unwrap_or(0.0);
fd[i] = (ap - am) / (2.0 * h);
}
let direct = compute_exclusive_regions_clipped(circles, container)
.get(mask)
.copied()
.unwrap_or(0.0);
let analytic_area = areas.get(mask).copied().unwrap_or(0.0);
assert!(
(analytic_area - direct).abs() < 1e-12,
"{}: mask {:b} area {} vs direct {} mismatch",
label,
mask,
analytic_area,
direct
);
let diff_norm: f64 = analytic
.iter()
.zip(fd.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
let fd_norm: f64 = fd.iter().map(|b| b * b).sum::<f64>().sqrt();
let rel = if fd_norm > 1e-9 {
diff_norm / fd_norm
} else {
diff_norm
};
assert!(
rel < tol,
"{}: mask {:b} analytic vs FD mismatch (rel={:.3e}, |fd|={:.3e})\n analytic={:?}\n fd ={:?}",
label,
mask,
rel,
fd_norm,
analytic,
fd
);
}
}
#[test]
fn clipped_grad_two_disks_inside_box_matches_fd() {
let circles = vec![
Circle::new(Point::new(-0.6, 0.05), 1.0),
Circle::new(Point::new(0.62, -0.04), 0.95),
];
let container = Rectangle::new(Point::new(0.0, 0.0), 6.0, 5.0);
assert_clipped_gradient_matches_fd(&circles, &container, 1e-6, 1e-5, "two_disks_inside");
}
#[test]
fn clipped_grad_two_disks_each_touching_an_edge_matches_fd() {
let circles = vec![
Circle::new(Point::new(1.4, 0.05), 0.9),
Circle::new(Point::new(0.0, -0.07), 0.85),
];
let container = Rectangle::new(Point::new(0.0, 0.0), 3.6, 3.0);
assert_clipped_gradient_matches_fd(
&circles,
&container,
1e-6,
1e-4,
"two_disks_clipped_edge",
);
}
#[test]
fn clipped_grad_three_disks_inside_box_matches_fd() {
let circles = vec![
Circle::new(Point::new(-0.5, -0.3), 1.0),
Circle::new(Point::new(0.5, -0.3), 1.0),
Circle::new(Point::new(0.0, 0.55), 1.0),
];
let container = Rectangle::new(Point::new(0.0, 0.05), 3.5, 3.5);
assert_clipped_gradient_matches_fd(&circles, &container, 1e-6, 1e-4, "three_disks");
}
#[test]
fn clipped_grad_three_disks_one_clipped_matches_fd() {
let circles = vec![
Circle::new(Point::new(-0.6, -0.2), 0.9),
Circle::new(Point::new(0.7, -0.2), 0.9),
Circle::new(Point::new(0.05, 0.6), 0.9),
];
let container = Rectangle::new(Point::new(0.0, 0.0), 3.5, 2.0);
assert_clipped_gradient_matches_fd(
&circles,
&container,
1e-6,
1e-4,
"three_disks_clipped_top",
);
}
}