use std::collections::HashMap;
use std::f64::consts::PI;
use crate::geometry::diagram::{
IntersectionPoint, RegionMask, discover_regions, mask_to_indices, to_exclusive_areas,
to_exclusive_areas_and_gradients,
};
use crate::geometry::primitives::Point;
use crate::geometry::shapes::Polygon;
use crate::geometry::traits::{
Area, BoundingBox, Centroid, Closed, DiagramShape, Distance, ExclusiveRegionsAndGradient,
Perimeter, Polygonize,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rectangle {
center: Point,
width: f64,
height: f64,
}
#[allow(dead_code)]
impl Rectangle {
pub fn new(center: Point, width: f64, height: f64) -> Self {
Rectangle {
center,
width,
height,
}
}
pub fn try_new(
center: Point,
width: f64,
height: f64,
) -> Result<Self, crate::error::DiagramError> {
if width <= 0.0 {
return Err(crate::error::DiagramError::InvalidShapeParameter {
shape: "Rectangle",
param: "width",
value: width,
});
}
if height <= 0.0 {
return Err(crate::error::DiagramError::InvalidShapeParameter {
shape: "Rectangle",
param: "height",
value: height,
});
}
Ok(Rectangle {
center,
width,
height,
})
}
pub fn from_corners(bottom_left: Point, top_right: Point) -> Self {
let center_x = (bottom_left.x() + top_right.x()) / 2.0;
let center_y = (bottom_left.y() + top_right.y()) / 2.0;
let width = top_right.x() - bottom_left.x();
let height = top_right.y() - bottom_left.y();
Rectangle::new(Point::new(center_x, center_y), width, height)
}
pub fn center(&self) -> &Point {
&self.center
}
pub fn width(&self) -> f64 {
self.width
}
pub fn height(&self) -> f64 {
self.height
}
pub fn set_center(&mut self, center: Point) {
self.center = center;
}
pub fn bounds(&self) -> (f64, f64, f64, f64) {
let half_width = self.width / 2.0;
let half_height = self.height / 2.0;
(
self.center.x() - half_width,
self.center.x() + half_width,
self.center.y() - half_height,
self.center.y() + half_height,
)
}
pub fn to_points(self) -> (Point, Point) {
let (x_min, x_max, y_min, y_max) = self.bounds();
(Point::new(x_min, y_min), Point::new(x_max, y_max))
}
pub fn corners(&self) -> [Point; 4] {
let (x_min, x_max, y_min, y_max) = self.bounds();
[
Point::new(x_min, y_min),
Point::new(x_max, y_min),
Point::new(x_max, y_max),
Point::new(x_min, y_max),
]
}
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Perimeter for Rectangle {
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
}
impl BoundingBox for Rectangle {
fn bounding_box(&self) -> Rectangle {
*self
}
}
impl Centroid for Rectangle {
fn centroid(&self) -> Point {
self.center
}
}
impl Distance for Rectangle {
fn distance(&self, other: &Self) -> f64 {
let (x1_min, x1_max, y1_min, y1_max) = self.bounds();
let (x2_min, x2_max, y2_min, y2_max) = other.bounds();
let dx = if x1_max < x2_min {
x2_min - x1_max
} else if x2_max < x1_min {
x1_min - x2_max
} else {
0.0
};
let dy = if y1_max < y2_min {
y2_min - y1_max
} else if y2_max < y1_min {
y1_min - y2_max
} else {
0.0
};
(dx * dx + dy * dy).sqrt()
}
}
#[allow(dead_code)]
impl Closed for Rectangle {
fn contains(&self, other: &Self) -> bool {
let (x1_min, x1_max, y1_min, y1_max) = self.bounds();
let (x2_min, x2_max, y2_min, y2_max) = other.bounds();
x2_min >= x1_min && x2_max <= x1_max && y2_min >= y1_min && y2_max <= y1_max
}
fn contains_point(&self, point: &Point) -> bool {
let (x_min, x_max, y_min, y_max) = self.bounds();
point.x() >= x_min && point.x() <= x_max && point.y() >= y_min && point.y() <= y_max
}
fn intersects(&self, other: &Self) -> bool {
let (x1_min, x1_max, y1_min, y1_max) = self.bounds();
let (x2_min, x2_max, y2_min, y2_max) = other.bounds();
!(x1_max < x2_min || x2_max < x1_min || y1_max < y2_min || y2_max < y1_min)
}
fn intersection_area(&self, other: &Self) -> f64 {
let (x1_min, x1_max, y1_min, y1_max) = self.bounds();
let (x2_min, x2_max, y2_min, y2_max) = other.bounds();
let x_overlap = (x1_max.min(x2_max) - x1_min.max(x2_min)).max(0.0);
let y_overlap = (y1_max.min(y2_max) - y1_min.max(y2_min)).max(0.0);
x_overlap * y_overlap
}
fn intersection_points(&self, other: &Self) -> Vec<Point> {
if !self.intersects(other) {
return vec![];
}
let (x1_min, x1_max, y1_min, y1_max) = self.bounds();
let (x2_min, x2_max, y2_min, y2_max) = other.bounds();
let x_min = x1_min.max(x2_min);
let x_max = x1_max.min(x2_max);
let y_min = y1_min.max(y2_min);
let y_max = y1_max.min(y2_max);
if x_min >= x_max || y_min >= y_max {
return vec![];
}
vec![
Point::new(x_min, y_min),
Point::new(x_max, y_min),
Point::new(x_max, y_max),
Point::new(x_min, y_max),
]
}
}
fn collect_intersections_rectangle(rects: &[Rectangle], n_sets: usize) -> Vec<IntersectionPoint> {
let mut intersections = Vec::new();
for i in 0..n_sets {
for j in (i + 1)..n_sets {
let pts = rects[i].intersection_points(&rects[j]);
for point in pts {
let mut adopters = vec![i, j];
for (k, r) in rects.iter().enumerate().take(n_sets) {
if k != i && k != j && r.contains_point(&point) {
adopters.push(k);
}
}
adopters.sort_unstable();
intersections.push(IntersectionPoint::new(point, (i, j), adopters));
}
}
}
intersections
}
fn compute_exclusive_regions_with_gradient_rectangles(
shapes: &[Rectangle],
) -> ExclusiveRegionsAndGradient {
let n_sets = shapes.len();
let n_params_per_shape = 4;
let n_params = n_sets * n_params_per_shape;
let intersections = collect_intersections_rectangle(shapes, n_sets);
let regions = discover_regions(shapes, &intersections, n_sets);
let mut overlapping_areas: HashMap<RegionMask, f64> = HashMap::new();
let mut overlapping_grads: HashMap<RegionMask, Vec<f64>> = HashMap::new();
for &mask in ®ions {
let indices = mask_to_indices(mask, n_sets);
let mut x_min = f64::NEG_INFINITY;
let mut x_max = f64::INFINITY;
let mut y_min = f64::NEG_INFINITY;
let mut y_max = f64::INFINITY;
for &i in &indices {
let (a, b, c, d) = shapes[i].bounds();
if a > x_min {
x_min = a;
}
if b < x_max {
x_max = b;
}
if c > y_min {
y_min = c;
}
if d < y_max {
y_max = d;
}
}
let dx_raw = x_max - x_min;
let dy_raw = y_max - y_min;
let dx = dx_raw.max(0.0);
let dy = dy_raw.max(0.0);
overlapping_areas.insert(mask, dx * dy);
let mut grad = vec![0.0; n_params];
if dx_raw > 0.0 && dy_raw > 0.0 {
let mut tied_l: Vec<usize> = Vec::with_capacity(indices.len());
let mut tied_r: Vec<usize> = Vec::with_capacity(indices.len());
let mut tied_b: Vec<usize> = Vec::with_capacity(indices.len());
let mut tied_t: Vec<usize> = Vec::with_capacity(indices.len());
for &i in &indices {
let (a, b, c, d) = shapes[i].bounds();
#[allow(clippy::float_cmp)]
{
if a == x_min {
tied_l.push(i);
}
if b == x_max {
tied_r.push(i);
}
if c == y_min {
tied_b.push(i);
}
if d == y_max {
tied_t.push(i);
}
}
}
let w_l = 1.0 / tied_l.len() as f64;
let w_r = 1.0 / tied_r.len() as f64;
let w_b = 1.0 / tied_b.len() as f64;
let w_t = 1.0 / tied_t.len() as f64;
let mut geom = vec![0.0_f64; n_params];
for &i in &tied_l {
geom[4 * i] -= dy * w_l;
geom[4 * i + 2] += dy * 0.5 * w_l;
}
for &i in &tied_r {
geom[4 * i] += dy * w_r;
geom[4 * i + 2] += dy * 0.5 * w_r;
}
for &i in &tied_b {
geom[4 * i + 1] -= dx * w_b;
geom[4 * i + 3] += dx * 0.5 * w_b;
}
for &i in &tied_t {
geom[4 * i + 1] += dx * w_t;
geom[4 * i + 3] += dx * 0.5 * w_t;
}
for i in 0..n_sets {
let w_i = shapes[i].width;
let h_i = shapes[i].height;
let da_dw = geom[4 * i + 2];
let da_dh = geom[4 * i + 3];
grad[4 * i] = geom[4 * i];
grad[4 * i + 1] = geom[4 * i + 1];
grad[4 * i + 2] = 0.5 * (da_dw * w_i + da_dh * h_i);
grad[4 * i + 3] = 0.5 * (da_dw * w_i - da_dh * h_i);
}
}
overlapping_grads.insert(mask, grad);
}
to_exclusive_areas_and_gradients(&overlapping_areas, &overlapping_grads, n_params)
}
pub(crate) fn compute_exclusive_regions_clipped_rectangles(
shapes: &[Rectangle],
container: &Rectangle,
) -> HashMap<RegionMask, f64> {
let n_sets = shapes.len();
let intersections = collect_intersections_rectangle(shapes, n_sets);
let regions = discover_regions(shapes, &intersections, n_sets);
let (cx_min, cx_max, cy_min, cy_max) = container.bounds();
let mut overlapping_areas: HashMap<RegionMask, f64> = HashMap::new();
overlapping_areas.insert(0, container.area());
for &mask in ®ions {
let indices = mask_to_indices(mask, n_sets);
let mut x_min = cx_min;
let mut x_max = cx_max;
let mut y_min = cy_min;
let mut y_max = cy_max;
for &i in &indices {
let (a, b, c, d) = shapes[i].bounds();
if a > x_min {
x_min = a;
}
if b < x_max {
x_max = b;
}
if c > y_min {
y_min = c;
}
if d < y_max {
y_max = d;
}
}
let dx = (x_max - x_min).max(0.0);
let dy = (y_max - y_min).max(0.0);
overlapping_areas.insert(mask, dx * dy);
}
to_exclusive_areas(&overlapping_areas)
}
pub(crate) fn compute_exclusive_regions_clipped_with_gradient_rectangles(
shapes: &[Rectangle],
container: &Rectangle,
) -> ExclusiveRegionsAndGradient {
let n_sets = shapes.len();
let n_params_per_shape = 4;
let container_off = n_sets * n_params_per_shape;
let n_params = container_off + 4;
let intersections = collect_intersections_rectangle(shapes, n_sets);
let regions = discover_regions(shapes, &intersections, n_sets);
let (cx_min, cx_max, cy_min, cy_max) = container.bounds();
let container_area = container.area();
let mut overlapping_areas: HashMap<RegionMask, f64> = HashMap::new();
let mut overlapping_grads: HashMap<RegionMask, Vec<f64>> = HashMap::new();
overlapping_areas.insert(0, container_area);
let mut zero_grad = vec![0.0; n_params];
zero_grad[container_off + 2] = container_area;
overlapping_grads.insert(0, zero_grad);
for &mask in ®ions {
let indices = mask_to_indices(mask, n_sets);
let mut x_min = cx_min;
let mut x_max = cx_max;
let mut y_min = cy_min;
let mut y_max = cy_max;
for &i in &indices {
let (a, b, c, d) = shapes[i].bounds();
if a > x_min {
x_min = a;
}
if b < x_max {
x_max = b;
}
if c > y_min {
y_min = c;
}
if d < y_max {
y_max = d;
}
}
let dx_raw = x_max - x_min;
let dy_raw = y_max - y_min;
let dx = dx_raw.max(0.0);
let dy = dy_raw.max(0.0);
overlapping_areas.insert(mask, dx * dy);
let mut grad = vec![0.0; n_params];
if dx_raw > 0.0 && dy_raw > 0.0 {
let mut tied_l: Vec<usize> = Vec::with_capacity(indices.len());
let mut tied_r: Vec<usize> = Vec::with_capacity(indices.len());
let mut tied_b: Vec<usize> = Vec::with_capacity(indices.len());
let mut tied_t: Vec<usize> = Vec::with_capacity(indices.len());
for &i in &indices {
let (a, b, c, d) = shapes[i].bounds();
#[allow(clippy::float_cmp)]
{
if a == x_min {
tied_l.push(i);
}
if b == x_max {
tied_r.push(i);
}
if c == y_min {
tied_b.push(i);
}
if d == y_max {
tied_t.push(i);
}
}
}
#[allow(clippy::float_cmp)]
let tied_l_c = cx_min == x_min;
#[allow(clippy::float_cmp)]
let tied_r_c = cx_max == x_max;
#[allow(clippy::float_cmp)]
let tied_b_c = cy_min == y_min;
#[allow(clippy::float_cmp)]
let tied_t_c = cy_max == y_max;
let n_l = tied_l.len() + tied_l_c as usize;
let n_r = tied_r.len() + tied_r_c as usize;
let n_b = tied_b.len() + tied_b_c as usize;
let n_t = tied_t.len() + tied_t_c as usize;
let w_l = 1.0 / n_l as f64;
let w_r = 1.0 / n_r as f64;
let w_b = 1.0 / n_b as f64;
let w_t = 1.0 / n_t as f64;
let mut geom = vec![0.0_f64; n_params];
for &i in &tied_l {
geom[4 * i] -= dy * w_l;
geom[4 * i + 2] += dy * 0.5 * w_l;
}
for &i in &tied_r {
geom[4 * i] += dy * w_r;
geom[4 * i + 2] += dy * 0.5 * w_r;
}
for &i in &tied_b {
geom[4 * i + 1] -= dx * w_b;
geom[4 * i + 3] += dx * 0.5 * w_b;
}
for &i in &tied_t {
geom[4 * i + 1] += dx * w_t;
geom[4 * i + 3] += dx * 0.5 * w_t;
}
if tied_l_c {
geom[container_off] -= dy * w_l;
geom[container_off + 2] += dy * 0.5 * w_l;
}
if tied_r_c {
geom[container_off] += dy * w_r;
geom[container_off + 2] += dy * 0.5 * w_r;
}
if tied_b_c {
geom[container_off + 1] -= dx * w_b;
geom[container_off + 3] += dx * 0.5 * w_b;
}
if tied_t_c {
geom[container_off + 1] += dx * w_t;
geom[container_off + 3] += dx * 0.5 * w_t;
}
for i in 0..n_sets {
let w_i = shapes[i].width;
let h_i = shapes[i].height;
let da_dw = geom[4 * i + 2];
let da_dh = geom[4 * i + 3];
grad[4 * i] = geom[4 * i];
grad[4 * i + 1] = geom[4 * i + 1];
grad[4 * i + 2] = 0.5 * (da_dw * w_i + da_dh * h_i);
grad[4 * i + 3] = 0.5 * (da_dw * w_i - da_dh * h_i);
}
let w_c = container.width;
let h_c = container.height;
let da_dw_c = geom[container_off + 2];
let da_dh_c = geom[container_off + 3];
grad[container_off] = geom[container_off];
grad[container_off + 1] = geom[container_off + 1];
grad[container_off + 2] = 0.5 * (da_dw_c * w_c + da_dh_c * h_c);
grad[container_off + 3] = 0.5 * (da_dw_c * w_c - da_dh_c * h_c);
}
overlapping_grads.insert(mask, grad);
}
to_exclusive_areas_and_gradients(&overlapping_areas, &overlapping_grads, n_params)
}
impl Polygonize for Rectangle {
fn polygonize(&self, _n_vertices: usize) -> Polygon {
let (x_min, x_max, y_min, y_max) = self.bounds();
Polygon::new(vec![
Point::new(x_min, y_min),
Point::new(x_max, y_min),
Point::new(x_max, y_max),
Point::new(x_min, y_max),
])
}
}
impl DiagramShape for Rectangle {
fn compute_exclusive_regions(shapes: &[Self]) -> HashMap<RegionMask, f64> {
let n_sets = shapes.len();
let intersections = collect_intersections_rectangle(shapes, n_sets);
let regions = discover_regions(shapes, &intersections, n_sets);
let mut overlapping_areas: HashMap<RegionMask, f64> = HashMap::new();
for &mask in ®ions {
let indices = mask_to_indices(mask, n_sets);
let mut x_min = f64::NEG_INFINITY;
let mut x_max = f64::INFINITY;
let mut y_min = f64::NEG_INFINITY;
let mut y_max = f64::INFINITY;
for &i in &indices {
let (a, b, c, d) = shapes[i].bounds();
if a > x_min {
x_min = a;
}
if b < x_max {
x_max = b;
}
if c > y_min {
y_min = c;
}
if d < y_max {
y_max = d;
}
}
let dx = (x_max - x_min).max(0.0);
let dy = (y_max - y_min).max(0.0);
overlapping_areas.insert(mask, dx * dy);
}
to_exclusive_areas(&overlapping_areas)
}
fn optimizer_params_from_circle(x: f64, y: f64, radius: f64) -> Vec<f64> {
let u = PI.ln() + 2.0 * radius.ln();
vec![x, y, u, 0.0]
}
fn mds_target_distance(
area_i: f64,
area_j: f64,
target_overlap: f64,
) -> Result<f64, crate::error::DiagramError> {
let s_i = area_i.sqrt();
let s_j = area_j.sqrt();
let half_sum = 0.5 * (s_i + s_j);
if target_overlap <= 0.0 {
return Ok(half_sum * 2.0_f64.sqrt());
}
let root = target_overlap.sqrt();
if root > half_sum {
return Ok(0.0);
}
let d = 2.0_f64.sqrt() * (half_sum - root);
Ok(d.max(0.0))
}
fn n_params() -> usize {
4 }
fn from_params(params: &[f64]) -> Self {
debug_assert_eq!(
params.len(),
4,
"Rectangle requires 4 parameters: x, y, width, height"
);
Rectangle::new(
Point::new(params[0], params[1]),
params[2].max(f64::MIN_POSITIVE),
params[3].max(f64::MIN_POSITIVE),
)
}
fn to_params(&self) -> Vec<f64> {
vec![self.center.x(), self.center.y(), self.width, self.height]
}
fn from_optimizer_params(params: &[f64]) -> Self {
debug_assert_eq!(
params.len(),
4,
"Rectangle optimizer params: x, y, ln(area), ln(ratio)"
);
let u = params[2];
let v = params[3];
let w = ((u + v) * 0.5).exp();
let h = ((u - v) * 0.5).exp();
let w = if w.is_finite() && w > 0.0 {
w
} else {
f64::MIN_POSITIVE
};
let h = if h.is_finite() && h > 0.0 {
h
} else {
f64::MIN_POSITIVE
};
Rectangle::new(Point::new(params[0], params[1]), w, h)
}
fn to_optimizer_params(&self) -> Vec<f64> {
let u = (self.width * self.height).ln();
let v = (self.width / self.height).ln();
vec![self.center.x(), self.center.y(), u, v]
}
fn compute_exclusive_regions_with_gradient(
shapes: &[Self],
) -> Option<ExclusiveRegionsAndGradient> {
Some(compute_exclusive_regions_with_gradient_rectangles(shapes))
}
fn compute_exclusive_regions_clipped(
shapes: &[Self],
container: &Rectangle,
) -> Option<HashMap<RegionMask, f64>> {
Some(compute_exclusive_regions_clipped_rectangles(
shapes, container,
))
}
fn compute_exclusive_regions_clipped_with_gradient(
shapes: &[Self],
container: &Rectangle,
) -> Option<ExclusiveRegionsAndGradient> {
Some(compute_exclusive_regions_clipped_with_gradient_rectangles(
shapes, container,
))
}
fn canonical_venn_layout(n: usize) -> Option<Vec<Self>> {
let centers_and_side: &[((f64, f64), f64)] = match n {
1 => &[((0.0, 0.0), 2.0)],
2 => &[((-0.4, 0.0), 1.0), ((0.4, 0.0), 1.0)],
3 => &[
((0.0, 0.36), 1.0),
((0.42, -0.36), 1.0),
((-0.42, -0.36), 1.0),
],
_ => return None,
};
Some(
centers_and_side
.iter()
.map(|&((x, y), s)| Rectangle::new(Point::new(x, y), s, s))
.collect(),
)
}
}
#[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_rectangle_new() {
let center = Point::new(1.0, 2.0);
let rect = Rectangle::new(center, 4.0, 3.0);
assert_eq!(rect.width(), 4.0);
assert_eq!(rect.height(), 3.0);
assert_eq!(rect.center().x(), 1.0);
assert_eq!(rect.center().y(), 2.0);
}
#[test]
fn test_rectangle_area() {
let rect = Rectangle::new(Point::new(0.0, 0.0), 4.0, 3.0);
assert!(approx_eq(rect.area(), 12.0));
let rect2 = Rectangle::new(Point::new(5.0, 5.0), 2.0, 5.0);
assert!(approx_eq(rect2.area(), 10.0));
}
#[test]
fn test_rectangle_perimeter() {
let rect = Rectangle::new(Point::new(0.0, 0.0), 4.0, 3.0);
assert!(approx_eq(rect.perimeter(), 14.0));
let rect2 = Rectangle::new(Point::new(1.0, 1.0), 2.0, 2.0);
assert!(approx_eq(rect2.perimeter(), 8.0));
}
#[test]
fn test_rectangle_bounds() {
let rect = Rectangle::new(Point::new(2.0, 3.0), 4.0, 6.0);
let (x_min, x_max, y_min, y_max) = rect.bounds();
assert!(approx_eq(x_min, 0.0));
assert!(approx_eq(x_max, 4.0));
assert!(approx_eq(y_min, 0.0));
assert!(approx_eq(y_max, 6.0));
}
#[test]
fn test_rectangle_corners() {
let rect = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let corners = rect.corners();
assert_eq!(corners.len(), 4);
let expected = [
Point::new(-1.0, -1.0),
Point::new(1.0, -1.0),
Point::new(1.0, 1.0),
Point::new(-1.0, 1.0),
];
for (corner, &expected_corner) in corners.iter().zip(expected.iter()) {
assert!(approx_eq(corner.x(), expected_corner.x()));
assert!(approx_eq(corner.y(), expected_corner.y()));
}
}
#[test]
fn test_rectangle_centroid() {
let rect = Rectangle::new(Point::new(3.0, 4.0), 2.0, 2.0);
let centroid = rect.centroid();
assert!(approx_eq(centroid.x(), 3.0));
assert!(approx_eq(centroid.y(), 4.0));
}
#[test]
fn test_rectangle_contains_point() {
let rect = Rectangle::new(Point::new(0.0, 0.0), 4.0, 2.0);
assert!(rect.contains_point(&Point::new(0.0, 0.0)));
assert!(rect.contains_point(&Point::new(1.0, 0.5)));
assert!(rect.contains_point(&Point::new(-1.0, -0.5)));
assert!(rect.contains_point(&Point::new(2.0, 1.0)));
assert!(!rect.contains_point(&Point::new(3.0, 0.0)));
assert!(!rect.contains_point(&Point::new(0.0, 2.0)));
}
#[test]
fn test_rectangle_distance_no_overlap() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let rect2 = Rectangle::new(Point::new(5.0, 0.0), 2.0, 2.0);
assert!(approx_eq(rect1.distance(&rect2), 3.0));
}
#[test]
fn test_rectangle_distance_touching() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let rect2 = Rectangle::new(Point::new(3.0, 0.0), 2.0, 2.0);
assert!(approx_eq(rect1.distance(&rect2), 1.0));
}
#[test]
fn test_rectangle_distance_overlapping() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0);
let rect2 = Rectangle::new(Point::new(1.0, 0.0), 2.0, 2.0);
assert!(approx_eq(rect1.distance(&rect2), 0.0));
}
#[test]
fn test_rectangle_distance_diagonal() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let rect2 = Rectangle::new(Point::new(5.0, 5.0), 2.0, 2.0);
let expected = ((3.0_f64).powi(2) + (3.0_f64).powi(2)).sqrt();
assert!(approx_eq(rect1.distance(&rect2), expected));
}
#[test]
fn test_rectangle_contains_smaller() {
let large = Rectangle::new(Point::new(0.0, 0.0), 10.0, 10.0);
let small = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0);
assert!(large.contains(&small));
}
#[test]
fn test_rectangle_contains_self() {
let rect = Rectangle::new(Point::new(0.0, 0.0), 3.0, 3.0);
assert!(rect.contains(&rect));
}
#[test]
fn test_rectangle_not_contains() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let rect2 = Rectangle::new(Point::new(5.0, 0.0), 2.0, 2.0);
assert!(!rect1.contains(&rect2));
}
#[test]
fn test_rectangle_not_contains_partial_overlap() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0);
let rect2 = Rectangle::new(Point::new(2.0, 0.0), 3.0, 3.0);
assert!(!rect1.contains(&rect2));
}
#[test]
fn test_rectangle_intersects_separate() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let rect2 = Rectangle::new(Point::new(5.0, 0.0), 2.0, 2.0);
assert!(!rect1.intersects(&rect2));
}
#[test]
fn test_rectangle_intersects_touching() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let rect2 = Rectangle::new(Point::new(2.0, 0.0), 2.0, 2.0);
assert!(rect1.intersects(&rect2));
}
#[test]
fn test_rectangle_intersects_overlapping() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0);
let rect2 = Rectangle::new(Point::new(1.0, 0.0), 2.0, 2.0);
assert!(rect1.intersects(&rect2));
}
#[test]
fn test_intersection_area_no_overlap() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let rect2 = Rectangle::new(Point::new(10.0, 0.0), 2.0, 2.0);
assert!(approx_eq(rect1.intersection_area(&rect2), 0.0));
}
#[test]
fn test_intersection_area_touching() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let rect2 = Rectangle::new(Point::new(3.0, 0.0), 2.0, 2.0);
assert!(approx_eq(rect1.intersection_area(&rect2), 0.0));
}
#[test]
fn test_intersection_area_complete_overlap_same_size() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 4.0, 3.0);
let rect2 = Rectangle::new(Point::new(0.0, 0.0), 4.0, 3.0);
assert!(approx_eq(rect1.intersection_area(&rect2), 12.0));
}
#[test]
fn test_intersection_area_one_inside_other() {
let large = Rectangle::new(Point::new(0.0, 0.0), 10.0, 10.0);
let small = Rectangle::new(Point::new(1.0, 0.0), 4.0, 4.0);
let expected = 16.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 rect1 = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0);
let rect2 = Rectangle::new(Point::new(2.0, 0.0), 4.0, 4.0);
let x_overlap = 2.0;
let y_overlap = 4.0;
let expected = x_overlap * y_overlap;
assert!(approx_eq(rect1.intersection_area(&rect2), expected));
}
#[test]
fn test_intersection_area_symmetric() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 4.0, 3.0);
let rect2 = Rectangle::new(Point::new(1.5, 0.0), 3.0, 2.0);
let area1 = rect1.intersection_area(&rect2);
let area2 = rect2.intersection_area(&rect1);
assert!(approx_eq(area1, area2));
}
#[test]
fn test_intersection_area_different_sizes() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 6.0, 4.0);
let rect2 = Rectangle::new(Point::new(2.0, 0.0), 2.0, 2.0);
let area = rect1.intersection_area(&rect2);
assert!(area > 0.0);
assert!(area <= 4.0);
}
#[test]
fn test_intersection_points_no_intersection() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let rect2 = Rectangle::new(Point::new(5.0, 0.0), 2.0, 2.0);
let points = rect1.intersection_points(&rect2);
assert_eq!(points.len(), 0);
}
#[test]
fn test_intersection_points_overlapping() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0);
let rect2 = Rectangle::new(Point::new(1.0, 1.0), 2.0, 2.0);
let points = rect1.intersection_points(&rect2);
assert_eq!(points.len(), 4);
for point in &points {
assert!(rect1.contains_point(point));
assert!(rect2.contains_point(point));
}
}
#[test]
fn test_intersection_points_partial_overlap() {
let rect1 = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0);
let rect2 = Rectangle::new(Point::new(2.0, 0.0), 4.0, 4.0);
let points = rect1.intersection_points(&rect2);
assert_eq!(points.len(), 4);
for point in &points {
assert!(rect1.contains_point(point));
assert!(rect2.contains_point(point));
}
}
#[test]
fn test_set_center() {
let mut rect = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
rect.set_center(Point::new(5.0, 3.0));
assert_eq!(rect.center().x(), 5.0);
assert_eq!(rect.center().y(), 3.0);
}
#[test]
fn test_try_new_accepts_positive() {
let r = Rectangle::try_new(Point::new(0.0, 0.0), 2.0, 1.0).unwrap();
assert_eq!(r.width(), 2.0);
assert_eq!(r.height(), 1.0);
}
#[test]
fn test_try_new_rejects_zero_or_negative_width() {
for bad in [0.0, -1.0] {
let err = Rectangle::try_new(Point::new(0.0, 0.0), bad, 1.0).unwrap_err();
assert!(matches!(
err,
crate::error::DiagramError::InvalidShapeParameter {
shape: "Rectangle",
param: "width",
..
}
));
}
}
#[test]
fn test_try_new_rejects_zero_or_negative_height() {
for bad in [0.0, -2.0] {
let err = Rectangle::try_new(Point::new(0.0, 0.0), 1.0, bad).unwrap_err();
assert!(matches!(
err,
crate::error::DiagramError::InvalidShapeParameter {
shape: "Rectangle",
param: "height",
..
}
));
}
}
#[test]
fn test_to_params_round_trip() {
let r = Rectangle::new(Point::new(1.5, -2.0), 3.0, 4.0);
let p = r.to_params();
assert_eq!(p, vec![1.5, -2.0, 3.0, 4.0]);
let back = Rectangle::from_params(&p);
assert_eq!(r, back);
}
#[test]
fn test_optimizer_params_round_trip() {
let r = Rectangle::new(Point::new(1.5, -2.0), 3.0, 4.0);
let p = r.to_optimizer_params();
let back = Rectangle::from_optimizer_params(&p);
assert!((r.center().x() - back.center().x()).abs() < 1e-12);
assert!((r.center().y() - back.center().y()).abs() < 1e-12);
assert!((r.width() - back.width()).abs() < 1e-12);
assert!((r.height() - back.height()).abs() < 1e-12);
}
#[test]
fn test_optimizer_params_from_circle_equal_area() {
let r = 2.0;
let p = Rectangle::optimizer_params_from_circle(0.0, 0.0, r);
assert!((p[2] - (PI * r * r).ln()).abs() < 1e-12);
assert_eq!(p[3], 0.0);
let rect = Rectangle::from_optimizer_params(&p);
assert!(approx_eq(rect.area(), PI * r * r));
assert!(approx_eq(rect.width(), rect.height()));
}
#[test]
fn test_compute_exclusive_regions_two_disjoint() {
let a = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let b = Rectangle::new(Point::new(10.0, 0.0), 2.0, 2.0);
let regions = Rectangle::compute_exclusive_regions(&[a, b]);
assert!(approx_eq(regions[&0b01], 4.0));
assert!(approx_eq(regions[&0b10], 4.0));
assert_eq!(regions.get(&0b11).copied().unwrap_or(0.0), 0.0);
}
#[test]
fn test_compute_exclusive_regions_two_partial() {
let a = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let b = Rectangle::new(Point::new(1.0, 0.0), 2.0, 2.0);
let regions = Rectangle::compute_exclusive_regions(&[a, b]);
assert!(approx_eq(regions[&0b01], 2.0));
assert!(approx_eq(regions[&0b10], 2.0));
assert!(approx_eq(regions[&0b11], 2.0));
}
#[test]
fn test_compute_exclusive_regions_unequal_sides() {
let a = Rectangle::new(Point::new(0.0, 0.0), 4.0, 2.0);
let b = Rectangle::new(Point::new(1.0, 0.0), 4.0, 2.0);
let regions = Rectangle::compute_exclusive_regions(&[a, b]);
assert!(approx_eq(regions[&0b01], 2.0));
assert!(approx_eq(regions[&0b10], 2.0));
assert!(approx_eq(regions[&0b11], 6.0));
}
#[test]
fn test_compute_exclusive_regions_three_way_grid() {
let a = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let b = Rectangle::new(Point::new(1.0, 0.0), 2.0, 2.0);
let c = Rectangle::new(Point::new(0.5, 1.0), 2.0, 2.0);
let regions = Rectangle::compute_exclusive_regions(&[a, b, c]);
assert!(
approx_eq(regions[&0b111], 1.0),
"triple ∩ area = {}, expected 1.0",
regions[&0b111]
);
}
#[test]
fn test_compute_exclusive_regions_nested() {
let outer = Rectangle::new(Point::new(0.0, 0.0), 6.0, 4.0);
let inner = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let regions = Rectangle::compute_exclusive_regions(&[outer, inner]);
assert!(approx_eq(regions[&0b11], 4.0)); assert!(approx_eq(regions[&0b01], 24.0 - 4.0));
assert_eq!(regions.get(&0b10).copied().unwrap_or(0.0), 0.0);
}
fn fd_exclusive_region_gradients(
shapes: &[Rectangle],
h: f64,
) -> HashMap<RegionMask, Vec<f64>> {
let n_sets = shapes.len();
let n_params = n_sets * 4;
let base = Rectangle::compute_exclusive_regions(shapes);
let mut grads: HashMap<RegionMask, Vec<f64>> =
base.keys().map(|&m| (m, vec![0.0; n_params])).collect();
for i in 0..n_sets {
for k in 0..4 {
let perturb = |delta: f64| -> HashMap<RegionMask, f64> {
let mut copy: Vec<Rectangle> = shapes.to_vec();
let mut opt = copy[i].to_optimizer_params();
opt[k] += delta;
copy[i] = Rectangle::from_optimizer_params(&opt);
Rectangle::compute_exclusive_regions(©)
};
let plus = perturb(h);
let minus = perturb(-h);
for (&mask, g) in grads.iter_mut() {
let p = plus.get(&mask).copied().unwrap_or(0.0);
let m = minus.get(&mask).copied().unwrap_or(0.0);
g[4 * i + k] = (p - m) / (2.0 * h);
}
}
}
grads
}
fn assert_grad_matches_fd(
analytical: &HashMap<RegionMask, Vec<f64>>,
fd: &HashMap<RegionMask, Vec<f64>>,
tol: f64,
) {
for (&mask, ag) in analytical.iter() {
let fg = fd
.get(&mask)
.expect("FD missing mask present in analytical");
assert_eq!(ag.len(), fg.len(), "param count mismatch for mask {mask:b}");
for (k, (&a, &f)) in ag.iter().zip(fg.iter()).enumerate() {
assert!(
(a - f).abs() < tol,
"mask {mask:b} param {k}: analytical={a} fd={f} (tol={tol})"
);
}
}
}
#[test]
fn test_gradient_single_rectangle_matches_area_only_on_u() {
let r = Rectangle::new(Point::new(1.5, -2.0), 3.0, 4.0);
let (areas, grads) = Rectangle::compute_exclusive_regions_with_gradient(&[r]).unwrap();
let a = 3.0 * 4.0;
assert!(approx_eq(areas[&0b1], a));
let g = &grads[&0b1];
assert_eq!(g.len(), 4);
assert!(approx_eq(g[0], 0.0));
assert!(approx_eq(g[1], 0.0));
assert!(approx_eq(g[2], a)); assert!(approx_eq(g[3], 0.0)); }
#[test]
fn test_gradient_two_rectangles_partial_overlap_matches_fd() {
let a = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let b = Rectangle::new(Point::new(1.0, 0.0), 2.0, 2.0);
let (_, grads) = Rectangle::compute_exclusive_regions_with_gradient(&[a, b]).unwrap();
let fd = fd_exclusive_region_gradients(&[a, b], 1e-6);
assert_grad_matches_fd(&grads, &fd, 1e-5);
}
#[test]
fn test_gradient_three_rectangles_overlap_matches_fd() {
let a = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let b = Rectangle::new(Point::new(1.0, 0.0), 2.0, 2.0);
let c = Rectangle::new(Point::new(0.5, 1.0), 2.0, 2.0);
let (_, grads) = Rectangle::compute_exclusive_regions_with_gradient(&[a, b, c]).unwrap();
let fd = fd_exclusive_region_gradients(&[a, b, c], 1e-6);
assert_grad_matches_fd(&grads, &fd, 1e-5);
}
#[test]
fn test_gradient_generic_no_ties_matches_fd_tightly() {
let a = Rectangle::new(Point::new(0.0, 0.0), 2.3, 1.7);
let b = Rectangle::new(Point::new(1.1, 0.4), 1.7, 2.1);
let c = Rectangle::new(Point::new(0.6, 1.2), 2.1, 1.5);
let (_, grads) = Rectangle::compute_exclusive_regions_with_gradient(&[a, b, c]).unwrap();
let fd = fd_exclusive_region_gradients(&[a, b, c], 1e-5);
assert_grad_matches_fd(&grads, &fd, 1e-7);
}
#[test]
fn test_gradient_disjoint_pair_is_zero_on_intersection() {
let a = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let b = Rectangle::new(Point::new(10.0, 0.0), 2.0, 2.0);
let (_, grads) = Rectangle::compute_exclusive_regions_with_gradient(&[a, b]).unwrap();
if let Some(g) = grads.get(&0b11) {
for &v in g {
assert!(approx_eq(v, 0.0), "expected zero on disjoint pair, got {v}");
}
}
let fd = fd_exclusive_region_gradients(&[a, b], 1e-5);
for &mask in &[0b01_usize, 0b10_usize] {
let ag = grads.get(&mask).expect("singleton missing");
let fg = fd.get(&mask).expect("FD singleton missing");
for (k, (&a, &f)) in ag.iter().zip(fg.iter()).enumerate() {
assert!(
(a - f).abs() < 1e-6,
"mask {mask:b} param {k}: analytical={a} fd={f}"
);
}
}
}
#[test]
fn test_gradient_nested_matches_fd() {
let outer = Rectangle::new(Point::new(0.0, 0.0), 6.0, 4.0);
let inner = Rectangle::new(Point::new(0.0, 0.0), 2.0, 2.0);
let (areas, grads) =
Rectangle::compute_exclusive_regions_with_gradient(&[outer, inner]).unwrap();
let fd = fd_exclusive_region_gradients(&[outer, inner], 1e-5);
assert!(approx_eq(areas[&0b11], 4.0));
assert!(approx_eq(areas[&0b01], 20.0));
assert_grad_matches_fd(&grads, &fd, 1e-7);
}
#[test]
fn test_mds_target_distance_zero_overlap_is_diagonal_tangency() {
let area = 1.0;
let d = Rectangle::mds_target_distance(area, area, 0.0).unwrap();
let s = area.sqrt();
let expected = 2.0_f64.sqrt() * s;
assert!(approx_eq(d, expected), "d = {d}, expected {expected}");
}
#[test]
fn test_mds_target_distance_full_overlap_is_zero() {
let area = 4.0;
let d = Rectangle::mds_target_distance(area, area, area).unwrap();
assert!(approx_eq(d, 0.0));
}
#[test]
fn test_polygonize_returns_4_ccw_vertices_with_correct_area() {
let r = Rectangle::new(Point::new(0.0, 0.0), 4.0, 2.0);
let p = r.polygonize(0);
assert_eq!(p.vertices().len(), 4);
let v = p.vertices();
let mut shoelace = 0.0;
for i in 0..4 {
let j = (i + 1) % 4;
shoelace += v[i].x() * v[j].y() - v[j].x() * v[i].y();
}
assert!(approx_eq(0.5 * shoelace, 8.0));
}
#[test]
fn test_fitter_end_to_end_two_partial_overlap() {
use crate::{DiagramSpecBuilder, Fitter, InputType};
let spec = DiagramSpecBuilder::new()
.set("A", 4.0)
.set("B", 4.0)
.intersection(&["A", "B"], 2.0)
.input_type(InputType::Exclusive)
.build()
.unwrap();
let layout = Fitter::<Rectangle>::new(&spec).seed(42).fit().unwrap();
let fitted = layout.fitted();
assert!(
fitted.values().all(|&v| v.is_finite()),
"non-finite fitted areas in {fitted:?}"
);
assert!(
layout.loss().is_finite(),
"non-finite loss {}",
layout.loss()
);
assert_eq!(layout.shapes().len(), 2);
}
fn assert_rect(actual: &Rectangle, x: f64, y: f64, w: f64, h: f64) {
assert!(approx_eq(actual.center().x(), x));
assert!(approx_eq(actual.center().y(), y));
assert!(approx_eq(actual.width(), w));
assert!(approx_eq(actual.height(), h));
}
#[test]
fn test_canonical_venn_layout_n1() {
let shapes = Rectangle::canonical_venn_layout(1).unwrap();
assert_eq!(shapes.len(), 1);
assert_rect(&shapes[0], 0.0, 0.0, 2.0, 2.0);
}
#[test]
fn test_canonical_venn_layout_n2() {
let shapes = Rectangle::canonical_venn_layout(2).unwrap();
assert_eq!(shapes.len(), 2);
assert_rect(&shapes[0], -0.4, 0.0, 1.0, 1.0);
assert_rect(&shapes[1], 0.4, 0.0, 1.0, 1.0);
}
#[test]
fn test_canonical_venn_layout_n3() {
let shapes = Rectangle::canonical_venn_layout(3).unwrap();
assert_eq!(shapes.len(), 3);
assert_rect(&shapes[0], 0.0, 0.36, 1.0, 1.0);
assert_rect(&shapes[1], 0.42, -0.36, 1.0, 1.0);
assert_rect(&shapes[2], -0.42, -0.36, 1.0, 1.0);
}
#[test]
fn test_canonical_venn_layout_unsupported() {
assert!(Rectangle::canonical_venn_layout(0).is_none());
assert!(Rectangle::canonical_venn_layout(4).is_none());
assert!(Rectangle::canonical_venn_layout(5).is_none());
}
#[test]
fn test_clipped_no_rects_complement_equals_container_area() {
let container = Rectangle::new(Point::new(0.0, 0.0), 5.0, 4.0);
let areas = compute_exclusive_regions_clipped_rectangles(&[], &container);
assert!(approx_eq(areas[&0], 20.0));
}
#[test]
fn test_clipped_single_rect_inside_container() {
let r = Rectangle::new(Point::new(0.0, 0.0), 2.0, 1.0);
let container = Rectangle::new(Point::new(0.0, 0.0), 5.0, 4.0);
let areas = compute_exclusive_regions_clipped_rectangles(&[r], &container);
assert!(approx_eq(areas[&0b1], 2.0));
assert!(approx_eq(areas[&0], 20.0 - 2.0));
}
#[test]
fn test_clipped_single_rect_outside_container() {
let r = Rectangle::new(Point::new(100.0, 0.0), 2.0, 1.0);
let container = Rectangle::new(Point::new(0.0, 0.0), 5.0, 4.0);
let areas = compute_exclusive_regions_clipped_rectangles(&[r], &container);
assert!(approx_eq(areas.get(&0b1).copied().unwrap_or(0.0), 0.0));
assert!(approx_eq(areas[&0], 20.0));
}
#[test]
fn test_clipped_single_rect_partial_clip() {
let r = Rectangle::new(Point::new(1.0, 0.0), 4.0, 2.0);
let container = Rectangle::new(Point::new(0.0, 0.0), 4.0, 4.0);
let areas = compute_exclusive_regions_clipped_rectangles(&[r], &container);
assert!(approx_eq(areas[&0b1], 6.0));
assert!(approx_eq(areas[&0], 16.0 - 6.0));
}
#[test]
fn test_clipped_container_fully_inside_rect() {
let r = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let container = Rectangle::new(Point::new(0.0, 0.0), 4.0, 3.0);
let areas = compute_exclusive_regions_clipped_rectangles(&[r], &container);
assert!(approx_eq(areas[&0b1], 12.0));
assert!(approx_eq(areas[&0], 0.0));
}
#[test]
fn test_clipped_two_rects_inside_sum_to_union() {
let a = Rectangle::new(Point::new(-0.6, 0.0), 1.6, 1.6);
let b = Rectangle::new(Point::new(0.6, 0.0), 1.6, 1.6);
let container = Rectangle::new(Point::new(0.0, 0.0), 5.0, 4.0);
let unclipped = Rectangle::compute_exclusive_regions(&[a, b]);
let clipped = compute_exclusive_regions_clipped_rectangles(&[a, b], &container);
for &mask in &[0b01_usize, 0b10_usize, 0b11_usize] {
let lhs = clipped.get(&mask).copied().unwrap_or(0.0);
let rhs = unclipped.get(&mask).copied().unwrap_or(0.0);
assert!(
approx_eq(lhs, rhs),
"mask {mask:b}: clipped={lhs} unclipped={rhs}"
);
}
let union: f64 = clipped[&0b01] + clipped[&0b10] + clipped[&0b11];
assert!(approx_eq(clipped[&0], 20.0 - union));
}
fn pack_clipped_rect_params(rects: &[Rectangle], container: &Rectangle) -> Vec<f64> {
let mut p = Vec::with_capacity(rects.len() * 4 + 4);
for r in rects {
p.extend(r.to_optimizer_params());
}
p.extend(container.to_optimizer_params());
p
}
fn unpack_clipped_rect_params(p: &[f64], n_sets: usize) -> (Vec<Rectangle>, Rectangle) {
let rects: Vec<Rectangle> = (0..n_sets)
.map(|i| Rectangle::from_optimizer_params(&p[4 * i..4 * i + 4]))
.collect();
let container = Rectangle::from_optimizer_params(&p[4 * n_sets..4 * n_sets + 4]);
(rects, container)
}
fn assert_clipped_rect_gradient_matches_fd(
rects: &[Rectangle],
container: &Rectangle,
h: f64,
tol: f64,
label: &str,
) {
let (areas, grads) =
compute_exclusive_regions_clipped_with_gradient_rectangles(rects, container);
let n_sets = rects.len();
let n_params = n_sets * 4 + 4;
let p0 = pack_clipped_rect_params(rects, 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 (rp, kp) = unpack_clipped_rect_params(&plus, n_sets);
let (rm, km) = unpack_clipped_rect_params(&minus, n_sets);
let ap = compute_exclusive_regions_clipped_rectangles(&rp, &kp)
.get(mask)
.copied()
.unwrap_or(0.0);
let am = compute_exclusive_regions_clipped_rectangles(&rm, &km)
.get(mask)
.copied()
.unwrap_or(0.0);
fd[i] = (ap - am) / (2.0 * h);
}
let direct = compute_exclusive_regions_clipped_rectangles(rects, 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,
"{label}: mask {mask:b} area {analytic_area} vs direct {direct} mismatch"
);
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,
"{label}: mask {mask:b} analytic vs FD mismatch (rel={rel:.3e}, |fd|={fd_norm:.3e})\n analytic={analytic:?}\n fd ={fd:?}"
);
}
}
#[test]
fn test_clipped_grad_two_rects_inside_box_matches_fd() {
let rects = vec![
Rectangle::new(Point::new(-0.6, 0.05), 1.7, 1.3),
Rectangle::new(Point::new(0.62, -0.04), 1.4, 1.5),
];
let container = Rectangle::new(Point::new(0.0, 0.0), 6.0, 5.0);
assert_clipped_rect_gradient_matches_fd(&rects, &container, 1e-5, 1e-7, "two_rects_inside");
}
#[test]
fn test_clipped_grad_two_rects_one_clipped_by_edge_matches_fd() {
let rects = vec![
Rectangle::new(Point::new(0.0, 0.07), 1.7, 1.3),
Rectangle::new(Point::new(1.4, -0.03), 1.5, 1.5),
];
let container = Rectangle::new(Point::new(0.0, 0.0), 3.6, 3.0);
assert_clipped_rect_gradient_matches_fd(
&rects,
&container,
1e-5,
1e-6,
"two_rects_one_clipped",
);
}
#[test]
fn test_clipped_grad_three_rects_inside_box_matches_fd() {
let rects = vec![
Rectangle::new(Point::new(-0.5, -0.3), 1.8, 1.4),
Rectangle::new(Point::new(0.5, -0.3), 1.7, 1.5),
Rectangle::new(Point::new(0.0, 0.55), 1.6, 1.3),
];
let container = Rectangle::new(Point::new(0.0, 0.05), 3.5, 3.5);
assert_clipped_rect_gradient_matches_fd(
&rects,
&container,
1e-5,
1e-7,
"three_rects_inside",
);
}
#[test]
fn test_clipped_grad_three_rects_one_clipped_matches_fd() {
let rects = vec![
Rectangle::new(Point::new(-0.6, -0.2), 1.5, 1.3),
Rectangle::new(Point::new(0.7, -0.2), 1.5, 1.4),
Rectangle::new(Point::new(0.05, 0.6), 1.4, 1.6),
];
let container = Rectangle::new(Point::new(0.0, 0.0), 3.5, 2.0);
assert_clipped_rect_gradient_matches_fd(
&rects,
&container,
1e-5,
1e-5,
"three_rects_one_clipped",
);
}
#[test]
fn fit_two_rectangles_with_complement_runs_to_finite_loss() {
use crate::{DiagramSpecBuilder, Fitter, InputType};
let spec = DiagramSpecBuilder::new()
.set("A", 4.0)
.set("B", 4.0)
.intersection(&["A", "B"], 1.0)
.complement(20.0)
.input_type(InputType::Exclusive)
.build()
.unwrap();
let layout = Fitter::<Rectangle>::new(&spec).seed(42).fit().unwrap();
let fitted = layout.fitted();
assert!(
fitted.values().all(|&v| v.is_finite()),
"non-finite fitted areas in {fitted:?}"
);
assert!(
layout.loss().is_finite(),
"non-finite loss {}",
layout.loss()
);
let container = layout
.container()
.expect("complement spec carries container");
assert!(
(container.area() - 29.0).abs() / 29.0 < 0.1,
"container area {} should be near universe 29",
container.area()
);
}
}