Skip to main content

igraph/misc/
spatial.rs

1//! Spatial graphs and computational geometry (`igraph_spatial.h`).
2//!
3//! Point sets are given as a [`Matrix`] with one point per row; the number
4//! of columns is the dimension of the space. All the graph builders except
5//! [`convex_hull_2d`] are marked *experimental* in igraph 1.0.x.
6//!
7//! The lune- and circle-based β-skeletons of igraph 1.0.0 and 1.0.1 return
8//! spurious edges for some ranges of β; the wrappers of this module detect
9//! these ranges and compute the correct skeletons instead (see
10//! [`Graph::lune_beta_skeleton`]).
11//!
12//! See also [`Graph::grg_game`] (random geometric graphs, which are
13//! nearest neighbor graphs with a distance cutoff), the layouts of
14//! [`crate::layout`] (which produce point sets for these functions), and
15//! the weighted path functions of [`crate::paths`], which take the output of
16//! [`Graph::spatial_edge_lengths`] as weights.
17
18use crate::{
19    error::{Error, Result},
20    ffi::*,
21    graph::Graph,
22    igraph_call,
23    matrix::Matrix,
24    vector::{Vector, VectorInt},
25};
26
27crate::ffi_enum! {
28    /// The distance metric used by spatial functions (`igraph_metric_t`).
29    pub enum Metric: igraph_metric_t {
30        /// The Euclidean (L2) distance, `sqrt(Σ (x_i - y_i)²)`.
31        Euclidean = igraph_metric_t_IGRAPH_METRIC_EUCLIDEAN,
32        /// The Manhattan (L1, taxicab) distance, `Σ |x_i - y_i|`.
33        Manhattan = igraph_metric_t_IGRAPH_METRIC_MANHATTAN,
34    }
35}
36
37impl Metric {
38    /// Alias of [`Metric::Euclidean`] (`IGRAPH_METRIC_L2`).
39    pub const L2: Metric = Metric::Euclidean;
40    /// Alias of [`Metric::Manhattan`] (`IGRAPH_METRIC_L1`).
41    pub const L1: Metric = Metric::Manhattan;
42}
43
44impl igraph_t {
45    /// The Delaunay graph of a point set: two points are adjacent when they
46    /// share an edge of the Delaunay triangulation (tetrahedralization, ...)
47    /// of the points.
48    ///
49    /// `points` has one point per row, in any dimension `d >= 1`; vertex `i`
50    /// of the result is the point in row `i`. The Delaunay graph is a
51    /// supergraph of the [Gabriel graph](Graph::gabriel_graph), itself a
52    /// supergraph of the [relative neighborhood graph](Graph::relative_neighborhood_graph)
53    /// and of the Euclidean minimum spanning tree. The computation relies on
54    /// Qhull.
55    ///
56    /// See also [`convex_hull_2d`]: in 2D, the outer boundary of the
57    /// triangulation is the convex hull of the points.
58    ///
59    /// Binds [`igraph_delaunay_graph`](https://igraph.org/c/html/latest/igraph-Spatial.html#igraph_delaunay_graph)
60    /// (experimental). Time complexity: `O(n log n)` for `d <= 3`, and
61    /// `O(n^⌊d/2⌋ / ⌊d/2⌋!)` in general.
62    ///
63    /// # Errors
64    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for
65    /// duplicate points, non-finite coordinates, zero-dimensional points,
66    /// and (currently) for degenerate sets that do not span the space, such
67    /// as `d + 1` or more points all lying on a hyperplane.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use igraph::prelude::*;
73    /// // A 3x3 square lattice (igraph's own test case).
74    /// let pts: Vec<[f64; 2]> =
75    ///     (0..9).map(|i| [(i / 3) as f64, (i % 3) as f64]).collect();
76    /// let g = Graph::delaunay_graph(&Matrix::from_rows(&pts)?)?;
77    /// assert_eq!(g.vcount(), 9);
78    /// // 12 lattice sides plus one diagonal in each of the 4 cells.
79    /// assert_eq!(g.ecount(), 16);
80    /// # Ok::<(), igraph::Error>(())
81    /// ```
82    pub fn delaunay_graph(points: &Matrix) -> Result<Graph> {
83        Graph::init_with(|g| unsafe { igraph_delaunay_graph(g, points) })
84    }
85
86    /// The lune-based β-skeleton of a point set.
87    ///
88    /// Two points `A` and `B` are adjacent when no other point lies in their
89    /// (closed) *lune*, a region whose size grows with `beta`: larger values
90    /// of `beta` give sparser graphs. For `beta >= 1` the lune is the
91    /// intersection of the two balls of radius `beta·|AB|/2` centered on the
92    /// line `AB` and passing through `A` and `B` respectively; for
93    /// `beta < 1` it is the intersection of the two disks of radius
94    /// `|AB|/(2·beta)` whose boundaries pass through both `A` and `B`.
95    /// `beta = 1` gives the [Gabriel graph](Graph::gabriel_graph);
96    /// `beta = 2` is (almost, see
97    /// [`relative_neighborhood_graph`](Graph::relative_neighborhood_graph))
98    /// the relative neighborhood graph. Values of `beta < 1` are only
99    /// supported in 2D, and are considerably slower.
100    ///
101    /// # Correctness workarounds
102    ///
103    /// igraph 1.0.0 and 1.0.1 under-estimate the search radius of the lune for
104    /// `beta > 2` and `beta < 0.5`, and then returns spurious edges (for
105    /// `beta < 0.5`, even the complete graph). This wrapper returns the
106    /// correct skeleton in these ranges: for `beta > 2` it keeps the edges of
107    /// [`beta_weighted_gabriel_graph`](Graph::beta_weighted_gabriel_graph)
108    /// whose threshold exceeds `beta` (same complexity; point sets with no
109    /// more points than dimensions are tested pair by pair, as igraph does),
110    /// for `beta < 0.5` it tests every pair of points against every other
111    /// point (`O(n³)`).
112    ///
113    /// Binds [`igraph_lune_beta_skeleton`](https://igraph.org/c/html/latest/igraph-Spatial.html#igraph_lune_beta_skeleton)
114    /// (experimental). Time complexity: about `O(n^⌊d/2⌋ log n)`.
115    ///
116    /// # Errors
117    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) unless
118    /// `beta` is positive and finite, or for NaN or infinite coordinates;
119    /// [`ErrorKind::Unimplemented`](crate::ErrorKind::Unimplemented)
120    /// for `beta < 1` outside of 2D; and, for `beta >= 1` with more points
121    /// than dimensions (where the candidate edges come from the Delaunay
122    /// graph), the errors of [`delaunay_graph`](Graph::delaunay_graph), e.g.
123    /// for duplicate points.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use igraph::prelude::*;
129    /// // Two points and a third one slightly off their midpoint.
130    /// let pts = Matrix::from_rows(&[[0.0, 0.0], [2.0, 0.0], [1.0, 1.2]])?;
131    /// // For beta = 1 the third point is outside the disk with diameter 0-1...
132    /// assert!(Graph::lune_beta_skeleton(&pts, 1.0)?.get_eid(0, 1, false)?.is_some());
133    /// // ...but it is inside the fatter lune of beta = 2.
134    /// assert!(Graph::lune_beta_skeleton(&pts, 2.0)?.get_eid(0, 1, false)?.is_none());
135    /// # Ok::<(), igraph::Error>(())
136    /// ```
137    pub fn lune_beta_skeleton(points: &Matrix, beta: f64) -> Result<Graph> {
138        check_beta(beta)?;
139        check_finite(points)?;
140        let (n, dim) = (points.nrow(), points.ncol());
141        if beta > 2.0 {
142            if n <= dim {
143                // Too few points for a Delaunay triangulation: igraph tests
144                // every pair, so do we (there are at most `dim` points).
145                return brute_force_skeleton(points, |a, b, p| in_lune(a, b, p, beta));
146            }
147            // Thresholds up to (just above) beta are enough to decide.
148            let (g, thresholds) = Graph::beta_weighted_gabriel_graph(points, beta * (1.0 + 1e-9))?;
149            let kept: Vec<(igraph_int_t, igraph_int_t)> = g
150                .edge_list()
151                .into_iter()
152                .zip(thresholds)
153                .filter(|&(_, t)| t > beta)
154                .map(|(e, _)| e)
155                .collect();
156            return Graph::from_edges(&kept, n, false);
157        }
158        if beta < 0.5 && dim == 2 {
159            return brute_force_skeleton(points, |a, b, p| in_lens(a, b, p, beta));
160        }
161        Graph::init_with(|g| unsafe { igraph_lune_beta_skeleton(g, points, beta) })
162    }
163
164    /// The circle-based β-skeleton of a 2D point set.
165    ///
166    /// For `beta >= 1`, `A` and `B` are adjacent when no other point lies in
167    /// the *union* of the two disks of radius `beta·|AB|/2` whose boundaries
168    /// pass through both `A` and `B`; for `beta < 1` the forbidden region is
169    /// the intersection of the disks of radius `|AB|/(2·beta)`, as for the
170    /// [lune-based skeleton](Graph::lune_beta_skeleton). `beta` must be
171    /// positive; larger values give sparser graphs, and values below 1 are
172    /// considerably slower. For `beta = 1` it coincides with the Gabriel
173    /// graph.
174    ///
175    /// For `beta < 0.5` igraph 1.0.0 and 1.0.1 return spurious edges (see the
176    /// [lune-based skeleton](Graph::lune_beta_skeleton)): this wrapper then
177    /// computes the correct skeleton by brute force, in `O(n³)`.
178    ///
179    /// Binds [`igraph_circle_beta_skeleton`](https://igraph.org/c/html/latest/igraph-Spatial.html#igraph_circle_beta_skeleton)
180    /// (experimental).
181    ///
182    /// # Errors
183    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) unless
184    /// `beta` is positive and finite, or for NaN or infinite coordinates;
185    /// [`ErrorKind::Unimplemented`](crate::ErrorKind::Unimplemented)
186    /// if the points are not two-dimensional.
187    pub fn circle_beta_skeleton(points: &Matrix, beta: f64) -> Result<Graph> {
188        check_beta(beta)?;
189        check_finite(points)?;
190        if beta < 0.5 && points.ncol() == 2 {
191            return brute_force_skeleton(points, |a, b, p| in_lens(a, b, p, beta));
192        }
193        Graph::init_with(|g| unsafe { igraph_circle_beta_skeleton(g, points, beta) })
194    }
195
196    /// The Gabriel graph together with, for each edge, the threshold β at
197    /// which the edge disappears from the lune-based β-skeleton.
198    ///
199    /// The edge `e` belongs to the [lune β-skeleton](Graph::lune_beta_skeleton)
200    /// exactly for `1 <= β < weights[e]`, so this single call summarizes all
201    /// the skeletons with `β >= 1`. Edges that persist for arbitrarily large
202    /// β, or beyond the `max_beta` cutoff, get the weight
203    /// [`f64::INFINITY`]. A smaller `max_beta` makes the computation faster;
204    /// pass [`f64::INFINITY`] for no cutoff.
205    ///
206    /// Returns the graph and the weights, indexed by edge id.
207    ///
208    /// Binds [`igraph_beta_weighted_gabriel_graph`](https://igraph.org/c/html/latest/igraph-Spatial.html#igraph_beta_weighted_gabriel_graph)
209    /// (experimental).
210    ///
211    /// # Errors
212    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if
213    /// `max_beta` is NaN, and the errors of
214    /// [`delaunay_graph`](Graph::delaunay_graph) (which it uses even for
215    /// tiny point sets: it needs more points than dimensions, and there must
216    /// be no duplicate points).
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use igraph::prelude::*;
222    /// let pts = Matrix::from_rows(&[[0.0, 0.0], [2.0, 0.0], [1.0, 1.2]])?;
223    /// let (g, beta) = Graph::beta_weighted_gabriel_graph(&pts, f64::INFINITY)?;
224    /// assert_eq!(g.ecount(), 3);
225    /// let long = g.get_eid(0, 1, false)?.unwrap() as usize;
226    /// // Edge 0-1 leaves the lune skeleton at beta = 1.22 (the third point
227    /// // enters its lune), the other two sides at a much larger beta.
228    /// assert!((beta[long] - 1.22).abs() < 1e-9);
229    /// assert!(beta.iter().all(|&b| b >= beta[long]));
230    /// # Ok::<(), igraph::Error>(())
231    /// ```
232    pub fn beta_weighted_gabriel_graph(
233        points: &Matrix,
234        max_beta: f64,
235    ) -> Result<(Graph, Vec<f64>)> {
236        if max_beta.is_nan() {
237            return Err(Error::invalid("max_beta must not be NaN"));
238        }
239        let mut weights = Vector::new();
240        let g = Graph::init_with(|g| unsafe {
241            igraph_beta_weighted_gabriel_graph(g, &mut weights, points, max_beta)
242        })?;
243        Ok((g, weights.into()))
244    }
245
246    /// The Gabriel graph of a point set: `A` and `B` are adjacent when no
247    /// other point lies in the closed ball having the segment `AB` as a
248    /// diameter.
249    ///
250    /// The Gabriel graph is connected, planar in 2D, and it is the β-skeleton
251    /// (lune- or circle-based) with `β = 1`. Any dimension is supported.
252    ///
253    /// Binds [`igraph_gabriel_graph`](https://igraph.org/c/html/latest/igraph-Spatial.html#igraph_gabriel_graph)
254    /// (experimental). Time complexity: about `O(n^⌊d/2⌋ log n)`.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use igraph::prelude::*;
260    /// // An obtuse triangle: the long side has the third point inside its
261    /// // diametral circle, so it is not a Gabriel edge.
262    /// let pts = Matrix::from_rows(&[[0.0, 0.0], [4.0, 0.0], [2.0, 0.5]])?;
263    /// let g = Graph::gabriel_graph(&pts)?;
264    /// assert_eq!(g.ecount(), 2);
265    /// assert_eq!(g.get_eid(0, 1, false)?, None);
266    /// # Ok::<(), igraph::Error>(())
267    /// ```
268    pub fn gabriel_graph(points: &Matrix) -> Result<Graph> {
269        Graph::init_with(|g| unsafe { igraph_gabriel_graph(g, points) })
270    }
271
272    /// The relative neighborhood graph of a point set: `A` and `B` are
273    /// adjacent unless some other point `C` is strictly closer to both of
274    /// them than they are to each other (`AC < AB` and `BC < AB`).
275    ///
276    /// It is always connected, and it is a supergraph of the Euclidean
277    /// minimum spanning tree. Unlike the `β = 2` lune skeleton (which uses
278    /// non-strict inequalities and is triangle-free), it connects the three
279    /// corners of an equilateral triangle.
280    ///
281    /// Binds [`igraph_relative_neighborhood_graph`](https://igraph.org/c/html/latest/igraph-Spatial.html#igraph_relative_neighborhood_graph)
282    /// (experimental). Time complexity: about `O(n^⌊d/2⌋ log n)`.
283    ///
284    /// See also [`Graph::minimum_spanning_tree`]: with the
285    /// [edge lengths](Graph::spatial_edge_lengths) as weights, the minimum
286    /// spanning tree of this graph is the Euclidean minimum spanning tree of
287    /// the points.
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use igraph::prelude::*;
293    /// // A 1x2 rectangle with its center: the center is closer to every
294    /// // corner than the corners to each other along the long sides.
295    /// let pts = Matrix::from_rows(&[[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0], [1.0, 0.5]])?;
296    /// let g = Graph::relative_neighborhood_graph(&pts)?;
297    /// let mut edges = g.edge_list();
298    /// edges.sort();
299    /// // The two short sides and the four spokes to the center.
300    /// assert_eq!(edges, vec![(0, 3), (0, 4), (1, 2), (1, 4), (2, 4), (3, 4)]);
301    /// # Ok::<(), igraph::Error>(())
302    /// ```
303    pub fn relative_neighborhood_graph(points: &Matrix) -> Result<Graph> {
304        Graph::init_with(|g| unsafe { igraph_relative_neighborhood_graph(g, points) })
305    }
306
307    /// The *k* nearest neighbor graph of a point set.
308    ///
309    /// Each point is connected to (at most) its `k` nearest other points
310    /// according to `metric`, considering only points closer than `cutoff`.
311    /// `k = None` means no limit on the number of neighbors, and
312    /// `cutoff = None` no limit on the distance (with both `None` the result
313    /// is complete). With `directed`, the edge `i → j` means that `j` is
314    /// among the neighbors of `i`; otherwise `i` and `j` are connected when
315    /// *either* chose the other (mutual choices give a single edge). Ties
316    /// between equidistant neighbors are broken arbitrarily.
317    ///
318    /// Binds [`igraph_nearest_neighbor_graph`](https://igraph.org/c/html/latest/igraph-Spatial.html#igraph_nearest_neighbor_graph)
319    /// (experimental). Time complexity: `O(n log n)` (k-d tree).
320    ///
321    /// See also [`Graph::grg_game`], which samples random points in the unit
322    /// square and connects those closer than a radius: the undirected graph
323    /// built here with `k = None` and that radius as `cutoff`.
324    ///
325    /// # Errors
326    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for
327    /// zero-dimensional points, non-finite coordinates, or a negative or NaN
328    /// `cutoff`.
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// use igraph::{misc::Metric, prelude::*};
334    /// // Points on a line: 0, 1, 3, 7.
335    /// let pts = Matrix::from_rows(&[[0.0], [1.0], [3.0], [7.0]])?;
336    /// let g = Graph::nearest_neighbor_graph(&pts, Metric::Euclidean, Some(1), None, true)?;
337    /// let mut edges = g.edge_list();
338    /// edges.sort();
339    /// assert_eq!(edges, vec![(0, 1), (1, 0), (2, 1), (3, 2)]);
340    /// # Ok::<(), igraph::Error>(())
341    /// ```
342    pub fn nearest_neighbor_graph(
343        points: &Matrix,
344        metric: Metric,
345        k: Option<usize>,
346        cutoff: Option<f64>,
347        directed: bool,
348    ) -> Result<Graph> {
349        // More neighbors than points means no limit at all.
350        let k = k.map_or(-1, |k| {
351            igraph_int_t::try_from(k).unwrap_or(igraph_int_t::MAX)
352        });
353        let cutoff = match cutoff {
354            None => f64::INFINITY,
355            Some(c) if c >= 0.0 => c,
356            Some(c) => {
357                return Err(Error::invalid(format!(
358                    "the cutoff distance must be non-negative, got {c}"
359                )));
360            }
361        };
362        Graph::init_with(|g| unsafe {
363            igraph_nearest_neighbor_graph(g, points, metric.into(), k, cutoff, directed)
364        })
365    }
366
367    /// The length of each edge, computed from the coordinates of its
368    /// endpoints with the given `metric`, indexed by edge id.
369    ///
370    /// Row `i` of `points` holds the coordinates of vertex `i`, in any
371    /// dimension. The lengths can be used as weights by path-length based
372    /// functions, e.g. [`Graph::distances_dijkstra`],
373    /// [`Graph::minimum_spanning_tree`], [`Graph::betweenness`],
374    /// [`Graph::closeness`] or [`Graph::voronoi`].
375    ///
376    /// Binds [`igraph_spatial_edge_lengths`](https://igraph.org/c/html/latest/igraph-Spatial.html#igraph_spatial_edge_lengths)
377    /// (experimental). Time complexity: `O(|E| d)`.
378    ///
379    /// # Errors
380    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the
381    /// number of rows differs from the number of vertices, or the points are
382    /// zero-dimensional (a `0 × 0` matrix is accepted for the null graph).
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use igraph::{misc::Metric, prelude::*};
388    /// let g = Graph::from_edges(&[(0, 1), (0, 2)], 3, false)?;
389    /// let pts = Matrix::from_rows(&[[0.0, 0.0], [3.0, 4.0], [1.0, 1.0]])?;
390    /// assert_eq!(g.spatial_edge_lengths(&pts, Metric::Euclidean)?[0], 5.0);
391    /// assert_eq!(g.spatial_edge_lengths(&pts, Metric::Manhattan)?, vec![7.0, 2.0]);
392    /// # Ok::<(), igraph::Error>(())
393    /// ```
394    pub fn spatial_edge_lengths(&self, points: &Matrix, metric: Metric) -> Result<Vec<f64>> {
395        let mut res = Vector::new();
396        igraph_call!(igraph_spatial_edge_lengths(
397            self,
398            &mut res,
399            points,
400            metric.into()
401        ))?;
402        Ok(res.into())
403    }
404}
405
406fn check_beta(beta: f64) -> Result<()> {
407    if beta > 0.0 && beta.is_finite() {
408        Ok(())
409    } else {
410        Err(Error::invalid(format!(
411            "beta must be positive and finite, got {beta}"
412        )))
413    }
414}
415
416fn check_finite(points: &Matrix) -> Result<()> {
417    if points.as_slice().iter().all(|x| x.is_finite()) {
418        Ok(())
419    } else {
420        Err(Error::invalid("coordinates must not be NaN or infinite"))
421    }
422}
423
424/// igraph's relative tolerance: the forbidden regions are closed.
425const TOL: f64 = 1.0 + 128.0 * f64::EPSILON;
426
427fn sqr_dist(p: &[f64], q: &[f64]) -> f64 {
428    p.iter().zip(q).map(|(x, y)| (x - y) * (x - y)).sum()
429}
430
431/// Whether `p` lies in the closed lune of the segment `ab` for `beta >= 1`:
432/// the intersection of the balls of radius `beta·|ab|/2` centered at
433/// `a + (beta/2 - 1)(a - b)` and `b + (beta/2 - 1)(b - a)` (any dimension).
434fn in_lune(a: &[f64], b: &[f64], p: &[f64], beta: f64) -> bool {
435    let r = beta / 2.0;
436    let radius2 = r * r * sqr_dist(a, b) * TOL * TOL;
437    let ca: Vec<f64> = a
438        .iter()
439        .zip(b)
440        .map(|(x, y)| x + (r - 1.0) * (x - y))
441        .collect();
442    let cb: Vec<f64> = a
443        .iter()
444        .zip(b)
445        .map(|(x, y)| y + (r - 1.0) * (y - x))
446        .collect();
447    sqr_dist(p, &ca) <= radius2 && sqr_dist(p, &cb) <= radius2
448}
449
450/// Whether the 2D point `p` lies in the closed lens of the segment `ab` for
451/// `beta < 1`: the intersection of the two disks of radius `|ab|/(2 beta)`
452/// whose boundaries pass through both `a` and `b`.
453fn in_lens(a: &[f64], b: &[f64], p: &[f64], beta: f64) -> bool {
454    let d2 = sqr_dist(a, b);
455    let mid = [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0];
456    // The lens lies within the disk having `ab` as a diameter.
457    if sqr_dist(p, &mid) > d2 / 4.0 * TOL * TOL {
458        return false;
459    }
460    let r = 0.5 / beta;
461    let shift = (r * r - 0.25).sqrt();
462    let perp = [-(a[1] - b[1]) * shift, (a[0] - b[0]) * shift];
463    let c1 = [mid[0] + perp[0], mid[1] + perp[1]];
464    let c2 = [mid[0] - perp[0], mid[1] - perp[1]];
465    let radius2 = r * r * d2 * TOL * TOL;
466    sqr_dist(p, &c1) <= radius2 && sqr_dist(p, &c2) <= radius2
467}
468
469/// A β-skeleton by brute force, in `O(n³ d)`: `a` and `b` are adjacent
470/// unless `blocked(a, b, p)` holds for some other point `p`. Used where
471/// igraph 1.0.0 and 1.0.1 are wrong.
472fn brute_force_skeleton(
473    points: &Matrix,
474    blocked: impl Fn(&[f64], &[f64], &[f64]) -> bool,
475) -> Result<Graph> {
476    let n = points.nrow();
477    let pts = points.to_rows();
478    let mut edges = Vec::new();
479    for a in 0..n {
480        for b in a + 1..n {
481            let hit = (0..n).any(|k| k != a && k != b && blocked(&pts[a], &pts[b], &pts[k]));
482            if !hit {
483                edges.push((a as igraph_int_t, b as igraph_int_t));
484            }
485        }
486    }
487    Graph::from_edges(&edges, n, false)
488}
489
490/// The convex hull of a 2D point set, as returned by [`convex_hull_2d`].
491#[derive(Debug, Clone, PartialEq)]
492pub struct ConvexHull {
493    /// The indices (rows of the input matrix) of the corners of the hull,
494    /// in order along the boundary. Points in the interior of a side are
495    /// not corners.
496    pub vertices: Vec<i64>,
497    /// The coordinates of the corners, in the same order as `vertices`.
498    pub points: Vec<[f64; 2]>,
499}
500
501impl ConvexHull {
502    /// The (non-negative) area enclosed by the hull, by the shoelace formula.
503    pub fn area(&self) -> f64 {
504        let n = self.points.len();
505        let twice: f64 = (0..n)
506            .map(|i| {
507                let [x1, y1] = self.points[i];
508                let [x2, y2] = self.points[(i + 1) % n];
509                x1 * y2 - x2 * y1
510            })
511            .sum();
512        twice.abs() / 2.0
513    }
514
515    /// The perimeter of the hull.
516    pub fn perimeter(&self) -> f64 {
517        let n = self.points.len();
518        if n < 2 {
519            return 0.0;
520        }
521        (0..n)
522            .map(|i| {
523                let [x1, y1] = self.points[i];
524                let [x2, y2] = self.points[(i + 1) % n];
525                (x2 - x1).hypot(y2 - y1)
526            })
527            .sum()
528    }
529}
530
531/// The convex hull of a set of points in the plane, by the Graham scan.
532///
533/// `points` must have two columns (x and y) and one point per row. The
534/// corners are reported in order along the boundary; collinear points on
535/// the sides are left out. Degenerate inputs are fine: one point gives a
536/// one-corner hull, collinear points give the two extremes, no points an
537/// empty hull.
538///
539/// Binds [`igraph_convex_hull_2d`](https://igraph.org/c/html/latest/igraph-Spatial.html#igraph_convex_hull_2d).
540/// Time complexity: `O(n log n)`.
541///
542/// See also [`Graph::layout_circle`](crate::Graph::layout_circle) and the
543/// other layouts of [`crate::layout`], whose coordinate matrices can be
544/// passed directly.
545///
546/// # Errors
547/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `points`
548/// does not have exactly two columns, or has NaN or infinite coordinates.
549///
550/// # Examples
551///
552/// ```
553/// use igraph::{misc, prelude::*};
554/// let pts = Matrix::from_rows(&[[0.0, 0.0], [2.0, 0.0], [1.0, 1.0], [2.0, 2.0], [0.0, 2.0]])?;
555/// let hull = misc::convex_hull_2d(&pts)?;
556/// let mut corners = hull.vertices.clone();
557/// corners.sort();
558/// assert_eq!(corners, vec![0, 1, 3, 4]); // the center point (row 2) is inside
559/// assert_eq!(hull.area(), 4.0);
560/// assert_eq!(hull.perimeter(), 8.0);
561/// # Ok::<(), igraph::Error>(())
562/// ```
563pub fn convex_hull_2d(points: &Matrix) -> Result<ConvexHull> {
564    check_finite(points)?;
565    let mut vertices = VectorInt::new();
566    let mut coords = Matrix::new();
567    igraph_call!(igraph_convex_hull_2d(points, &mut vertices, &mut coords))?;
568    let points = (0..coords.nrow())
569        .map(|i| [coords[(i, 0)], coords[(i, 1)]])
570        .collect();
571    Ok(ConvexHull {
572        vertices: vertices.into(),
573        points,
574    })
575}