rust_igraph/algorithms/spatial/
circle_beta_skeleton.rs1use crate::core::{Graph, IgraphError, IgraphResult};
30
31const TOLERANCE: f64 = 128.0 * f64::EPSILON;
34
35#[allow(clippy::many_single_char_names)]
75#[allow(clippy::similar_names)]
76pub fn circle_beta_skeleton(points: &[Vec<f64>], beta: f64) -> IgraphResult<Graph> {
77 if !beta.is_finite() || beta <= 0.0 {
78 return Err(IgraphError::InvalidArgument(format!(
79 "circle_beta_skeleton: beta must be a finite positive number, got {beta}"
80 )));
81 }
82
83 let n = points.len();
84 let dim = if n == 0 { 0 } else { points[0].len() };
85 if n > 0 {
86 for (i, row) in points.iter().enumerate().skip(1) {
87 if row.len() != dim {
88 return Err(IgraphError::InvalidArgument(format!(
89 "circle_beta_skeleton: point row {i} has dimension {} but expected {dim}",
90 row.len()
91 )));
92 }
93 }
94 if dim != 2 {
95 return Err(IgraphError::Unsupported(
96 "circle_beta_skeleton: circle-based beta skeletons are only supported in 2 dimensions",
97 ));
98 }
99 }
100
101 let n_u32 = u32::try_from(n).map_err(|_| {
102 IgraphError::InvalidArgument("circle_beta_skeleton: too many points".into())
103 })?;
104 let mut graph = Graph::with_vertices(n_u32);
105
106 let r = if beta < 1.0 { 0.5 / beta } else { 0.5 * beta };
107 let perp_scale = (r * r - 0.25).max(0.0).sqrt();
110
111 let union_mode = beta >= 1.0;
115 let tol = 1.0 + TOLERANCE;
116 let radius_factor = if union_mode { tol } else { tol * tol };
117
118 let mut a_center = [0.0_f64; 2];
119 let mut b_center = [0.0_f64; 2];
120
121 for i in 0..n {
122 let a = &points[i];
123 for j in (i + 1)..n {
124 let b = &points[j];
125
126 let dx = a[0] - b[0];
127 let dy = a[1] - b[1];
128 let dist_sq = dx * dx + dy * dy;
129
130 let mid0 = 0.5 * (a[0] + b[0]);
132 let mid1 = 0.5 * (a[1] + b[1]);
133 let perp0 = -dy * perp_scale;
135 let perp1 = dx * perp_scale;
136 a_center[0] = mid0 + perp0;
137 a_center[1] = mid1 + perp1;
138 b_center[0] = mid0 - perp0;
139 b_center[1] = mid1 - perp1;
140
141 let circle_radius = dist_sq * r * r * radius_factor;
142
143 let mut empty = true;
144 for (k, c) in points.iter().enumerate() {
145 if k == i || k == j {
146 continue;
147 }
148 let ca0 = c[0] - a_center[0];
149 let ca1 = c[1] - a_center[1];
150 let da = ca0 * ca0 + ca1 * ca1;
151 let cb0 = c[0] - b_center[0];
152 let cb1 = c[1] - b_center[1];
153 let db = cb0 * cb0 + cb1 * cb1;
154
155 let blocks = if union_mode {
156 da < circle_radius || db < circle_radius
158 } else {
159 da < circle_radius && db < circle_radius
161 };
162 if blocks {
163 empty = false;
164 break;
165 }
166 }
167
168 if empty {
169 let u = u32::try_from(i).map_err(|_| {
170 IgraphError::Internal("circle_beta_skeleton: vertex id overflow")
171 })?;
172 let v = u32::try_from(j).map_err(|_| {
173 IgraphError::Internal("circle_beta_skeleton: vertex id overflow")
174 })?;
175 graph.add_edge(u, v)?;
176 }
177 }
178 }
179
180 Ok(graph)
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 fn edge_set(g: &Graph) -> Vec<(u32, u32)> {
189 let mut edges: Vec<(u32, u32)> = (0..g.ecount())
190 .map(|e| {
191 let (u, v) = g
192 .edge(u32::try_from(e).expect("edge id fits in u32"))
193 .expect("edge");
194 (u.min(v), u.max(v))
195 })
196 .collect();
197 edges.sort_unstable();
198 edges
199 }
200
201 #[allow(clippy::unreadable_literal)]
204 fn points_25() -> Vec<Vec<f64>> {
205 let flat = [
206 0.474217, 0.0314797, 0.208089, 0.439308, 0.967367, 0.530466, 0.177005, 0.426713,
207 0.568462, 0.57507, 0.441834, 0.284514, 0.479224, 0.817988, 0.720209, 0.225744,
208 0.204941, 0.44297, 0.285318, 0.912984, 0.831097, 0.0176603, 0.827154, 0.472702,
209 0.173059, 0.561858, 0.156276, 0.88019, 0.65935, 0.538207, 0.570379, 0.518081, 0.900553,
210 0.656416, 0.726631, 0.863709, 0.380264, 0.287159, 0.31098, 0.230773, 0.243089,
211 0.164584, 0.967974, 0.524992, 0.726605, 0.0724703, 0.739752, 0.447069, 0.0443581,
212 0.444839,
213 ];
214 flat.chunks_exact(2).map(<[f64]>::to_vec).collect()
215 }
216
217 #[test]
218 fn empty_point_set() {
219 let g = circle_beta_skeleton(&[], 1.1).expect("empty ok");
220 assert_eq!(g.vcount(), 0);
221 assert_eq!(g.ecount(), 0);
222 }
223
224 #[test]
225 fn single_point() {
226 let g = circle_beta_skeleton(&[vec![0.5, 0.5]], 2.0).expect("singleton ok");
227 assert_eq!(g.vcount(), 1);
228 assert_eq!(g.ecount(), 0);
229 }
230
231 #[test]
232 fn two_points_always_connected() {
233 let g = circle_beta_skeleton(&[vec![0.0, 0.0], vec![3.0, 4.0]], 1.1).expect("pair ok");
234 assert_eq!(edge_set(&g), vec![(0, 1)]);
235 }
236
237 #[test]
238 fn non_finite_beta_is_error() {
239 assert!(matches!(
240 circle_beta_skeleton(&[vec![0.0, 0.0]], f64::NAN).unwrap_err(),
241 IgraphError::InvalidArgument(_)
242 ));
243 assert!(matches!(
244 circle_beta_skeleton(&[vec![0.0, 0.0]], f64::INFINITY).unwrap_err(),
245 IgraphError::InvalidArgument(_)
246 ));
247 }
248
249 #[test]
250 fn non_positive_beta_is_error() {
251 assert!(matches!(
252 circle_beta_skeleton(&[vec![0.0, 0.0]], 0.0).unwrap_err(),
253 IgraphError::InvalidArgument(_)
254 ));
255 assert!(matches!(
256 circle_beta_skeleton(&[vec![0.0, 0.0]], -1.0).unwrap_err(),
257 IgraphError::InvalidArgument(_)
258 ));
259 }
260
261 #[test]
262 fn non_2d_is_unsupported() {
263 let pts = vec![vec![0.0, 0.0, 0.0], vec![1.0, 1.0, 1.0]];
266 assert!(matches!(
267 circle_beta_skeleton(&pts, 2.0).unwrap_err(),
268 IgraphError::Unsupported(_)
269 ));
270 assert!(matches!(
271 circle_beta_skeleton(&pts, 0.5).unwrap_err(),
272 IgraphError::Unsupported(_)
273 ));
274 }
275
276 #[test]
277 fn inconsistent_dimensions_error() {
278 let err = circle_beta_skeleton(&[vec![0.0, 0.0], vec![1.0]], 1.1).unwrap_err();
279 assert!(matches!(err, IgraphError::InvalidArgument(_)));
280 }
281
282 #[test]
283 fn beta_one_equals_gabriel() {
284 let pts = points_25();
287 let circle = circle_beta_skeleton(&pts, 1.0).expect("circle ok");
288 let gabriel = crate::gabriel_graph(&pts).expect("gabriel ok");
289 assert_eq!(edge_set(&circle), edge_set(&gabriel));
290 }
291
292 #[allow(clippy::unreadable_literal)]
295 #[test]
296 fn c_anchor_beta11_circle_25_points() {
297 let g = circle_beta_skeleton(&points_25(), 1.1).expect("ok");
298 let expected = vec![
299 (0, 22),
300 (1, 8),
301 (2, 16),
302 (2, 21),
303 (3, 24),
304 (4, 6),
305 (4, 15),
306 (5, 7),
307 (5, 15),
308 (5, 18),
309 (6, 9),
310 (6, 17),
311 (7, 22),
312 (7, 23),
313 (8, 12),
314 (9, 13),
315 (10, 22),
316 (11, 23),
317 (12, 13),
318 (14, 23),
319 (16, 17),
320 (18, 19),
321 (19, 20),
322 ];
323 assert_eq!(edge_set(&g), expected);
324 }
325}