1use crate::core::{Graph, IgraphError, IgraphResult};
26
27const TOLERANCE: f64 = 128.0 * f64::EPSILON;
32
33#[allow(clippy::many_single_char_names)]
75pub fn lune_beta_skeleton(points: &[Vec<f64>], beta: f64) -> IgraphResult<Graph> {
76 if !beta.is_finite() || beta <= 0.0 {
77 return Err(IgraphError::InvalidArgument(format!(
78 "lune_beta_skeleton: beta must be a finite positive number, got {beta}"
79 )));
80 }
81
82 let n = points.len();
83 let dim = if n == 0 { 0 } else { points[0].len() };
84 if n > 0 {
85 if dim == 0 {
86 return Err(IgraphError::InvalidArgument(
87 "lune_beta_skeleton: points must not be zero-dimensional".into(),
88 ));
89 }
90 for (i, row) in points.iter().enumerate().skip(1) {
91 if row.len() != dim {
92 return Err(IgraphError::InvalidArgument(format!(
93 "lune_beta_skeleton: point row {i} has dimension {} but expected {dim}",
94 row.len()
95 )));
96 }
97 }
98 }
99
100 if beta < 1.0 && n > 0 && dim != 2 {
101 return Err(IgraphError::Unsupported(
102 "lune_beta_skeleton: beta < 1 is only supported in 2 dimensions",
103 ));
104 }
105
106 let n_u32 = u32::try_from(n)
107 .map_err(|_| IgraphError::InvalidArgument("lune_beta_skeleton: too many points".into()))?;
108 let mut graph = Graph::with_vertices(n_u32);
109
110 let r = if beta < 1.0 { 0.5 / beta } else { 0.5 * beta };
111 let tol_sq = (1.0 + TOLERANCE) * (1.0 + TOLERANCE);
112 let perp_scale = if beta < 1.0 {
115 (r * r - 0.25).max(0.0).sqrt()
116 } else {
117 0.0
118 };
119
120 let mut a_center = vec![0.0_f64; dim];
121 let mut b_center = vec![0.0_f64; dim];
122
123 for i in 0..n {
124 let a = &points[i];
125 for j in (i + 1)..n {
126 let b = &points[j];
127
128 let mut dist_sq = 0.0_f64;
129 for d in 0..dim {
130 let t = a[d] - b[d];
131 dist_sq += t * t;
132 }
133
134 if beta < 1.0 {
135 let mid0 = 0.5 * (a[0] + b[0]);
137 let mid1 = 0.5 * (a[1] + b[1]);
138 let perp0 = -(a[1] - b[1]) * perp_scale;
140 let perp1 = (a[0] - b[0]) * perp_scale;
141 a_center[0] = mid0 + perp0;
142 a_center[1] = mid1 + perp1;
143 b_center[0] = mid0 - perp0;
144 b_center[1] = mid1 - perp1;
145 } else {
146 for d in 0..dim {
147 a_center[d] = a[d] + (r - 1.0) * (a[d] - b[d]);
148 b_center[d] = b[d] + (r - 1.0) * (b[d] - a[d]);
149 }
150 }
151
152 let beta_radius = dist_sq * r * r * tol_sq;
155
156 let mut empty = true;
157 for (k, c) in points.iter().enumerate() {
158 if k == i || k == j {
159 continue;
160 }
161 let mut da = 0.0_f64;
162 let mut db = 0.0_f64;
163 for d in 0..dim {
164 let ta = c[d] - a_center[d];
165 da += ta * ta;
166 let tb = c[d] - b_center[d];
167 db += tb * tb;
168 }
169 if da < beta_radius && db < beta_radius {
170 empty = false;
171 break;
172 }
173 }
174
175 if empty {
176 let u = u32::try_from(i)
177 .map_err(|_| IgraphError::Internal("lune_beta_skeleton: vertex id overflow"))?;
178 let v = u32::try_from(j)
179 .map_err(|_| IgraphError::Internal("lune_beta_skeleton: vertex id overflow"))?;
180 graph.add_edge(u, v)?;
181 }
182 }
183 }
184
185 Ok(graph)
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn edge_set(g: &Graph) -> Vec<(u32, u32)> {
194 let mut edges: Vec<(u32, u32)> = (0..g.ecount())
195 .map(|e| {
196 let (u, v) = g
197 .edge(u32::try_from(e).expect("edge id fits in u32"))
198 .expect("edge");
199 (u.min(v), u.max(v))
200 })
201 .collect();
202 edges.sort_unstable();
203 edges
204 }
205
206 #[allow(clippy::unreadable_literal)]
209 fn points_25() -> Vec<Vec<f64>> {
210 let flat = [
211 0.474217, 0.0314797, 0.208089, 0.439308, 0.967367, 0.530466, 0.177005, 0.426713,
212 0.568462, 0.57507, 0.441834, 0.284514, 0.479224, 0.817988, 0.720209, 0.225744,
213 0.204941, 0.44297, 0.285318, 0.912984, 0.831097, 0.0176603, 0.827154, 0.472702,
214 0.173059, 0.561858, 0.156276, 0.88019, 0.65935, 0.538207, 0.570379, 0.518081, 0.900553,
215 0.656416, 0.726631, 0.863709, 0.380264, 0.287159, 0.31098, 0.230773, 0.243089,
216 0.164584, 0.967974, 0.524992, 0.726605, 0.0724703, 0.739752, 0.447069, 0.0443581,
217 0.444839,
218 ];
219 flat.chunks_exact(2).map(<[f64]>::to_vec).collect()
220 }
221
222 #[allow(clippy::unreadable_literal)]
226 fn points_10_3d() -> Vec<Vec<f64>> {
227 let flat = [
228 0.474217, 0.0314797, 0.208089, 0.439308, 0.967367, 0.530466, 0.177005, 0.426713,
229 0.568462, 0.57507, 0.441834, 0.284514, 0.479224, 0.817988, 0.720209, 0.225744,
230 0.204941, 0.44297, 0.285318, 0.912984, 0.831097, 0.0176603, 0.827154, 0.472702,
231 0.173059, 0.561858, 0.156276, 0.88019, 0.65935, 0.538207,
232 ];
233 flat.chunks_exact(3).map(<[f64]>::to_vec).collect()
234 }
235
236 #[allow(clippy::unreadable_literal)]
238 fn triangular_lattice() -> Vec<Vec<f64>> {
239 vec![
240 vec![0.5, 2.598076211353316],
241 vec![0.0, 1.7320508075688772],
242 vec![1.0, 1.7320508075688772],
243 vec![-0.5, 0.8660254037844386],
244 vec![0.5, 0.8660254037844386],
245 vec![1.5, 0.8660254037844386],
246 vec![-1.0, 0.0],
247 vec![0.0, 0.0],
248 vec![1.0, 0.0],
249 vec![2.0, 0.0],
250 ]
251 }
252
253 #[test]
254 fn empty_point_set() {
255 let g = lune_beta_skeleton(&[], 1.0).expect("empty ok");
256 assert_eq!(g.vcount(), 0);
257 assert_eq!(g.ecount(), 0);
258 }
259
260 #[test]
261 fn single_point() {
262 let g = lune_beta_skeleton(&[vec![0.5, 0.5]], 2.0).expect("singleton ok");
263 assert_eq!(g.vcount(), 1);
264 assert_eq!(g.ecount(), 0);
265 }
266
267 #[test]
268 fn two_points_always_connected() {
269 let g = lune_beta_skeleton(&[vec![0.0, 0.0], vec![3.0, 4.0]], 2.0).expect("pair ok");
270 assert_eq!(edge_set(&g), vec![(0, 1)]);
271 }
272
273 #[test]
274 fn non_finite_beta_is_error() {
275 assert!(matches!(
276 lune_beta_skeleton(&[vec![0.0, 0.0]], f64::NAN).unwrap_err(),
277 IgraphError::InvalidArgument(_)
278 ));
279 assert!(matches!(
280 lune_beta_skeleton(&[vec![0.0, 0.0]], f64::INFINITY).unwrap_err(),
281 IgraphError::InvalidArgument(_)
282 ));
283 }
284
285 #[test]
286 fn non_positive_beta_is_error() {
287 assert!(matches!(
288 lune_beta_skeleton(&[vec![0.0, 0.0]], 0.0).unwrap_err(),
289 IgraphError::InvalidArgument(_)
290 ));
291 assert!(matches!(
292 lune_beta_skeleton(&[vec![0.0, 0.0]], -1.0).unwrap_err(),
293 IgraphError::InvalidArgument(_)
294 ));
295 }
296
297 #[test]
298 fn zero_dimensional_error() {
299 let err = lune_beta_skeleton(&[vec![], vec![]], 1.0).unwrap_err();
300 assert!(matches!(err, IgraphError::InvalidArgument(_)));
301 }
302
303 #[test]
304 fn inconsistent_dimensions_error() {
305 let err = lune_beta_skeleton(&[vec![0.0, 0.0], vec![1.0]], 1.0).unwrap_err();
306 assert!(matches!(err, IgraphError::InvalidArgument(_)));
307 }
308
309 #[test]
310 fn small_beta_non_2d_is_unsupported() {
311 let pts = vec![vec![0.0, 0.0, 0.0], vec![1.0, 1.0, 1.0]];
312 let err = lune_beta_skeleton(&pts, 0.5).unwrap_err();
313 assert!(matches!(err, IgraphError::Unsupported(_)));
314 }
315
316 #[test]
317 fn beta_one_equals_gabriel() {
318 let pts = points_25();
320 let lune = lune_beta_skeleton(&pts, 1.0).expect("lune ok");
321 let gabriel = crate::gabriel_graph(&pts).expect("gabriel ok");
322 assert_eq!(edge_set(&lune), edge_set(&gabriel));
323 }
324
325 #[allow(clippy::unreadable_literal)]
328 #[test]
329 fn c_anchor_beta2_25_points() {
330 let g = lune_beta_skeleton(&points_25(), 2.0).expect("ok");
331 let expected = vec![
332 (0, 5),
333 (0, 22),
334 (1, 8),
335 (1, 18),
336 (2, 16),
337 (2, 21),
338 (3, 8),
339 (3, 24),
340 (4, 6),
341 (4, 15),
342 (5, 7),
343 (5, 15),
344 (5, 18),
345 (6, 9),
346 (6, 17),
347 (7, 22),
348 (7, 23),
349 (8, 12),
350 (9, 13),
351 (10, 22),
352 (11, 21),
353 (11, 23),
354 (12, 13),
355 (14, 15),
356 (14, 23),
357 (16, 17),
358 (18, 19),
359 (19, 20),
360 ];
361 assert_eq!(edge_set(&g), expected);
362 }
363
364 #[allow(clippy::unreadable_literal)]
365 #[test]
366 fn c_anchor_gabriel_25_points() {
367 let g = lune_beta_skeleton(&points_25(), 1.0).expect("ok");
368 let expected = vec![
369 (0, 5),
370 (0, 19),
371 (0, 20),
372 (0, 22),
373 (1, 8),
374 (1, 15),
375 (1, 18),
376 (1, 19),
377 (2, 16),
378 (2, 21),
379 (3, 8),
380 (3, 24),
381 (4, 6),
382 (4, 12),
383 (4, 14),
384 (4, 15),
385 (4, 17),
386 (5, 7),
387 (5, 15),
388 (5, 18),
389 (6, 9),
390 (6, 12),
391 (6, 17),
392 (7, 22),
393 (7, 23),
394 (8, 12),
395 (9, 13),
396 (10, 22),
397 (11, 16),
398 (11, 21),
399 (11, 23),
400 (12, 13),
401 (12, 24),
402 (14, 15),
403 (14, 17),
404 (14, 23),
405 (16, 17),
406 (18, 19),
407 (19, 20),
408 ];
409 assert_eq!(edge_set(&g), expected);
410 }
411
412 #[allow(clippy::unreadable_literal)]
413 #[test]
414 fn c_anchor_beta2_10_points_3d() {
415 let g = lune_beta_skeleton(&points_10_3d(), 2.0).expect("ok");
416 let expected = vec![
417 (0, 3),
418 (0, 5),
419 (1, 4),
420 (1, 7),
421 (2, 4),
422 (2, 5),
423 (2, 8),
424 (3, 8),
425 (3, 9),
426 (4, 6),
427 (4, 9),
428 (7, 8),
429 ];
430 assert_eq!(edge_set(&g), expected);
431 }
432
433 #[test]
434 fn c_anchor_beta2_triangular_lattice_is_empty() {
435 let g = lune_beta_skeleton(&triangular_lattice(), 2.0).expect("ok");
438 assert_eq!(g.vcount(), 10);
439 assert_eq!(g.ecount(), 0);
440 }
441}