use crate::core::{Graph, IgraphError, IgraphResult};
const TOLERANCE: f64 = 128.0 * f64::EPSILON;
#[allow(clippy::many_single_char_names)]
#[allow(clippy::similar_names)]
pub fn circle_beta_skeleton(points: &[Vec<f64>], beta: f64) -> IgraphResult<Graph> {
if !beta.is_finite() || beta <= 0.0 {
return Err(IgraphError::InvalidArgument(format!(
"circle_beta_skeleton: beta must be a finite positive number, got {beta}"
)));
}
let n = points.len();
let dim = if n == 0 { 0 } else { points[0].len() };
if n > 0 {
for (i, row) in points.iter().enumerate().skip(1) {
if row.len() != dim {
return Err(IgraphError::InvalidArgument(format!(
"circle_beta_skeleton: point row {i} has dimension {} but expected {dim}",
row.len()
)));
}
}
if dim != 2 {
return Err(IgraphError::Unsupported(
"circle_beta_skeleton: circle-based beta skeletons are only supported in 2 dimensions",
));
}
}
let n_u32 = u32::try_from(n).map_err(|_| {
IgraphError::InvalidArgument("circle_beta_skeleton: too many points".into())
})?;
let mut graph = Graph::with_vertices(n_u32);
let r = if beta < 1.0 { 0.5 / beta } else { 0.5 * beta };
let perp_scale = (r * r - 0.25).max(0.0).sqrt();
let union_mode = beta >= 1.0;
let tol = 1.0 + TOLERANCE;
let radius_factor = if union_mode { tol } else { tol * tol };
let mut a_center = [0.0_f64; 2];
let mut b_center = [0.0_f64; 2];
for i in 0..n {
let a = &points[i];
for j in (i + 1)..n {
let b = &points[j];
let dx = a[0] - b[0];
let dy = a[1] - b[1];
let dist_sq = dx * dx + dy * dy;
let mid0 = 0.5 * (a[0] + b[0]);
let mid1 = 0.5 * (a[1] + b[1]);
let perp0 = -dy * perp_scale;
let perp1 = dx * perp_scale;
a_center[0] = mid0 + perp0;
a_center[1] = mid1 + perp1;
b_center[0] = mid0 - perp0;
b_center[1] = mid1 - perp1;
let circle_radius = dist_sq * r * r * radius_factor;
let mut empty = true;
for (k, c) in points.iter().enumerate() {
if k == i || k == j {
continue;
}
let ca0 = c[0] - a_center[0];
let ca1 = c[1] - a_center[1];
let da = ca0 * ca0 + ca1 * ca1;
let cb0 = c[0] - b_center[0];
let cb1 = c[1] - b_center[1];
let db = cb0 * cb0 + cb1 * cb1;
let blocks = if union_mode {
da < circle_radius || db < circle_radius
} else {
da < circle_radius && db < circle_radius
};
if blocks {
empty = false;
break;
}
}
if empty {
let u = u32::try_from(i).map_err(|_| {
IgraphError::Internal("circle_beta_skeleton: vertex id overflow")
})?;
let v = u32::try_from(j).map_err(|_| {
IgraphError::Internal("circle_beta_skeleton: vertex id overflow")
})?;
graph.add_edge(u, v)?;
}
}
}
Ok(graph)
}
#[cfg(test)]
mod tests {
use super::*;
fn edge_set(g: &Graph) -> Vec<(u32, u32)> {
let mut edges: Vec<(u32, u32)> = (0..g.ecount())
.map(|e| {
let (u, v) = g
.edge(u32::try_from(e).expect("edge id fits in u32"))
.expect("edge");
(u.min(v), u.max(v))
})
.collect();
edges.sort_unstable();
edges
}
#[allow(clippy::unreadable_literal)]
fn points_25() -> Vec<Vec<f64>> {
let flat = [
0.474217, 0.0314797, 0.208089, 0.439308, 0.967367, 0.530466, 0.177005, 0.426713,
0.568462, 0.57507, 0.441834, 0.284514, 0.479224, 0.817988, 0.720209, 0.225744,
0.204941, 0.44297, 0.285318, 0.912984, 0.831097, 0.0176603, 0.827154, 0.472702,
0.173059, 0.561858, 0.156276, 0.88019, 0.65935, 0.538207, 0.570379, 0.518081, 0.900553,
0.656416, 0.726631, 0.863709, 0.380264, 0.287159, 0.31098, 0.230773, 0.243089,
0.164584, 0.967974, 0.524992, 0.726605, 0.0724703, 0.739752, 0.447069, 0.0443581,
0.444839,
];
flat.chunks_exact(2).map(<[f64]>::to_vec).collect()
}
#[test]
fn empty_point_set() {
let g = circle_beta_skeleton(&[], 1.1).expect("empty ok");
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
}
#[test]
fn single_point() {
let g = circle_beta_skeleton(&[vec![0.5, 0.5]], 2.0).expect("singleton ok");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
}
#[test]
fn two_points_always_connected() {
let g = circle_beta_skeleton(&[vec![0.0, 0.0], vec![3.0, 4.0]], 1.1).expect("pair ok");
assert_eq!(edge_set(&g), vec![(0, 1)]);
}
#[test]
fn non_finite_beta_is_error() {
assert!(matches!(
circle_beta_skeleton(&[vec![0.0, 0.0]], f64::NAN).unwrap_err(),
IgraphError::InvalidArgument(_)
));
assert!(matches!(
circle_beta_skeleton(&[vec![0.0, 0.0]], f64::INFINITY).unwrap_err(),
IgraphError::InvalidArgument(_)
));
}
#[test]
fn non_positive_beta_is_error() {
assert!(matches!(
circle_beta_skeleton(&[vec![0.0, 0.0]], 0.0).unwrap_err(),
IgraphError::InvalidArgument(_)
));
assert!(matches!(
circle_beta_skeleton(&[vec![0.0, 0.0]], -1.0).unwrap_err(),
IgraphError::InvalidArgument(_)
));
}
#[test]
fn non_2d_is_unsupported() {
let pts = vec![vec![0.0, 0.0, 0.0], vec![1.0, 1.0, 1.0]];
assert!(matches!(
circle_beta_skeleton(&pts, 2.0).unwrap_err(),
IgraphError::Unsupported(_)
));
assert!(matches!(
circle_beta_skeleton(&pts, 0.5).unwrap_err(),
IgraphError::Unsupported(_)
));
}
#[test]
fn inconsistent_dimensions_error() {
let err = circle_beta_skeleton(&[vec![0.0, 0.0], vec![1.0]], 1.1).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn beta_one_equals_gabriel() {
let pts = points_25();
let circle = circle_beta_skeleton(&pts, 1.0).expect("circle ok");
let gabriel = crate::gabriel_graph(&pts).expect("gabriel ok");
assert_eq!(edge_set(&circle), edge_set(&gabriel));
}
#[allow(clippy::unreadable_literal)]
#[test]
fn c_anchor_beta11_circle_25_points() {
let g = circle_beta_skeleton(&points_25(), 1.1).expect("ok");
let expected = vec![
(0, 22),
(1, 8),
(2, 16),
(2, 21),
(3, 24),
(4, 6),
(4, 15),
(5, 7),
(5, 15),
(5, 18),
(6, 9),
(6, 17),
(7, 22),
(7, 23),
(8, 12),
(9, 13),
(10, 22),
(11, 23),
(12, 13),
(14, 23),
(16, 17),
(18, 19),
(19, 20),
];
assert_eq!(edge_set(&g), expected);
}
}