igraph/layout.rs
1//! Graph layouts: placing vertices in the plane or in 3D space (`igraph_layout.h`).
2//!
3//! A *layout* assigns coordinates to every vertex of a graph, typically to
4//! draw it. Every layout function of this module returns a [`Matrix`] with
5//! **one row per vertex** (row `i` holds the coordinates of vertex `i`) and
6//! two columns (`x`, `y`) for 2D layouts, or three columns (`x`, `y`, `z`) for
7//! the `*_3d` variants. Use [`Matrix::row`], [`Matrix::column`] or
8//! [`Matrix::to_rows`] to read them.
9//!
10//! This module binds the whole of the C header `igraph_layout.h`: simple
11//! geometric layouts, force-directed layouts (with tunable *options structs*
12//! whose [`Default`] follows the recommendations of the igraph documentation),
13//! tree and layered layouts, dimensionality reduction layouts (MDS, UMAP) and
14//! a few helpers.
15//!
16//! ```
17//! use igraph::prelude::*;
18//! use igraph::layout::FruchtermanReingoldOptions;
19//!
20//! // A 6-cycle.
21//! let g = Graph::ring(6, false, false, true).unwrap();
22//!
23//! // Deterministic geometric layout: all vertices on the unit circle.
24//! let circle = g.layout_circle(..).unwrap();
25//! assert_eq!(circle.shape(), (6, 2));
26//! for p in circle.rows() {
27//! assert!((p[0].hypot(p[1]) - 1.0).abs() < 1e-12);
28//! }
29//!
30//! // Randomized force-directed layout, made reproducible by seeding the RNG.
31//! let opts = FruchtermanReingoldOptions::default();
32//! rng::seed(42).unwrap();
33//! let fr = g.layout_fruchterman_reingold(&opts).unwrap();
34//! rng::seed(42).unwrap();
35//! assert_eq!(g.layout_fruchterman_reingold(&opts).unwrap(), fr);
36//! assert_eq!(fr.shape(), (6, 2));
37//! ```
38//!
39//! # Provided functionality
40//!
41//! | Family | 2D | 3D |
42//! |--------|----|----|
43//! | Random | [`layout_random`](Graph::layout_random) | [`layout_random_3d`](Graph::layout_random_3d) |
44//! | Regular shapes | [`layout_circle`](Graph::layout_circle), [`layout_star`](Graph::layout_star), [`layout_grid`](Graph::layout_grid) | [`layout_sphere`](Graph::layout_sphere), [`layout_grid_3d`](Graph::layout_grid_3d) |
45//! | Fruchterman–Reingold | [`layout_fruchterman_reingold`](Graph::layout_fruchterman_reingold) | [`layout_fruchterman_reingold_3d`](Graph::layout_fruchterman_reingold_3d) |
46//! | Kamada–Kawai | [`layout_kamada_kawai`](Graph::layout_kamada_kawai) | [`layout_kamada_kawai_3d`](Graph::layout_kamada_kawai_3d) |
47//! | DrL | [`layout_drl`](Graph::layout_drl) | [`layout_drl_3d`](Graph::layout_drl_3d) |
48//! | UMAP | [`layout_umap`](Graph::layout_umap), [`layout_umap_compute_weights`](Graph::layout_umap_compute_weights) | [`layout_umap_3d`](Graph::layout_umap_3d) |
49//! | Other force-directed | [`layout_lgl`](Graph::layout_lgl), [`layout_graphopt`](Graph::layout_graphopt), [`layout_gem`](Graph::layout_gem), [`layout_davidson_harel`](Graph::layout_davidson_harel) | |
50//! | Trees | [`layout_reingold_tilford`](Graph::layout_reingold_tilford), [`layout_reingold_tilford_circular`](Graph::layout_reingold_tilford_circular), [`roots_for_tree_layout`](Graph::roots_for_tree_layout) | |
51//! | Layered / bipartite | [`layout_sugiyama`](Graph::layout_sugiyama), [`layout_bipartite`](Graph::layout_bipartite) | |
52//! | Distance based | [`layout_mds`](Graph::layout_mds) (any dimension) | |
53//! | Helpers | [`layout_align`](Graph::layout_align) (any dimension), [`layout_merge_dla`] | |
54//!
55//! # Starting positions and reproducibility
56//!
57//! Iterative layouts (Fruchterman–Reingold, Kamada–Kawai, DrL, UMAP, graphopt,
58//! GEM, Davidson–Harel) can start from a given layout: set the `initial` field
59//! of their options (or pass it explicitly) to refine an existing layout,
60//! otherwise they start from a random (or, for Kamada–Kawai, circular)
61//! configuration. Random choices (random starts, LGL's random root, GEM's
62//! vertex order, DLA random walks, ...) use the calling thread's default
63//! random number generator. Every thread has its own, so
64//! [`rng::seed`](crate::rng::seed) makes the layouts computed afterwards in
65//! that thread reproducible, whatever other threads do. To leave the
66//! thread's default stream untouched, run the layout inside
67//! [`Rng::scoped`](crate::rng::Rng::scoped) with a generator of your own:
68//!
69//! ```
70//! use igraph::prelude::*;
71//! use igraph::layout::GemOptions;
72//!
73//! let g = Graph::ring(4, false, false, true).unwrap();
74//! let run = || Rng::new(RngType::Pcg64, 42).unwrap().scoped(|| g.layout_gem(&GemOptions::default()));
75//! assert_eq!(run().unwrap(), run().unwrap());
76//! ```
77//!
78//! # See also
79//!
80//! - Graphs to lay out: [`Graph::famous`], [`Graph::ring`],
81//! [`Graph::kary_tree`], [`Graph::square_lattice`] (constructors module).
82//! - Disconnected graphs: [`Graph::decompose`] splits a graph into its
83//! components, which can be laid out separately and merged with
84//! [`layout_merge_dla`].
85//! - Inputs of some layouts: [`Graph::distances`] (the default distance
86//! matrix of [`layout_mds`](Graph::layout_mds)), [`Graph::bipartite_types`]
87//! (the `types` of [`layout_bipartite`](Graph::layout_bipartite)),
88//! [`Graph::feedback_arc_set`] (how [`layout_sugiyama`](Graph::layout_sugiyama)
89//! breaks cycles), [`Graph::nearest_neighbor_graph`] (a typical input of
90//! [`layout_umap`](Graph::layout_umap)).
91//! - Using a layout: [`Graph::spatial_edge_lengths`] measures the edges of a
92//! drawing, [`convex_hull_2d`](crate::misc::convex_hull_2d) its outline, and
93//! community detection (e.g. [`Graph::community_multilevel`]) gives vertex
94//! colors.
95
96use crate::{
97 constants::{LayoutGrid, NeighborMode},
98 error::{Error, Result},
99 ffi::*,
100 graph::{Graph, VertexId},
101 igraph_call,
102 list::MatrixList,
103 matrix::Matrix,
104 selector::VertexSelector,
105 vector::{Vector, VectorBool, VectorInt, View},
106};
107use std::{ffi::c_void, mem::MaybeUninit, ptr};
108
109// ---------------------------------------------------------------------------
110// Private helpers
111// ---------------------------------------------------------------------------
112
113/// Zero-copy view of an optional real slice.
114fn opt_view(v: Option<&[f64]>) -> Option<View<'_, Vector>> {
115 v.map(Vector::view)
116}
117
118/// Raw pointer to an optional view, `NULL` when absent.
119fn opt_ptr<V>(v: &Option<View<'_, V>>) -> *const V {
120 v.as_ref().map_or(ptr::null(), |v| v.as_ptr())
121}
122
123/// Checks that an optional per-item vector has the expected length.
124fn check_len<T>(name: &str, v: Option<&[T]>, expected: usize) -> Result<()> {
125 match v {
126 Some(v) if v.len() != expected => Err(Error::invalid(format!(
127 "`{name}` has length {}, expected {expected}",
128 v.len()
129 ))),
130 _ => Ok(()),
131 }
132}
133
134/// Builds the result matrix, copying the starting positions if given.
135///
136/// Returns `(matrix, use_seed)`.
137fn start_matrix(initial: Option<&Matrix>, n: usize, dim: usize) -> Result<(Matrix, bool)> {
138 match initial {
139 Some(m) if m.shape() != (n, dim) => Err(Error::invalid(format!(
140 "the initial layout is {}x{}, expected {n}x{dim}",
141 m.nrow(),
142 m.ncol()
143 ))),
144 Some(m) if !m.as_slice().iter().all(|x| x.is_finite()) => Err(Error::invalid(
145 "the initial layout contains NaN or infinite coordinates",
146 )),
147 Some(m) => Ok((m.clone(), true)),
148 None => Ok((Matrix::new(), false)),
149 }
150}
151
152/// Checks that an optional real parameter is finite and positive.
153fn check_positive(name: &str, x: f64) -> Result<()> {
154 if x.is_finite() && x > 0.0 {
155 Ok(())
156 } else {
157 Err(Error::invalid(format!(
158 "`{name}` must be finite and positive, got {x}"
159 )))
160 }
161}
162
163/// Largest number of grid cells [`Graph::layout_lgl`] accepts regardless of
164/// the graph size (the limit is `max(LGL_MAX_CELLS, 4 |V|)`; the default
165/// parameters give about `1.3 |V|` cells).
166const LGL_MAX_CELLS: i64 = 1 << 28;
167
168/// Largest vertex count (graph plus the extra `rootlevel` vertices) accepted
169/// by the Reingold–Tilford layouts.
170const RT_MAX_VERTICES: igraph_int_t = 1 << 40;
171
172/// Invalidates the property cache of a graph when dropped.
173struct CacheReset<'a>(Option<&'a Graph>);
174
175impl Drop for CacheReset<'_> {
176 fn drop(&mut self) {
177 if let Some(g) = self.0 {
178 g.invalidate_cache();
179 }
180 }
181}
182
183/// Largest edge weight accepted by [`Graph::layout_drl`]. DrL computes its
184/// energies in single precision: in igraph 1.0.1 weights around `1e37`
185/// already overflow them into NaN positions (converted to integers by its
186/// density grid, which is undefined behavior); `1e20` leaves a wide margin.
187const DRL_MAX_WEIGHT: f64 = 1e20;
188
189/// Converts a count to `igraph_int_t`, saturating at `igraph_int_t::MAX`
190/// (a plain `as` cast would turn huge counts into negative values, which
191/// igraph interprets differently, e.g. as "automatic").
192fn int(x: usize) -> igraph_int_t {
193 igraph_int_t::try_from(x).unwrap_or(igraph_int_t::MAX)
194}
195
196/// Vertex count as an `f64`, at least one (for defaults that must be positive).
197fn nf(g: &Graph) -> f64 {
198 g.vcount().max(1) as f64
199}
200
201/// Checks that all the ids are valid vertex ids.
202fn check_vertices(name: &str, ids: &[VertexId], n: usize) -> Result<()> {
203 match ids.iter().find(|&&v| v < 0 || v as usize >= n) {
204 Some(v) => Err(Error::new(
205 crate::error::ErrorKind::InvalidVertexId,
206 format!("`{name}` contains {v}, which is not a vertex id (the graph has {n} vertices)"),
207 )),
208 None => Ok(()),
209 }
210}
211
212/// Coordinate bounds of 2D/3D force-directed layouts, as raw views.
213struct Bounds<'a> {
214 views: [Option<View<'a, Vector>>; 6],
215}
216
217impl<'a> Bounds<'a> {
218 fn new(g: &Graph, bounds: [Option<&'a [f64]>; 6]) -> Result<Self> {
219 const NAMES: [&str; 6] = ["minx", "maxx", "miny", "maxy", "minz", "maxz"];
220 let n = g.vcount();
221 for (i, (name, b)) in NAMES.iter().zip(bounds.iter()).enumerate() {
222 check_len(name, *b, n)?;
223 // A lower bound of `-inf` or an upper bound of `+inf` means "no
224 // bound" and is fine. igraph requires the caller to rule out NaN,
225 // and its random initial placement fails on a lower bound of
226 // `+inf` or an upper bound of `-inf` with an error that the
227 // force-directed layouts ignore, going on with a wrongly sized
228 // matrix (heap corruption).
229 let forbidden = if i % 2 == 0 {
230 f64::INFINITY
231 } else {
232 f64::NEG_INFINITY
233 };
234 if b.is_some_and(|b| b.iter().any(|&x| x.is_nan() || x == forbidden)) {
235 return Err(Error::invalid(format!(
236 "`{name}` contains NaN or {forbidden}"
237 )));
238 }
239 }
240 Ok(Self {
241 views: bounds.map(opt_view),
242 })
243 }
244
245 fn ptr(&self, i: usize) -> *const Vector {
246 opt_ptr(&self.views[i])
247 }
248}
249
250// ---------------------------------------------------------------------------
251// Enums
252// ---------------------------------------------------------------------------
253
254crate::ffi_enum! {
255 /// Heuristic used by [`Graph::roots_for_tree_layout`] to choose among
256 /// several possible roots (`igraph_root_choice_t`).
257 pub enum RootChoice: igraph_root_choice_t {
258 /// Prefer the vertices with the highest degree (out- or in-degree in
259 /// directed mode). Fast even on large graphs.
260 Degree = igraph_root_choice_t_IGRAPH_ROOT_CHOICE_DEGREE,
261 /// Prefer the vertices with the lowest eccentricity: usually gives
262 /// "wide and shallow" trees, but takes quadratic time.
263 Eccentricity = igraph_root_choice_t_IGRAPH_ROOT_CHOICE_ECCENTRICITY,
264 }
265}
266
267crate::ffi_enum! {
268 /// Predefined parameter templates for the DrL layout
269 /// (`igraph_layout_drl_default_t`), see [`DrlOptions::from_template`].
270 pub enum DrlTemplate: igraph_layout_drl_default_t {
271 /// The default parameters.
272 Default = igraph_layout_drl_default_t_IGRAPH_LAYOUT_DRL_DEFAULT,
273 /// Slightly modified parameters giving a coarser layout.
274 Coarsen = igraph_layout_drl_default_t_IGRAPH_LAYOUT_DRL_COARSEN,
275 /// An even coarser layout.
276 Coarsest = igraph_layout_drl_default_t_IGRAPH_LAYOUT_DRL_COARSEST,
277 /// Refine an already computed layout.
278 Refine = igraph_layout_drl_default_t_IGRAPH_LAYOUT_DRL_REFINE,
279 /// Finalize an already refined layout.
280 Final = igraph_layout_drl_default_t_IGRAPH_LAYOUT_DRL_FINAL,
281 }
282}
283
284// ---------------------------------------------------------------------------
285// Options structs
286// ---------------------------------------------------------------------------
287
288/// Parameters of the Fruchterman–Reingold layouts
289/// ([`Graph::layout_fruchterman_reingold`] and
290/// [`Graph::layout_fruchterman_reingold_3d`]).
291///
292/// The [`Default`] follows the igraph documentation: 500 iterations, a start
293/// temperature of `sqrt(n) / 10`, automatic grid selection, unit weights, a
294/// random start and no coordinate bounds.
295///
296/// ```
297/// use igraph::layout::FruchtermanReingoldOptions;
298/// let weights = [1.0, 2.0, 3.0];
299/// let opts = FruchtermanReingoldOptions { niter: 1000, ..Default::default() }
300/// .with_weights(&weights);
301/// assert_eq!(opts.niter, 1000);
302/// ```
303#[derive(Debug, Clone, PartialEq)]
304pub struct FruchtermanReingoldOptions<'a> {
305 /// Number of iterations (default 500).
306 pub niter: usize,
307 /// Start temperature: the maximum movement of a vertex along one axis in
308 /// one step; it decreases linearly to zero. `None` means `sqrt(n) / 10`.
309 pub start_temp: Option<f64>,
310 /// Whether to use the faster, less accurate grid-based variant (2D only;
311 /// [`LayoutGrid::AutoGrid`], the default, uses it above 1000 vertices).
312 pub grid: LayoutGrid,
313 /// Positive edge weights multiplying the attraction along edges (higher
314 /// weight: closer vertices; an isolated edge of weight `w` has length
315 /// `w^(-1/3)`). `None` means unit weights.
316 pub weights: Option<&'a [f64]>,
317 /// Starting positions (`n` × 2, or `n` × 3 for the 3D version); `None`
318 /// starts from a random layout.
319 pub initial: Option<&'a Matrix>,
320 /// Per-vertex minimum `x` coordinate (`-inf` means no bound).
321 pub minx: Option<&'a [f64]>,
322 /// Per-vertex maximum `x` coordinate (`+inf` means no bound).
323 pub maxx: Option<&'a [f64]>,
324 /// Per-vertex minimum `y` coordinate (`-inf` means no bound).
325 pub miny: Option<&'a [f64]>,
326 /// Per-vertex maximum `y` coordinate (`+inf` means no bound).
327 pub maxy: Option<&'a [f64]>,
328 /// Per-vertex minimum `z` coordinate (3D only).
329 pub minz: Option<&'a [f64]>,
330 /// Per-vertex maximum `z` coordinate (3D only).
331 pub maxz: Option<&'a [f64]>,
332}
333
334impl Default for FruchtermanReingoldOptions<'_> {
335 fn default() -> Self {
336 Self {
337 niter: 500,
338 start_temp: None,
339 grid: LayoutGrid::AutoGrid,
340 weights: None,
341 initial: None,
342 minx: None,
343 maxx: None,
344 miny: None,
345 maxy: None,
346 minz: None,
347 maxz: None,
348 }
349 }
350}
351
352impl<'a> FruchtermanReingoldOptions<'a> {
353 /// Sets the edge weights.
354 pub fn with_weights(mut self, weights: &'a [f64]) -> Self {
355 self.weights = Some(weights);
356 self
357 }
358
359 /// Sets the starting positions.
360 pub fn with_initial(mut self, initial: &'a Matrix) -> Self {
361 self.initial = Some(initial);
362 self
363 }
364
365 /// Sets the number of iterations.
366 pub fn with_niter(mut self, niter: usize) -> Self {
367 self.niter = niter;
368 self
369 }
370}
371
372/// Parameters of the Kamada–Kawai layouts ([`Graph::layout_kamada_kawai`] and
373/// [`Graph::layout_kamada_kawai_3d`]).
374///
375/// The [`Default`] uses `50 n` iterations (the C documentation asks for at
376/// least `10 n`; `50 n` is the default of the R and Python interfaces),
377/// `epsilon = 0` (always run all the iterations), `kkconst = n` (as the C
378/// documentation recommends), unit edge lengths, a circular (spherical in
379/// 3D) start and no bounds.
380#[derive(Debug, Clone, PartialEq, Default)]
381pub struct KamadaKawaiOptions<'a> {
382 /// Maximum number of iterations; `None` means `50 n`.
383 pub maxiter: Option<usize>,
384 /// Stop when the maximum energy change falls below this value (`0.0`,
385 /// the default, performs all `maxiter` iterations).
386 pub epsilon: f64,
387 /// The vertex attraction constant; `None` means the number of vertices.
388 pub kkconst: Option<f64>,
389 /// Positive edge *lengths* used in the shortest path computation
390 /// (higher weight: farther vertices). `None` means unit lengths.
391 pub weights: Option<&'a [f64]>,
392 /// Starting positions (`n` × 2, or `n` × 3 for the 3D version). `None`
393 /// starts from a circle (sphere) of radius `0.36 sqrt(n)`, or from a
394 /// random layout when some bounds are given.
395 pub initial: Option<&'a Matrix>,
396 /// Per-vertex minimum `x` coordinate (`-inf` means no bound).
397 pub minx: Option<&'a [f64]>,
398 /// Per-vertex maximum `x` coordinate (`+inf` means no bound).
399 pub maxx: Option<&'a [f64]>,
400 /// Per-vertex minimum `y` coordinate (`-inf` means no bound).
401 pub miny: Option<&'a [f64]>,
402 /// Per-vertex maximum `y` coordinate (`+inf` means no bound).
403 pub maxy: Option<&'a [f64]>,
404 /// Per-vertex minimum `z` coordinate (3D only).
405 pub minz: Option<&'a [f64]>,
406 /// Per-vertex maximum `z` coordinate (3D only).
407 pub maxz: Option<&'a [f64]>,
408}
409
410impl<'a> KamadaKawaiOptions<'a> {
411 /// Sets the edge lengths.
412 pub fn with_weights(mut self, weights: &'a [f64]) -> Self {
413 self.weights = Some(weights);
414 self
415 }
416
417 /// Sets the starting positions.
418 pub fn with_initial(mut self, initial: &'a Matrix) -> Self {
419 self.initial = Some(initial);
420 self
421 }
422}
423
424/// Parameters of the Large Graph Layout ([`Graph::layout_lgl`]).
425///
426/// The [`Default`] follows the igraph documentation (`n` is the number of
427/// vertices): 150 iterations, `maxdelta = n`, `area = n²`, `coolexp = 1.5`,
428/// `repulserad = area · n`, `cellsize = area^(1/4)` and a random root.
429#[derive(Debug, Clone, PartialEq)]
430pub struct LglOptions {
431 /// Maximum number of cooling iterations per layout step (default 150).
432 pub maxiter: usize,
433 /// Maximum movement of a vertex in one iteration; `None` means `n`.
434 pub maxdelta: Option<f64>,
435 /// Area of the square the vertices are placed on; `None` means `n²`.
436 pub area: Option<f64>,
437 /// The cooling exponent (default 1.5).
438 pub coolexp: f64,
439 /// Radius at which repulsion cancels attraction; `None` means `area · n`.
440 pub repulserad: Option<f64>,
441 /// Side of the grid cells; `None` means the fourth root of `area`.
442 pub cellsize: Option<f64>,
443 /// Root vertex, placed first; `None` picks a random vertex.
444 pub root: Option<VertexId>,
445}
446
447impl Default for LglOptions {
448 fn default() -> Self {
449 Self {
450 maxiter: 150,
451 maxdelta: None,
452 area: None,
453 coolexp: 1.5,
454 repulserad: None,
455 cellsize: None,
456 root: None,
457 }
458 }
459}
460
461/// Parameters of the Sugiyama layered layout ([`Graph::layout_sugiyama`]).
462///
463/// The [`Default`] uses automatic layering, unit gaps, 100 crossing
464/// minimization iterations and unit weights.
465#[derive(Debug, Clone, PartialEq)]
466pub struct SugiyamaOptions<'a> {
467 /// Non-negative layer index of every vertex. Empty layers are skipped
468 /// when minimizing crossings, but keep their room on the `y` axis.
469 /// `None` lets igraph compute a layering, after breaking cycles.
470 pub layers: Option<&'a [i64]>,
471 /// Preferred minimum horizontal gap between vertices of a layer (default 1).
472 pub hgap: f64,
473 /// Distance between consecutive layers (default 1).
474 pub vgap: f64,
475 /// Maximum number of iterations of the crossing minimization (default 100).
476 pub maxiter: usize,
477 /// Edge weights, used only to break cycles: igraph tends to reverse
478 /// edges with smaller weights.
479 pub weights: Option<&'a [f64]>,
480}
481
482impl Default for SugiyamaOptions<'_> {
483 fn default() -> Self {
484 Self {
485 layers: None,
486 hgap: 1.0,
487 vgap: 1.0,
488 maxiter: 100,
489 weights: None,
490 }
491 }
492}
493
494impl<'a> SugiyamaOptions<'a> {
495 /// Sets the layer of each vertex.
496 pub fn with_layers(mut self, layers: &'a [i64]) -> Self {
497 self.layers = Some(layers);
498 self
499 }
500}
501
502/// Result of [`Graph::layout_sugiyama`].
503#[derive(Debug, Clone, PartialEq)]
504pub struct SugiyamaLayout {
505 /// The vertex coordinates, `n` × 2; the `y` coordinate of a vertex is
506 /// its layer index times `vgap`.
507 pub coords: Matrix,
508 /// One matrix per edge with the extra *control points* (one per row)
509 /// the edge must pass through, from its source to its target; edges
510 /// spanning adjacent layers have a 0 × 2 matrix.
511 pub routing: Vec<Matrix>,
512}
513
514/// Parameters of the graphopt layout ([`Graph::layout_graphopt`]).
515///
516/// The [`Default`] uses the original graphopt defaults: 500 iterations,
517/// node charge 0.001, node mass 30, spring length 0, spring constant 1,
518/// maximum movement 5, and a random start.
519#[derive(Debug, Clone, PartialEq)]
520pub struct GraphoptOptions<'a> {
521 /// Number of iterations (default 500).
522 pub niter: usize,
523 /// Charge of the vertices, for the electric repulsion (default 0.001).
524 /// With zero charge each iteration is only `O(|E|)`.
525 pub node_charge: f64,
526 /// Mass of the vertices, for the spring forces (default 30).
527 pub node_mass: f64,
528 /// Rest length of the springs (default 0).
529 pub spring_length: f64,
530 /// Spring constant (default 1).
531 pub spring_constant: f64,
532 /// Maximum movement along an axis in a single step (default 5).
533 pub max_sa_movement: f64,
534 /// Starting positions (`n` × 2); `None` starts from a random layout.
535 pub initial: Option<&'a Matrix>,
536}
537
538impl Default for GraphoptOptions<'_> {
539 fn default() -> Self {
540 Self {
541 niter: 500,
542 node_charge: 0.001,
543 node_mass: 30.0,
544 spring_length: 0.0,
545 spring_constant: 1.0,
546 max_sa_movement: 5.0,
547 initial: None,
548 }
549 }
550}
551
552/// Parameters of the UMAP layouts ([`Graph::layout_umap`] and
553/// [`Graph::layout_umap_3d`]).
554///
555/// The [`Default`] uses `min_dist = 0.01`, 500 epochs, unit distances and a
556/// random start.
557#[derive(Debug, Clone, PartialEq)]
558pub struct UmapOptions<'a> {
559 /// Distance (or, with `distances_are_weights`, weight) of every edge;
560 /// `None` means all edges have the same distance.
561 pub distances: Option<&'a [f64]>,
562 /// How close two unconnected vertices may get before repelling each
563 /// other; non-negative, typically in `[0, 1]` (default 0.01).
564 pub min_dist: f64,
565 /// Number of epochs of stochastic gradient descent, typically in
566 /// `[30, 500]` (default 500).
567 pub epochs: usize,
568 /// Whether `distances` already holds UMAP weights (e.g. from
569 /// [`Graph::layout_umap_compute_weights`]).
570 pub distances_are_weights: bool,
571 /// Starting positions (`n` × 2, or `n` × 3 for the 3D version); `None`
572 /// starts from a random layout.
573 pub initial: Option<&'a Matrix>,
574}
575
576impl Default for UmapOptions<'_> {
577 fn default() -> Self {
578 Self {
579 distances: None,
580 min_dist: 0.01,
581 epochs: 500,
582 distances_are_weights: false,
583 initial: None,
584 }
585 }
586}
587
588/// Parameters of the GEM layout ([`Graph::layout_gem`]).
589///
590/// The [`Default`] follows the igraph documentation (`n` is the number of
591/// vertices): `40 n²` iterations, `temp_max = n`, `temp_min = 0.1`,
592/// `temp_init = sqrt(n)` and a random start.
593#[derive(Debug, Clone, PartialEq)]
594pub struct GemOptions<'a> {
595 /// Maximum number of iterations (a single vertex update counts as one);
596 /// `None` means `40 n²`.
597 pub maxiter: Option<usize>,
598 /// Maximum local temperature; `None` means `n`.
599 pub temp_max: Option<f64>,
600 /// Global temperature at which the algorithm stops (default 0.1).
601 pub temp_min: f64,
602 /// Initial local temperature; `None` means `sqrt(n)`.
603 pub temp_init: Option<f64>,
604 /// Starting positions (`n` × 2); `None` starts from a random layout.
605 pub initial: Option<&'a Matrix>,
606}
607
608impl Default for GemOptions<'_> {
609 fn default() -> Self {
610 Self {
611 maxiter: None,
612 temp_max: None,
613 temp_min: 0.1,
614 temp_init: None,
615 initial: None,
616 }
617 }
618}
619
620/// Parameters of the Davidson–Harel layout ([`Graph::layout_davidson_harel`]).
621///
622/// The [`Default`] follows the igraph documentation, where `d` is the density
623/// of the graph (edge directions ignored): 10 annealing iterations,
624/// `max(10, log2 n)` fine tuning iterations, cooling factor 0.75, node
625/// distance weight 1, border weight 0, edge length weight `d / 10`, edge
626/// crossing weight `1 - sqrt(d)`, node-edge distance weight `(1 - d) / 5`.
627#[derive(Debug, Clone, PartialEq)]
628pub struct DavidsonHarelOptions<'a> {
629 /// Number of annealing iterations (default 10).
630 pub maxiter: usize,
631 /// Number of fine tuning iterations; `None` means `max(10, log2 n)`.
632 pub fineiter: Option<usize>,
633 /// Cooling factor, in `(0, 1)` (default 0.75).
634 pub cool_fact: f64,
635 /// Weight of the node-node distance energy term (default 1).
636 pub weight_node_dist: f64,
637 /// Weight of the distance-from-border term (default 0: vertices may sit
638 /// on the border).
639 pub weight_border: f64,
640 /// Weight of the edge length term; `None` means density / 10.
641 pub weight_edge_lengths: Option<f64>,
642 /// Weight of the edge crossing term; `None` means `1 - sqrt(density)`.
643 pub weight_edge_crossings: Option<f64>,
644 /// Weight of the node-edge distance term; `None` means `(1 - density) / 5`.
645 pub weight_node_edge_dist: Option<f64>,
646 /// Starting positions (`n` × 2); `None` starts from a random layout.
647 pub initial: Option<&'a Matrix>,
648}
649
650impl Default for DavidsonHarelOptions<'_> {
651 fn default() -> Self {
652 Self {
653 maxiter: 10,
654 fineiter: None,
655 cool_fact: 0.75,
656 weight_node_dist: 1.0,
657 weight_border: 0.0,
658 weight_edge_lengths: None,
659 weight_edge_crossings: None,
660 weight_node_edge_dist: None,
661 initial: None,
662 }
663 }
664}
665
666/// Parameters of the DrL layout (`igraph_layout_drl_options_t`), used by
667/// [`Graph::layout_drl`] and [`Graph::layout_drl_3d`].
668///
669/// This is the C struct itself: start from a [`DrlTemplate`] with
670/// [`DrlOptions::from_template`] (or [`Default`], which is
671/// [`DrlTemplate::Default`]) and adjust the public fields. The algorithm runs
672/// through six phases (*init*, *liquid*, *expansion*, *cooldown*, *crunch*,
673/// *simmer*), each with its number of iterations, start temperature,
674/// attraction and (non-negative) damping multiplier; `edge_cut` in `[0, 1]`
675/// controls how aggressively stressed edges are cut in the late phases
676/// (default 32/40).
677///
678/// ```
679/// use igraph::layout::{DrlOptions, DrlTemplate};
680/// let mut opts = DrlOptions::from_template(DrlTemplate::Coarsest);
681/// assert_eq!(opts.crunch_iterations, 200);
682/// opts.edge_cut = 0.0; // no edge cutting
683/// assert_eq!(DrlOptions::default().liquid_iterations, 200);
684/// ```
685pub type DrlOptions = igraph_layout_drl_options_t;
686
687impl igraph_layout_drl_options_t {
688 /// Parameters initialized from a predefined template
689 /// ([`igraph_layout_drl_options_init`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_drl_options_init)).
690 pub fn from_template(template: DrlTemplate) -> Self {
691 let mut raw = MaybeUninit::<Self>::zeroed();
692 // Cannot fail: every template is valid.
693 igraph_call!(igraph_layout_drl_options_init(
694 raw.as_mut_ptr(),
695 template.into()
696 ))
697 .expect("igraph_layout_drl_options_init failed on a valid template");
698 unsafe { raw.assume_init() }
699 }
700}
701
702impl Default for igraph_layout_drl_options_t {
703 fn default() -> Self {
704 Self::from_template(DrlTemplate::Default)
705 }
706}
707
708// A plain-old-data struct of numbers: copying it is trivially safe.
709impl Copy for igraph_layout_drl_options_t {}
710
711impl Clone for igraph_layout_drl_options_t {
712 fn clone(&self) -> Self {
713 *self
714 }
715}
716
717// ---------------------------------------------------------------------------
718// Layouts
719// ---------------------------------------------------------------------------
720
721impl igraph_t {
722 /// Places the vertices uniformly at random in the square `[-1, 1]²`.
723 ///
724 /// Uses the calling thread's default random number generator (see
725 /// [`rng::seed`](crate::rng::seed)). Time complexity: `O(|V|)`.
726 /// Binds [`igraph_layout_random`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_random).
727 ///
728 /// # Examples
729 ///
730 /// ```
731 /// use igraph::prelude::*;
732 /// let g = Graph::new(10, false);
733 /// rng::seed(1).unwrap();
734 /// let l = g.layout_random().unwrap();
735 /// assert_eq!(l.shape(), (10, 2));
736 /// assert!(l.as_slice().iter().all(|c| (-1.0..=1.0).contains(c)));
737 /// rng::seed(1).unwrap();
738 /// assert_eq!(g.layout_random().unwrap(), l);
739 /// ```
740 pub fn layout_random(&self) -> Result<Matrix> {
741 let mut res = Matrix::new();
742 igraph_call!(igraph_layout_random(self, &mut res))?;
743 Ok(res)
744 }
745
746 /// Places the vertices uniformly at random in the cube `[-1, 1]³`.
747 ///
748 /// The 3D version of [`layout_random`](Graph::layout_random). Time
749 /// complexity: `O(|V|)`.
750 /// Binds [`igraph_layout_random_3d`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_random_3d).
751 pub fn layout_random_3d(&self) -> Result<Matrix> {
752 let mut res = Matrix::new();
753 igraph_call!(igraph_layout_random_3d(self, &mut res))?;
754 Ok(res)
755 }
756
757 /// Places the vertices uniformly on the unit circle, in the given order.
758 ///
759 /// The `k`-th vertex of `order` (out of `m` selected vertices) is placed
760 /// at angle `2πk/m`; vertices not in `order` are placed at the origin.
761 /// Pass `..` to place all vertices in increasing id order. This is the
762 /// natural drawing of [`Graph::ring`]. Time complexity: `O(|V|)`.
763 /// Binds [`igraph_layout_circle`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_circle).
764 ///
765 /// # Errors
766 /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) if
767 /// `order` contains invalid vertices.
768 ///
769 /// # Examples
770 ///
771 /// ```
772 /// use igraph::prelude::*;
773 /// let g = Graph::new(4, false);
774 /// let l = g.layout_circle(..).unwrap();
775 /// assert!((l[(1, 0)] - 0.0).abs() < 1e-12 && (l[(1, 1)] - 1.0).abs() < 1e-12);
776 /// // Only two vertices on the circle, the others at the origin.
777 /// let l = g.layout_circle(&[3, 1]).unwrap();
778 /// assert_eq!(l.row(3), vec![1.0, 0.0]);
779 /// assert_eq!(l.row(0), vec![0.0, 0.0]);
780 /// ```
781 pub fn layout_circle<'a>(&self, order: impl Into<VertexSelector<'a>>) -> Result<Matrix> {
782 let vs = order.into().to_raw()?;
783 let mut res = Matrix::new();
784 igraph_call!(igraph_layout_circle(self, &mut res, vs.get()))?;
785 Ok(res)
786 }
787
788 /// Star-like layout: `center` at the origin, the other vertices evenly
789 /// spaced on the unit circle.
790 ///
791 /// The edges are ignored. The non-center vertices are placed in the
792 /// order given by `order`, which must be a permutation of *all* the
793 /// vertices (including the center), or in increasing id order if `None`;
794 /// the first one is at angle zero. `center` is ignored for the null
795 /// graph. Time complexity: `O(|V|)`. This is the natural drawing of
796 /// [`Graph::star`].
797 /// Binds [`igraph_layout_star`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_star).
798 ///
799 /// # Errors
800 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if
801 /// `center` is not a vertex, or `order` is not a permutation of the
802 /// vertices. (igraph itself only checks the length and the range of
803 /// `order`: with a repeated vertex it would leave the rows of the missing
804 /// vertices uninitialized, so this wrapper rejects such orders.)
805 ///
806 /// # Examples
807 ///
808 /// ```
809 /// use igraph::prelude::*;
810 /// let g = Graph::star(5, StarMode::Undirected, 2).unwrap();
811 /// let l = g.layout_star(2, None).unwrap();
812 /// assert_eq!(l.row(2), vec![0.0, 0.0]);
813 /// assert_eq!(l.row(0), vec![1.0, 0.0]);
814 /// ```
815 pub fn layout_star(&self, center: VertexId, order: Option<&[VertexId]>) -> Result<Matrix> {
816 let n = self.vcount();
817 check_len("order", order, n)?;
818 if let Some(order) = order {
819 // igraph does not initialize the rows of vertices missing from
820 // `order`: insist on a permutation so no garbage is ever read.
821 let mut seen = vec![false; n];
822 for &v in order {
823 match usize::try_from(v).ok().and_then(|i| seen.get_mut(i)) {
824 Some(s) if !*s => *s = true,
825 _ => {
826 return Err(Error::invalid(format!(
827 "`order` must be a permutation of the {n} vertices, \
828 but {v} is out of range or repeated"
829 )));
830 }
831 }
832 }
833 }
834 let order = order.map(VectorInt::view);
835 let mut res = Matrix::new();
836 igraph_call!(igraph_layout_star(self, &mut res, center, opt_ptr(&order)))?;
837 Ok(res)
838 }
839
840 /// Places the vertices on a regular 2D grid, row by row.
841 ///
842 /// Vertex `i` gets coordinates `(i mod w, i div w)`, where the width `w`
843 /// is `width`, or `ceil(sqrt(n))` if `None` (`Some(0)` is the same as
844 /// `None`). With `width` equal to the first dimension, this draws the
845 /// vertices of a [`Graph::square_lattice`] at their lattice points. Time
846 /// complexity: `O(|V|)`.
847 /// Binds [`igraph_layout_grid`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_grid).
848 ///
849 /// # Examples
850 ///
851 /// ```
852 /// use igraph::prelude::*;
853 /// let g = Graph::new(5, false);
854 /// let l = g.layout_grid(Some(2)).unwrap();
855 /// assert_eq!(l.to_rows(), vec![
856 /// vec![0.0, 0.0], vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0], vec![0.0, 2.0],
857 /// ]);
858 /// ```
859 pub fn layout_grid(&self, width: Option<usize>) -> Result<Matrix> {
860 let mut res = Matrix::new();
861 let width = width.map_or(0, int);
862 igraph_call!(igraph_layout_grid(self, &mut res, width))?;
863 Ok(res)
864 }
865
866 /// Places the vertices on a regular 3D grid, filling rows (along `x`),
867 /// then layers (along `y`), then stacking layers along `z`.
868 ///
869 /// `width` is the number of vertices in a row and `height` the number of
870 /// rows in a layer; if both are `None` they are `ceil(cbrt(n))`, if one is
871 /// `None` it is chosen so that layers are roughly square (`Some(0)` is
872 /// the same as `None`). Time complexity: `O(|V|)`.
873 /// Binds [`igraph_layout_grid_3d`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_grid_3d).
874 ///
875 /// # Examples
876 ///
877 /// ```
878 /// use igraph::prelude::*;
879 /// // 8 vertices: a 2 x 2 x 2 cube.
880 /// let l = Graph::new(8, false).layout_grid_3d(None, None).unwrap();
881 /// assert_eq!(l.row(0), vec![0.0, 0.0, 0.0]);
882 /// assert_eq!(l.row(3), vec![1.0, 1.0, 0.0]);
883 /// assert_eq!(l.row(7), vec![1.0, 1.0, 1.0]);
884 /// ```
885 pub fn layout_grid_3d(&self, width: Option<usize>, height: Option<usize>) -> Result<Matrix> {
886 let mut res = Matrix::new();
887 let width = width.map_or(0, int);
888 let height = height.map_or(0, int);
889 igraph_call!(igraph_layout_grid_3d(self, &mut res, width, height))?;
890 Ok(res)
891 }
892
893 /// Places the vertices (more or less) uniformly on the unit sphere.
894 ///
895 /// Vertices are placed along a spiral wrapped around the sphere, in
896 /// increasing id order, so consecutive ids end up close to each other
897 /// (Saff & Kuijlaars, 1997). Time complexity: `O(|V|)`.
898 /// Binds [`igraph_layout_sphere`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_sphere).
899 ///
900 /// # Examples
901 ///
902 /// ```
903 /// use igraph::prelude::*;
904 /// let l = Graph::new(20, false).layout_sphere().unwrap();
905 /// for p in l.rows() {
906 /// let r = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
907 /// assert!((r - 1.0).abs() < 1e-9);
908 /// }
909 /// ```
910 pub fn layout_sphere(&self) -> Result<Matrix> {
911 let mut res = Matrix::new();
912 igraph_call!(igraph_layout_sphere(self, &mut res))?;
913 Ok(res)
914 }
915
916 /// Force-directed layout in the plane with the Fruchterman–Reingold algorithm.
917 ///
918 /// It simulates an attractive force `f_a(d) = -w d²` between connected
919 /// vertices (`w` is the edge weight) and a repulsive force `f_r(d) = 1/d`
920 /// between all pairs, so the equilibrium length of an isolated edge is
921 /// `w^(-1/3)` (the C documentation of igraph 1.0.0 and 1.0.1 says `1/w^3`,
922 /// a typo). In
923 /// disconnected graphs a weak attraction of weight `n^(-3/2)` between all
924 /// pairs keeps the components close. The movement is limited by a
925 /// temperature decreasing linearly to zero, and per-vertex coordinate
926 /// bounds may be given. Uses the calling thread's default random number
927 /// generator for the random start. See [`FruchtermanReingoldOptions`] for
928 /// all the parameters. Time complexity: `O(|V|²)` per iteration (less with the
929 /// grid variant).
930 /// Binds [`igraph_layout_fruchterman_reingold`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_fruchterman_reingold).
931 ///
932 /// # Errors
933 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for
934 /// non-positive weights, vectors of the wrong length, inconsistent bounds,
935 /// NaN bounds, a lower bound of `+inf` or an upper bound of `-inf` (an
936 /// infinite bound in the other direction means "no bound"), or a start matrix of the wrong shape or with NaN or infinite
937 /// coordinates.
938 ///
939 /// # Examples
940 ///
941 /// ```
942 /// use igraph::prelude::*;
943 /// use igraph::layout::FruchtermanReingoldOptions;
944 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (2, 3)], 4, false).unwrap();
945 /// rng::seed(7).unwrap();
946 /// // Keep every vertex in the right half-plane.
947 /// let minx = [0.0; 4];
948 /// let opts = FruchtermanReingoldOptions { minx: Some(&minx), ..Default::default() };
949 /// let l = g.layout_fruchterman_reingold(&opts).unwrap();
950 /// assert!(l.column(0).iter().all(|&x| x >= 0.0));
951 /// ```
952 pub fn layout_fruchterman_reingold(
953 &self,
954 options: &FruchtermanReingoldOptions<'_>,
955 ) -> Result<Matrix> {
956 let n = self.vcount();
957 check_len("weights", options.weights, self.ecount())?;
958 let bounds = Bounds::new(
959 self,
960 [
961 options.minx,
962 options.maxx,
963 options.miny,
964 options.maxy,
965 None,
966 None,
967 ],
968 )?;
969 let (mut res, use_seed) = start_matrix(options.initial, n, 2)?;
970 let weights = opt_view(options.weights);
971 let start_temp = options
972 .start_temp
973 .unwrap_or_else(|| (n as f64).sqrt() / 10.0);
974 igraph_call!(igraph_layout_fruchterman_reingold(
975 self,
976 &mut res,
977 use_seed,
978 int(options.niter),
979 start_temp,
980 options.grid.into(),
981 opt_ptr(&weights),
982 bounds.ptr(0),
983 bounds.ptr(1),
984 bounds.ptr(2),
985 bounds.ptr(3)
986 ))?;
987 Ok(res)
988 }
989
990 /// Force-directed layout in 3D space with the Fruchterman–Reingold algorithm.
991 ///
992 /// The 3D version of [`layout_fruchterman_reingold`](Graph::layout_fruchterman_reingold)
993 /// (the `grid` option is ignored, the `minz`/`maxz` bounds are used).
994 ///
995 /// Caveat (igraph 1.0.0 and 1.0.1): on *disconnected* graphs the C code
996 /// adds the `z` component of the repulsion acting on one vertex of each
997 /// pair to its `y` displacement instead of its `z` displacement, so the
998 /// forces are slightly unbalanced; connected graphs are not affected.
999 /// Binds [`igraph_layout_fruchterman_reingold_3d`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_fruchterman_reingold_3d).
1000 ///
1001 /// # Errors
1002 /// As for the 2D version.
1003 pub fn layout_fruchterman_reingold_3d(
1004 &self,
1005 options: &FruchtermanReingoldOptions<'_>,
1006 ) -> Result<Matrix> {
1007 let n = self.vcount();
1008 check_len("weights", options.weights, self.ecount())?;
1009 let bounds = Bounds::new(
1010 self,
1011 [
1012 options.minx,
1013 options.maxx,
1014 options.miny,
1015 options.maxy,
1016 options.minz,
1017 options.maxz,
1018 ],
1019 )?;
1020 let (mut res, use_seed) = start_matrix(options.initial, n, 3)?;
1021 let weights = opt_view(options.weights);
1022 let start_temp = options
1023 .start_temp
1024 .unwrap_or_else(|| (n as f64).sqrt() / 10.0);
1025 igraph_call!(igraph_layout_fruchterman_reingold_3d(
1026 self,
1027 &mut res,
1028 use_seed,
1029 int(options.niter),
1030 start_temp,
1031 opt_ptr(&weights),
1032 bounds.ptr(0),
1033 bounds.ptr(1),
1034 bounds.ptr(2),
1035 bounds.ptr(3),
1036 bounds.ptr(4),
1037 bounds.ptr(5)
1038 ))?;
1039 Ok(res)
1040 }
1041
1042 /// Force-directed layout in the plane with the Kamada–Kawai spring algorithm.
1043 ///
1044 /// A spring is placed between *every* pair of vertices `u`, `v`, whose
1045 /// rest length is proportional to their (undirected, possibly weighted)
1046 /// graph distance `d(u, v)`, namely `sqrt(n) · d(u, v) / D` where `D` is
1047 /// the largest finite distance (so the drawing has a size of about
1048 /// `sqrt(n)`), and whose stiffness `kkconst / d(u, v)²` decreases with
1049 /// that distance; the energy is then minimized one vertex at a time.
1050 /// Vertices in different components are treated as being at distance
1051 /// `D`. It works particularly well for lattice-like, locally connected
1052 /// graphs; memory is `O(|V|²)`, so it is not suitable for large graphs.
1053 /// Without a start layout (and with no bounds) it starts from a circle of
1054 /// radius `0.36 sqrt(n)`, so the result is deterministic.
1055 /// The target distances are the ones [`Graph::distances`] would compute
1056 /// with `NeighborMode::All`. See [`KamadaKawaiOptions`]. Time complexity:
1057 /// `O(|V|)` per iteration after an `O(|V|² log |V|)` initialization.
1058 /// Binds [`igraph_layout_kamada_kawai`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_kamada_kawai).
1059 ///
1060 /// # Errors
1061 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for
1062 /// non-positive weights or `kkconst`, wrong lengths or shapes, NaN bounds,
1063 /// a lower bound of `+inf` or an upper bound of `-inf`, or NaN or infinite
1064 /// start coordinates.
1065 ///
1066 /// # Examples
1067 ///
1068 /// ```
1069 /// use igraph::prelude::*;
1070 /// use igraph::layout::KamadaKawaiOptions;
1071 /// // A path 0 - 1 - 2: drawn straight, with edges of length sqrt(3) / 2.
1072 /// let g = Graph::ring(3, false, false, false).unwrap();
1073 /// let l = g.layout_kamada_kawai(&KamadaKawaiOptions::default()).unwrap();
1074 /// let d = |i: usize, j: usize| (l[(i, 0)] - l[(j, 0)]).hypot(l[(i, 1)] - l[(j, 1)]);
1075 /// assert!((d(0, 2) - d(0, 1) - d(1, 2)).abs() < 1e-3);
1076 /// assert!((d(0, 1) - 3f64.sqrt() / 2.0).abs() < 1e-3);
1077 /// ```
1078 pub fn layout_kamada_kawai(&self, options: &KamadaKawaiOptions<'_>) -> Result<Matrix> {
1079 self.kamada_kawai_impl(options, false)
1080 }
1081
1082 /// Force-directed layout in 3D space with the Kamada–Kawai spring algorithm.
1083 ///
1084 /// The 3D version of [`layout_kamada_kawai`](Graph::layout_kamada_kawai);
1085 /// without a start layout (and with no bounds) it starts from a
1086 /// [`layout_sphere`](Graph::layout_sphere) of radius `0.36 sqrt(n)`.
1087 /// Binds [`igraph_layout_kamada_kawai_3d`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_kamada_kawai_3d).
1088 ///
1089 /// # Errors
1090 /// As for the 2D version.
1091 pub fn layout_kamada_kawai_3d(&self, options: &KamadaKawaiOptions<'_>) -> Result<Matrix> {
1092 self.kamada_kawai_impl(options, true)
1093 }
1094
1095 fn kamada_kawai_impl(&self, options: &KamadaKawaiOptions<'_>, three_d: bool) -> Result<Matrix> {
1096 let n = self.vcount();
1097 check_len("weights", options.weights, self.ecount())?;
1098 let (minz, maxz) = if three_d {
1099 (options.minz, options.maxz)
1100 } else {
1101 (None, None)
1102 };
1103 let bounds = Bounds::new(
1104 self,
1105 [
1106 options.minx,
1107 options.maxx,
1108 options.miny,
1109 options.maxy,
1110 minz,
1111 maxz,
1112 ],
1113 )?;
1114 let (mut res, use_seed) = start_matrix(options.initial, n, if three_d { 3 } else { 2 })?;
1115 let weights = opt_view(options.weights);
1116 let maxiter = int(options.maxiter.unwrap_or(n.saturating_mul(50)));
1117 let kkconst = options.kkconst.unwrap_or_else(|| nf(self));
1118 if three_d {
1119 igraph_call!(igraph_layout_kamada_kawai_3d(
1120 self,
1121 &mut res,
1122 use_seed,
1123 maxiter,
1124 options.epsilon,
1125 kkconst,
1126 opt_ptr(&weights),
1127 bounds.ptr(0),
1128 bounds.ptr(1),
1129 bounds.ptr(2),
1130 bounds.ptr(3),
1131 bounds.ptr(4),
1132 bounds.ptr(5)
1133 ))?;
1134 } else {
1135 igraph_call!(igraph_layout_kamada_kawai(
1136 self,
1137 &mut res,
1138 use_seed,
1139 maxiter,
1140 options.epsilon,
1141 kkconst,
1142 opt_ptr(&weights),
1143 bounds.ptr(0),
1144 bounds.ptr(1),
1145 bounds.ptr(2),
1146 bounds.ptr(3)
1147 ))?;
1148 }
1149 Ok(res)
1150 }
1151
1152 /// Force-directed layout for large (connected) graphs, inspired by the
1153 /// Large Graph Layout program.
1154 ///
1155 /// The root is placed first, then its neighbors, then the second
1156 /// neighbors and so on (following a BFS); after each layer a
1157 /// Fruchterman–Reingold-style simulated annealing runs, computing
1158 /// repulsion only between vertices in nearby grid cells. The graph must
1159 /// be connected (igraph warns otherwise; lay out the pieces from
1160 /// [`Graph::decompose`] separately and merge them with
1161 /// [`layout_merge_dla`] instead). See [`LglOptions`]. Time
1162 /// complexity: ideally `O(dia · maxiter · (|V| + |E|))`.
1163 /// Binds [`igraph_layout_lgl`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_lgl).
1164 ///
1165 /// # Errors
1166 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if a
1167 /// parameter is not finite and positive (`repulserad` may be `+inf`), or if `cellsize` is so small
1168 /// compared to `area` that the grid would have more than
1169 /// `max(2^28, 4 |V|)` cells;
1170 /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) for
1171 /// an invalid root.
1172 ///
1173 /// # Examples
1174 ///
1175 /// ```
1176 /// use igraph::prelude::*;
1177 /// use igraph::layout::LglOptions;
1178 /// let g = Graph::kary_tree(40, 3, TreeMode::Undirected).unwrap();
1179 /// rng::seed(5).unwrap();
1180 /// let l = g.layout_lgl(&LglOptions { root: Some(0), ..Default::default() }).unwrap();
1181 /// assert_eq!(l.shape(), (40, 2));
1182 /// assert!(l.as_slice().iter().all(|x| x.is_finite()));
1183 /// ```
1184 pub fn layout_lgl(&self, options: &LglOptions) -> Result<Matrix> {
1185 let n = self.vcount();
1186 let nn = nf(self);
1187 if let Some(root) = options.root {
1188 check_vertices("root", &[root], n)?;
1189 }
1190 let area = options.area.unwrap_or(nn * nn);
1191 let maxdelta = options.maxdelta.unwrap_or(nn);
1192 let repulserad = options.repulserad.unwrap_or(area * nn);
1193 let cellsize = options.cellsize.unwrap_or_else(|| area.sqrt().sqrt());
1194 if n > 0 {
1195 // igraph only rejects values `<= 0`: NaN and infinite values
1196 // reach its grid code, which aborts the process.
1197 check_positive("area", area)?;
1198 check_positive("maxdelta", maxdelta)?;
1199 check_positive("coolexp", options.coolexp)?;
1200 // An infinite cutoff radius only drops the `dist² / repulserad`
1201 // term from the repulsive force, which is harmless.
1202 if repulserad.is_nan() || repulserad <= 0.0 {
1203 return Err(Error::invalid(format!(
1204 "`repulserad` must be positive, got {repulserad}"
1205 )));
1206 }
1207 check_positive("cellsize", cellsize)?;
1208 // igraph covers the disc of the given area with a square grid of
1209 // `steps × steps` cells (a C integer), which must not overflow
1210 // nor be absurdly large.
1211 let steps = (2.0 * (area / std::f64::consts::PI).sqrt() / cellsize).ceil();
1212 let cells = steps * steps;
1213 let limit = (LGL_MAX_CELLS as f64).max(4.0 * n as f64);
1214 if !cells.is_finite() || cells > limit {
1215 return Err(Error::invalid(format!(
1216 "`cellsize` {cellsize} is too small for `area` {area}: \
1217 the grid would have {cells:e} cells (at most {limit:e} allowed)"
1218 )));
1219 }
1220 }
1221 let mut res = Matrix::new();
1222 igraph_call!(igraph_layout_lgl(
1223 self,
1224 &mut res,
1225 int(options.maxiter),
1226 maxdelta,
1227 area,
1228 options.coolexp,
1229 repulserad,
1230 cellsize,
1231 options.root.unwrap_or(-1)
1232 ))?;
1233 Ok(res)
1234 }
1235
1236 /// Reingold–Tilford tree layout: parents centered above their children.
1237 ///
1238 /// Vertices are placed in levels by distance from the root (`y` is the
1239 /// depth), and subtrees are packed as tightly as possible. If the graph
1240 /// is not a tree a BFS spanning tree is used. `mode` selects which edges
1241 /// are followed from a parent ([`NeighborMode::Out`], `In`, or `All`,
1242 /// which is forced for undirected graphs). `roots` should reach every
1243 /// vertex (e.g. one per component): igraph hangs any unreachable vertex
1244 /// directly below the (single) root, or places it next to the roots when
1245 /// there are several. With several roots, igraph joins them under a
1246 /// hidden common root, so the roots are drawn at depth `y = 1` and their
1247 /// children at `y = 2`. `None` (or empty) selects the roots
1248 /// automatically: in igraph 1.0.1 with [`RootChoice::Degree`] below 500
1249 /// vertices and [`RootChoice::Eccentricity`] from 500 on (the C
1250 /// documentation states the opposite, the code does this; see
1251 /// [`roots_for_tree_layout`](Graph::roots_for_tree_layout) for a
1252 /// controllable choice). `rootlevel`, only used with several given roots,
1253 /// gives the extra depth of each root, which is useful for forests.
1254 /// Trees are conveniently built with [`Graph::kary_tree`]; use
1255 /// [`Graph::is_tree`] to check whether the drawing shows every edge, and
1256 /// [`Graph::unfold_tree`] to get the tree itself when the graph has cycles.
1257 /// Binds [`igraph_layout_reingold_tilford`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_reingold_tilford).
1258 ///
1259 /// # Errors
1260 /// [`ErrorKind::InvalidVertexId`](crate::ErrorKind::InvalidVertexId) for
1261 /// invalid roots, [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue)
1262 /// if `rootlevel` has negative entries or a huge sum (the extra levels
1263 /// are added as vertices), or is non-empty with a length
1264 /// different from that of `roots` (when several roots are given).
1265 ///
1266 /// # Examples
1267 ///
1268 /// ```
1269 /// use igraph::prelude::*;
1270 /// // A binary tree of depth 2 rooted at 0.
1271 /// let g = Graph::kary_tree(7, 2, TreeMode::Undirected).unwrap();
1272 /// let l = g.layout_reingold_tilford(NeighborMode::All, Some(&[0]), None).unwrap();
1273 /// assert_eq!(l.column(1), &[0.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0]); // depths
1274 /// assert_eq!(l[(0, 0)], (l[(1, 0)] + l[(2, 0)]) / 2.0); // root centered
1275 /// ```
1276 pub fn layout_reingold_tilford(
1277 &self,
1278 mode: NeighborMode,
1279 roots: Option<&[VertexId]>,
1280 rootlevel: Option<&[i64]>,
1281 ) -> Result<Matrix> {
1282 self.reingold_tilford_impl(mode, roots, rootlevel, false)
1283 }
1284
1285 /// Circular Reingold–Tilford tree layout: the root at the center and the
1286 /// levels on concentric circles.
1287 ///
1288 /// Same parameters as [`layout_reingold_tilford`](Graph::layout_reingold_tilford);
1289 /// the distance from the origin is the depth of a vertex, and the
1290 /// horizontal positions of the plain layout are mapped linearly to
1291 /// angles, the leftmost vertex at angle 0 and the rightmost one at
1292 /// `2π (n - 1) / n`.
1293 /// Binds [`igraph_layout_reingold_tilford_circular`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_reingold_tilford_circular).
1294 ///
1295 /// # Errors
1296 /// As for [`layout_reingold_tilford`](Graph::layout_reingold_tilford).
1297 ///
1298 /// # Examples
1299 ///
1300 /// ```
1301 /// use igraph::prelude::*;
1302 /// let g = Graph::kary_tree(13, 3, TreeMode::Undirected).unwrap();
1303 /// let l = g.layout_reingold_tilford_circular(NeighborMode::All, Some(&[0]), None).unwrap();
1304 /// let radius = |v: usize| l[(v, 0)].hypot(l[(v, 1)]);
1305 /// assert_eq!(radius(0), 0.0);
1306 /// assert!((radius(1) - 1.0).abs() < 1e-12 && (radius(12) - 2.0).abs() < 1e-12);
1307 /// ```
1308 pub fn layout_reingold_tilford_circular(
1309 &self,
1310 mode: NeighborMode,
1311 roots: Option<&[VertexId]>,
1312 rootlevel: Option<&[i64]>,
1313 ) -> Result<Matrix> {
1314 self.reingold_tilford_impl(mode, roots, rootlevel, true)
1315 }
1316
1317 fn reingold_tilford_impl(
1318 &self,
1319 mode: NeighborMode,
1320 roots: Option<&[VertexId]>,
1321 rootlevel: Option<&[i64]>,
1322 circular: bool,
1323 ) -> Result<Matrix> {
1324 let n = self.vcount();
1325 if let Some(r) = roots {
1326 check_vertices("roots", r, n)?;
1327 // Like igraph, `rootlevel` is only used (and checked) when it is
1328 // non-empty and there are several roots.
1329 if let Some(levels) = rootlevel
1330 && r.len() > 1
1331 && !levels.is_empty()
1332 && levels.len() != r.len()
1333 {
1334 return Err(Error::invalid("`roots` and `rootlevel` lengths differ"));
1335 }
1336 }
1337 if let Some(levels) = rootlevel {
1338 if levels.iter().any(|&x| x < 0) {
1339 return Err(Error::invalid(
1340 "`rootlevel` must not contain negative levels",
1341 ));
1342 }
1343 // igraph adds one vertex per level to a copy of the graph,
1344 // summing the levels unchecked.
1345 let total = levels
1346 .iter()
1347 .try_fold(int(n), |acc, &x| acc.checked_add(x))
1348 .filter(|&t| t <= RT_MAX_VERTICES);
1349 if total.is_none() {
1350 return Err(Error::invalid(format!(
1351 "the sum of `rootlevel` is too large (the graph plus the extra \
1352 levels must have at most {RT_MAX_VERTICES} vertices)"
1353 )));
1354 }
1355 }
1356 // igraph writes into the (nominally `const`) `roots` vector when
1357 // `rootlevel` is used: always hand it an owned copy, through a
1358 // pointer derived from a mutable borrow.
1359 let mut roots = roots.map(VectorInt::from_slice);
1360 let roots_ptr = roots
1361 .as_mut()
1362 .map_or(ptr::null(), |r| ptr::from_mut(r).cast_const());
1363 let rootlevel = rootlevel.map(VectorInt::view);
1364 let mut res = Matrix::new();
1365 // An earlier ALL-mode adjacency list of a directed graph with mutual
1366 // edges may have cached a wrong "has multi-edges" flag (igraph 1.0.1),
1367 // which makes igraph abort when its OUT/IN adjacency list finds none:
1368 // drop the cache before the call, and after it (ALL mode may write
1369 // the wrong value again).
1370 let guard = self.is_directed();
1371 if guard {
1372 self.invalidate_cache();
1373 }
1374 let _reset = CacheReset(guard.then_some(self));
1375 if circular {
1376 igraph_call!(igraph_layout_reingold_tilford_circular(
1377 self,
1378 &mut res,
1379 mode.into(),
1380 roots_ptr,
1381 opt_ptr(&rootlevel)
1382 ))?;
1383 } else {
1384 igraph_call!(igraph_layout_reingold_tilford(
1385 self,
1386 &mut res,
1387 mode.into(),
1388 roots_ptr,
1389 opt_ptr(&rootlevel)
1390 ))?;
1391 }
1392 Ok(res)
1393 }
1394
1395 /// Chooses a minimal set of roots for a nice tree layout, such that all
1396 /// vertices are reachable from them.
1397 ///
1398 /// For undirected graphs (or `mode = All`) one root is chosen per
1399 /// connected component; in directed mode one per strongly connected
1400 /// component without incoming (for `Out`) or outgoing (for `In`) edges
1401 /// (the components are those of [`Graph::connected_components`]). Within
1402 /// a component the root is chosen with the given [`RootChoice`] heuristic
1403 /// ([`RootChoice::Eccentricity`] relates to [`Graph::eccentricity`]).
1404 /// Typically used with [`layout_reingold_tilford`](Graph::layout_reingold_tilford).
1405 /// Binds [`igraph_roots_for_tree_layout`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_roots_for_tree_layout).
1406 ///
1407 /// # Examples
1408 ///
1409 /// ```
1410 /// use igraph::prelude::*;
1411 /// use igraph::layout::RootChoice;
1412 /// // Two stars: centers 0 and 4.
1413 /// let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (4, 5), (4, 6)], 7, false).unwrap();
1414 /// let roots = g.roots_for_tree_layout(NeighborMode::All, RootChoice::Degree).unwrap();
1415 /// assert_eq!(roots, vec![0, 4]);
1416 /// ```
1417 pub fn roots_for_tree_layout(
1418 &self,
1419 mode: NeighborMode,
1420 heuristic: RootChoice,
1421 ) -> Result<Vec<VertexId>> {
1422 let mut roots = VectorInt::new();
1423 igraph_call!(igraph_roots_for_tree_layout(
1424 self,
1425 mode.into(),
1426 &mut roots,
1427 heuristic.into()
1428 ))?;
1429 Ok(roots.into())
1430 }
1431
1432 /// Sugiyama layout for layered (directed acyclic) graphs, minimizing edge
1433 /// crossings.
1434 ///
1435 /// Vertices of the same layer are placed on the same horizontal line
1436 /// (`y = layer · vgap`; empty layers are skipped by the crossing
1437 /// minimization but still take room vertically); their `x`
1438 /// coordinates follow the heuristic of Sugiyama, Tagawa and Toda (1981).
1439 /// Without given layers, igraph breaks cycles (via a feedback arc set) and
1440 /// computes a layering itself. Edges spanning several layers are routed
1441 /// through dummy vertices, whose positions are returned in
1442 /// [`SugiyamaLayout::routing`]. Disconnected components are placed side
1443 /// by side. See [`SugiyamaOptions`]. Related: [`Graph::feedback_arc_set`]
1444 /// (the edges reversed to break cycles), [`Graph::is_dag`] and
1445 /// [`Graph::topological_sorting`].
1446 /// Binds [`igraph_layout_sugiyama`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_sugiyama).
1447 ///
1448 /// # Errors
1449 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `layers`
1450 /// or `weights` have the wrong length, or `layers` contains a negative
1451 /// index.
1452 ///
1453 /// # Examples
1454 ///
1455 /// ```
1456 /// use igraph::prelude::*;
1457 /// use igraph::layout::SugiyamaOptions;
1458 /// // 0 -> 1 -> 2 plus a shortcut 0 -> 2 spanning two layers.
1459 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], 3, true).unwrap();
1460 /// let s = g.layout_sugiyama(&SugiyamaOptions::default()).unwrap();
1461 /// assert_eq!(s.coords.column(1), &[0.0, 1.0, 2.0]);
1462 /// assert_eq!(s.routing[2].nrow(), 1); // one bend for the shortcut
1463 /// ```
1464 pub fn layout_sugiyama(&self, options: &SugiyamaOptions<'_>) -> Result<SugiyamaLayout> {
1465 check_len("layers", options.layers, self.vcount())?;
1466 check_len("weights", options.weights, self.ecount())?;
1467 if options.layers.is_some_and(|l| l.iter().any(|&x| x < 0)) {
1468 return Err(Error::invalid("layer indices must not be negative"));
1469 }
1470 let layers = options.layers.map(VectorInt::view);
1471 let weights = opt_view(options.weights);
1472 let mut coords = Matrix::new();
1473 let mut routing = MatrixList::new();
1474 igraph_call!(igraph_layout_sugiyama(
1475 self,
1476 &mut coords,
1477 &mut routing,
1478 opt_ptr(&layers),
1479 options.hgap,
1480 options.vgap,
1481 int(options.maxiter),
1482 opt_ptr(&weights)
1483 ))?;
1484 Ok(SugiyamaLayout {
1485 coords,
1486 routing: routing.into_vec(),
1487 })
1488 }
1489
1490 /// Places the vertices in a space of dimension `dim` with classical
1491 /// (Torgerson) multidimensional scaling.
1492 ///
1493 /// The Euclidean distances of the layout approximate the given symmetric
1494 /// `n` × `n` distance matrix (symmetry is not checked, and its diagonal
1495 /// is ignored), or, if `None`, the undirected shortest path lengths. `dim`
1496 /// must be at least 2 and at most the number of vertices. Disconnected
1497 /// graphs are laid out
1498 /// per component and merged with [`layout_merge_dla`], which only works
1499 /// for `dim = 2`. Vertices symmetric to each other (e.g. leaves of the
1500 /// same parent) may receive identical coordinates. Time complexity:
1501 /// usually around `O(|V|² dim)`. The default distance matrix is the one of
1502 /// [`Graph::distances`] with `NeighborMode::All`.
1503 /// Binds [`igraph_layout_mds`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_mds).
1504 ///
1505 /// # Errors
1506 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for a
1507 /// distance matrix of the wrong shape, `dim` out of range, or `dim > 2`
1508 /// on a disconnected graph.
1509 ///
1510 /// # Examples
1511 ///
1512 /// ```
1513 /// use igraph::prelude::*;
1514 /// // Three points on a line at 0, 1, 3: MDS recovers their distances.
1515 /// let g = Graph::from_edges(&[(0, 1), (1, 2)], 3, false).unwrap();
1516 /// let d = Matrix::from_rows(&[[0.0, 1.0, 3.0], [1.0, 0.0, 2.0], [3.0, 2.0, 0.0]]).unwrap();
1517 /// let l = g.layout_mds(Some(&d), 2).unwrap();
1518 /// let dist = |i: usize, j: usize| (l[(i, 0)] - l[(j, 0)]).hypot(l[(i, 1)] - l[(j, 1)]);
1519 /// assert!((dist(0, 2) - 3.0).abs() < 1e-9 && (dist(0, 1) - 1.0).abs() < 1e-9);
1520 /// ```
1521 pub fn layout_mds(&self, dist: Option<&Matrix>, dim: usize) -> Result<Matrix> {
1522 let n = self.vcount();
1523 if let Some(d) = dist
1524 && d.shape() != (n, n)
1525 {
1526 return Err(Error::invalid(format!(
1527 "the distance matrix is {}x{}, expected {n}x{n}",
1528 d.nrow(),
1529 d.ncol()
1530 )));
1531 }
1532 if let Some(d) = dist {
1533 // NaN distances reach LAPACK, which converts them to integers.
1534 crate::linalg::check_finite(d.as_slice(), "the distance matrix")?;
1535 }
1536 let dist = dist.map_or(ptr::null(), |d| d as *const Matrix);
1537 let mut res = Matrix::new();
1538 igraph_call!(igraph_layout_mds(self, &mut res, dist, int(dim)))?;
1539 Ok(res)
1540 }
1541
1542 /// Simple two-row layout for bipartite graphs.
1543 ///
1544 /// Vertices with type `true` are placed on the line `y = 0`, those with
1545 /// type `false` on `y = vgap`; positions within the rows are then
1546 /// optimized to reduce edge crossings with the Sugiyama heuristic, using
1547 /// `hgap` as the preferred minimum gap and at most `maxiter` iterations
1548 /// (100 is a reasonable default). Only the `types` matter, edges between
1549 /// vertices of the same type are allowed: a proper 2-coloring can be
1550 /// obtained with [`Graph::bipartite_types`], and the bipartite
1551 /// constructors (e.g. [`Graph::full_bipartite`]) return the types with
1552 /// the graph.
1553 /// Binds [`igraph_layout_bipartite`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_bipartite).
1554 ///
1555 /// # Errors
1556 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if `types`
1557 /// has the wrong length or `hgap` is negative.
1558 ///
1559 /// # Examples
1560 ///
1561 /// ```
1562 /// use igraph::prelude::*;
1563 /// let g = Graph::from_edges(&[(0, 2), (1, 2), (1, 3)], 4, false).unwrap();
1564 /// let l = g.layout_bipartite(&[false, false, true, true], 1.0, 1.0, 100).unwrap();
1565 /// assert_eq!(l.column(1), &[1.0, 1.0, 0.0, 0.0]);
1566 /// ```
1567 pub fn layout_bipartite(
1568 &self,
1569 types: &[bool],
1570 hgap: f64,
1571 vgap: f64,
1572 maxiter: usize,
1573 ) -> Result<Matrix> {
1574 check_len("types", Some(types), self.vcount())?;
1575 let types = VectorBool::view(types);
1576 let mut res = Matrix::new();
1577 igraph_call!(igraph_layout_bipartite(
1578 self,
1579 types.as_ptr(),
1580 &mut res,
1581 hgap,
1582 vgap,
1583 int(maxiter)
1584 ))?;
1585 Ok(res)
1586 }
1587
1588 /// Layout with Uniform Manifold Approximation and Projection (UMAP), in
1589 /// the plane. **Experimental in igraph.**
1590 ///
1591 /// UMAP embeds a (typically sparse, e.g. k-nearest-neighbors) distance
1592 /// graph: distances are turned into exponentially decaying weights in
1593 /// `[0, 1]`, and a stochastic gradient descent on the cross-entropy then
1594 /// places strongly connected vertices close together, while repelling
1595 /// unconnected ones beyond `min_dist`. Without distances all edges have
1596 /// the same weight. If `distances_are_weights` is set, the `distances`
1597 /// are used directly as weights (see
1598 /// [`layout_umap_compute_weights`](Graph::layout_umap_compute_weights)).
1599 /// A typical input is a k-nearest-neighbor graph of data points built
1600 /// with [`Graph::nearest_neighbor_graph`], with the distances from
1601 /// [`Graph::spatial_edge_lengths`]. Uses the calling thread's default
1602 /// random number generator. See [`UmapOptions`].
1603 /// Binds [`igraph_layout_umap`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_umap).
1604 ///
1605 /// # Errors
1606 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for
1607 /// invalid distances, a negative `min_dist` or a wrongly shaped or non-finite start.
1608 pub fn layout_umap(&self, options: &UmapOptions<'_>) -> Result<Matrix> {
1609 self.umap_impl(options, false)
1610 }
1611
1612 /// Layout with UMAP in 3D space. **Experimental in igraph.**
1613 ///
1614 /// The 3D version of [`layout_umap`](Graph::layout_umap).
1615 /// Binds [`igraph_layout_umap_3d`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_umap_3d).
1616 ///
1617 /// # Errors
1618 /// As for the 2D version.
1619 pub fn layout_umap_3d(&self, options: &UmapOptions<'_>) -> Result<Matrix> {
1620 self.umap_impl(options, true)
1621 }
1622
1623 fn umap_impl(&self, options: &UmapOptions<'_>, three_d: bool) -> Result<Matrix> {
1624 check_len("distances", options.distances, self.ecount())?;
1625 let (mut res, use_seed) =
1626 start_matrix(options.initial, self.vcount(), if three_d { 3 } else { 2 })?;
1627 let distances = opt_view(options.distances);
1628 let f = if three_d {
1629 igraph_layout_umap_3d
1630 } else {
1631 igraph_layout_umap
1632 };
1633 igraph_call!(f(
1634 self,
1635 &mut res,
1636 use_seed,
1637 opt_ptr(&distances),
1638 options.min_dist,
1639 int(options.epochs),
1640 options.distances_are_weights
1641 ))?;
1642 Ok(res)
1643 }
1644
1645 /// Computes the UMAP edge weights from the edge distances.
1646 /// **Experimental in igraph.**
1647 ///
1648 /// For each vertex a scale factor and a connectivity correction are
1649 /// computed, and distances become exponentially decaying weights in
1650 /// `[0, 1]`. The graph may be directed but must have no loops or multi-edges
1651 /// (pairs of opposite directed edges are allowed): such pairs are
1652 /// symmetrized with the *fuzzy union* `W = W1 + W2 - W1 W2`, stored on one
1653 /// of the two edges, while the other gets weight 0 (see
1654 /// [`Graph::is_simple`] and [`Graph::simplify`]). Loops are reported as
1655 /// errors, but multi-edges are not detected by igraph: all but one of
1656 /// the parallel edges silently get weight 0. Pass the result to
1657 /// [`layout_umap`](Graph::layout_umap) with `distances_are_weights`.
1658 /// `None` means that all edges have the same distance.
1659 /// Binds [`igraph_layout_umap_compute_weights`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_umap_compute_weights).
1660 ///
1661 /// # Errors
1662 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for
1663 /// negative or NaN distances, a wrong length, or loops.
1664 ///
1665 /// # Examples
1666 ///
1667 /// ```
1668 /// use igraph::prelude::*;
1669 /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], 4, false).unwrap();
1670 /// let w = g.layout_umap_compute_weights(Some(&[1.0, 2.0, 3.0])).unwrap();
1671 /// assert_eq!(w.len(), 3);
1672 /// assert!(w.iter().all(|&x| (0.0..=1.0).contains(&x)));
1673 /// ```
1674 pub fn layout_umap_compute_weights(&self, distances: Option<&[f64]>) -> Result<Vec<f64>> {
1675 check_len("distances", distances, self.ecount())?;
1676 let distances = opt_view(distances);
1677 let mut weights = Vector::new();
1678 igraph_call!(igraph_layout_umap_compute_weights(
1679 self,
1680 opt_ptr(&distances),
1681 &mut weights
1682 ))?;
1683 Ok(weights.into())
1684 }
1685
1686 /// The DrL (Distributed Recursive Layout) force-directed layout, in the plane.
1687 ///
1688 /// Designed for large graphs, it runs a sequence of simulated annealing
1689 /// phases driven by the given [`DrlOptions`] (start from a
1690 /// [`DrlTemplate`]), cutting highly stressed edges in the late phases to
1691 /// produce less dense, clustered layouts (Martin et al., 2008). `weights`
1692 /// must be positive (`None`: unit weights); `initial` gives optional
1693 /// starting positions (`n` × 2).
1694 /// Binds [`igraph_layout_drl`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_drl).
1695 ///
1696 /// # Errors
1697 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for
1698 /// negative damping multipliers, non-positive weights, weights above
1699 /// `1e20` (DrL computes in single precision and overflows; rescale such
1700 /// weights), NaN or infinite start coordinates, or wrong lengths.
1701 ///
1702 /// # Examples
1703 ///
1704 /// ```
1705 /// use igraph::prelude::*;
1706 /// use igraph::layout::DrlOptions;
1707 /// let edges: Vec<(i64, i64)> = (0..10).map(|i| (i, (i + 1) % 10)).collect();
1708 /// let g = Graph::from_edges(&edges, 10, false).unwrap();
1709 /// rng::seed(3).unwrap();
1710 /// let l = g.layout_drl(&DrlOptions::default(), None, None).unwrap();
1711 /// assert_eq!(l.shape(), (10, 2));
1712 /// ```
1713 pub fn layout_drl(
1714 &self,
1715 options: &DrlOptions,
1716 weights: Option<&[f64]>,
1717 initial: Option<&Matrix>,
1718 ) -> Result<Matrix> {
1719 self.drl_impl(options, weights, initial, false)
1720 }
1721
1722 /// The DrL force-directed layout in 3D space.
1723 ///
1724 /// The 3D version of [`layout_drl`](Graph::layout_drl) (`initial`, if
1725 /// given, must be `n` × 3).
1726 /// Binds [`igraph_layout_drl_3d`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_drl_3d).
1727 ///
1728 /// # Errors
1729 /// As for the 2D version.
1730 pub fn layout_drl_3d(
1731 &self,
1732 options: &DrlOptions,
1733 weights: Option<&[f64]>,
1734 initial: Option<&Matrix>,
1735 ) -> Result<Matrix> {
1736 self.drl_impl(options, weights, initial, true)
1737 }
1738
1739 fn drl_impl(
1740 &self,
1741 options: &DrlOptions,
1742 weights: Option<&[f64]>,
1743 initial: Option<&Matrix>,
1744 three_d: bool,
1745 ) -> Result<Matrix> {
1746 check_len("weights", weights, self.ecount())?;
1747 if let Some(w) = weights
1748 && !w.iter().all(|&x| x > 0.0 && x <= DRL_MAX_WEIGHT)
1749 {
1750 // Huge weights overflow DrL's single-precision energies into NaN
1751 // positions, which its density grid converts to integers (UB).
1752 return Err(Error::invalid(format!(
1753 "DrL weights must be positive and at most {DRL_MAX_WEIGHT:e}"
1754 )));
1755 }
1756 let (mut res, use_seed) =
1757 start_matrix(initial, self.vcount(), if three_d { 3 } else { 2 })?;
1758 let weights = opt_view(weights);
1759 let f = if three_d {
1760 igraph_layout_drl_3d
1761 } else {
1762 igraph_layout_drl
1763 };
1764 igraph_call!(f(self, &mut res, use_seed, options, opt_ptr(&weights)))?;
1765 Ok(res)
1766 }
1767
1768 /// The graphopt force-directed layout (a port of Michael Schmuhl's graphopt).
1769 ///
1770 /// Vertices are charged particles repelling each other (Coulomb's law,
1771 /// ignored beyond distance 500) and edges are springs (Hooke's law); the
1772 /// physical system is simulated for `niter` steps, without simulated
1773 /// annealing, so a stable fixed point is not guaranteed. A layout can be
1774 /// refined by passing it back as `initial`. See [`GraphoptOptions`].
1775 /// Time complexity: `O(niter (|V|² + |E|))`, or `O(niter |E|)` with zero
1776 /// charge.
1777 /// Binds [`igraph_layout_graphopt`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_graphopt).
1778 ///
1779 /// # Errors
1780 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for a
1781 /// wrongly shaped or non-finite start.
1782 ///
1783 /// # Examples
1784 ///
1785 /// Pure springs of rest length 1 (no charge) stretch a squeezed path
1786 /// along its diagonal (values from igraph's `igraph_layout_graphopt` unit
1787 /// test):
1788 ///
1789 /// ```
1790 /// use igraph::prelude::*;
1791 /// use igraph::layout::GraphoptOptions;
1792 /// let g = Graph::ring(4, false, false, false).unwrap(); // the path 0 - 1 - 2 - 3
1793 /// let start = Matrix::from_rows(&[[0.15, -0.15], [0.05, -0.05], [-0.05, 0.05], [-0.15, 0.15]]).unwrap();
1794 /// let opts = GraphoptOptions {
1795 /// node_charge: 0.0,
1796 /// spring_length: 1.0,
1797 /// spring_constant: 10.0,
1798 /// initial: Some(&start),
1799 /// ..Default::default()
1800 /// };
1801 /// let l = g.layout_graphopt(&opts).unwrap();
1802 /// assert!((l[(0, 0)] - 1.06066).abs() < 1e-5 && (l[(1, 1)] + 0.353553).abs() < 1e-5);
1803 /// ```
1804 pub fn layout_graphopt(&self, options: &GraphoptOptions<'_>) -> Result<Matrix> {
1805 let (mut res, use_seed) = start_matrix(options.initial, self.vcount(), 2)?;
1806 igraph_call!(igraph_layout_graphopt(
1807 self,
1808 &mut res,
1809 int(options.niter),
1810 options.node_charge,
1811 options.node_mass,
1812 options.spring_length,
1813 options.spring_constant,
1814 options.max_sa_movement,
1815 use_seed
1816 ))?;
1817 Ok(res)
1818 }
1819
1820 /// The GEM force-directed layout (Frick, Ludwig and Mehldau, 1994).
1821 ///
1822 /// Vertices are updated one at a time in random order, each with its own
1823 /// local temperature adapted to detect oscillations and rotations; the
1824 /// algorithm stops when the global temperature drops below `temp_min`
1825 /// or after `maxiter` vertex updates. Edge directions are ignored. See
1826 /// [`GemOptions`]. Time complexity: `O(t · n · (n + e))` for `t` steps.
1827 /// Binds [`igraph_layout_gem`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_gem).
1828 ///
1829 /// # Errors
1830 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) unless
1831 /// `0 < temp_min <= temp_init <= temp_max`, or for a wrongly shaped or non-finite start.
1832 ///
1833 /// # Examples
1834 ///
1835 /// ```
1836 /// use igraph::prelude::*;
1837 /// use igraph::layout::GemOptions;
1838 /// let g = Graph::ring(8, false, false, true).unwrap();
1839 /// rng::seed(11).unwrap();
1840 /// let l = g.layout_gem(&GemOptions::default()).unwrap();
1841 /// assert_eq!(l.shape(), (8, 2));
1842 /// // Zero iterations keep the start.
1843 /// let opts = GemOptions { maxiter: Some(0), initial: Some(&l), ..Default::default() };
1844 /// assert_eq!(g.layout_gem(&opts).unwrap(), l);
1845 /// ```
1846 pub fn layout_gem(&self, options: &GemOptions<'_>) -> Result<Matrix> {
1847 let n = self.vcount();
1848 let nn = nf(self);
1849 let (mut res, use_seed) = start_matrix(options.initial, n, 2)?;
1850 igraph_call!(igraph_layout_gem(
1851 self,
1852 &mut res,
1853 use_seed,
1854 int(options
1855 .maxiter
1856 .unwrap_or_else(|| n.saturating_mul(n).saturating_mul(40))),
1857 options.temp_max.unwrap_or(nn),
1858 options.temp_min,
1859 options.temp_init.unwrap_or_else(|| nn.sqrt())
1860 ))?;
1861 if n == 0 {
1862 // igraph returns early on the null graph without resizing.
1863 res.resize(0, 2);
1864 }
1865 Ok(res)
1866 }
1867
1868 /// The Davidson–Harel simulated annealing layout (1996).
1869 ///
1870 /// Minimizes an energy combining node-node distances, distances from the
1871 /// border, edge lengths, edge crossings and node-edge distances, first
1872 /// with simulated annealing then with a fine tuning phase; coordinates are
1873 /// kept within the bounds of the layout rectangle. Edge directions are
1874 /// ignored. The energy weights are hard to tune in general; see
1875 /// [`DavidsonHarelOptions`] for defaults determined by experimentation.
1876 /// Time complexity: `O(n² + m²)` per annealing iteration, `O(mn)` per fine
1877 /// tuning iteration.
1878 /// Binds [`igraph_layout_davidson_harel`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_davidson_harel).
1879 ///
1880 /// # Errors
1881 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if
1882 /// `cool_fact` is not in `(0, 1)`, if `maxiter + fineiter` overflows a
1883 /// 64-bit integer, or for a wrongly shaped or non-finite start.
1884 ///
1885 /// # Examples
1886 ///
1887 /// ```
1888 /// use igraph::prelude::*;
1889 /// use igraph::layout::DavidsonHarelOptions;
1890 /// let g = Graph::full(10, false, false).unwrap();
1891 /// rng::seed(42).unwrap();
1892 /// let l = g.layout_davidson_harel(&DavidsonHarelOptions::default()).unwrap();
1893 /// assert_eq!(l.shape(), (10, 2));
1894 /// assert!(l.as_slice().iter().all(|x| x.abs() < 20.0));
1895 /// ```
1896 pub fn layout_davidson_harel(&self, options: &DavidsonHarelOptions<'_>) -> Result<Matrix> {
1897 let n = self.vcount();
1898 let density = if n < 2 {
1899 0.0
1900 } else {
1901 (self.ecount() as f64 / (n as f64 * (n as f64 - 1.0) / 2.0)).min(1.0)
1902 };
1903 let fineiter = options
1904 .fineiter
1905 .unwrap_or_else(|| ((n.max(1) as f64).log2() as usize).max(10));
1906 // igraph loops while `round < maxiter + fineiter`.
1907 let (maxiter, fineiter) = (int(options.maxiter), int(fineiter));
1908 if maxiter.checked_add(fineiter).is_none() {
1909 return Err(Error::invalid(
1910 "`maxiter + fineiter` does not fit in a 64-bit integer",
1911 ));
1912 }
1913 let (mut res, use_seed) = start_matrix(options.initial, n, 2)?;
1914 igraph_call!(igraph_layout_davidson_harel(
1915 self,
1916 &mut res,
1917 use_seed,
1918 maxiter,
1919 fineiter,
1920 options.cool_fact,
1921 options.weight_node_dist,
1922 options.weight_border,
1923 options.weight_edge_lengths.unwrap_or(density / 10.0),
1924 options
1925 .weight_edge_crossings
1926 .unwrap_or(1.0 - density.sqrt()),
1927 options
1928 .weight_node_edge_dist
1929 .unwrap_or((1.0 - density) / 5.0)
1930 ))?;
1931 Ok(res)
1932 }
1933
1934 /// Centers a layout on the origin and rotates it to align it with the
1935 /// coordinate axes, in place.
1936 ///
1937 /// The principal axes are computed from the edge directions (weighted by
1938 /// squared edge lengths), or from the vertex positions if there are no
1939 /// edges of non-zero length. Useful after force-directed layouts; works
1940 /// in any dimension. Time complexity: `O(|V| + |E|)`.
1941 /// Binds [`igraph_layout_align`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_align).
1942 ///
1943 /// # Errors
1944 /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the
1945 /// layout does not have one row per vertex, has zero columns (and the
1946 /// graph is not the null graph), or contains NaN or infinite coordinates.
1947 ///
1948 /// # Examples
1949 ///
1950 /// ```
1951 /// use igraph::prelude::*;
1952 /// // A diagonal segment becomes horizontal and centered.
1953 /// let g = Graph::from_edges(&[(0, 1)], 2, false).unwrap();
1954 /// let mut l = Matrix::from_rows(&[[1.0, 1.0], [3.0, 3.0]]).unwrap();
1955 /// g.layout_align(&mut l).unwrap();
1956 /// assert!(l[(0, 1)].abs() < 1e-9 && l[(1, 1)].abs() < 1e-9);
1957 /// assert!((l[(0, 0)] + l[(1, 0)]).abs() < 1e-9);
1958 /// ```
1959 pub fn layout_align(&self, layout: &mut Matrix) -> Result<()> {
1960 if layout.nrow() != self.vcount() {
1961 return Err(Error::invalid(format!(
1962 "the layout has {} rows, expected one per vertex ({})",
1963 layout.nrow(),
1964 self.vcount()
1965 )));
1966 }
1967 if !layout.as_slice().iter().all(|x| x.is_finite()) {
1968 return Err(Error::invalid(
1969 "the layout contains NaN or infinite coordinates",
1970 ));
1971 }
1972 igraph_call!(igraph_layout_align(self, layout))
1973 }
1974}
1975
1976/// Merges the 2D layouts of several graphs (typically the components of a
1977/// graph) into one, using diffusion-limited aggregation (DLA).
1978///
1979/// Each layout is covered by a circle and rescaled so that the area of the
1980/// circle grows with the size of the graph; the largest layout is placed at
1981/// the origin and the others, from larger to smaller, perform random walks
1982/// until they stick next to the already placed ones. The result has the rows
1983/// of all the layouts, in the given order. The graphs are currently only
1984/// used for bookkeeping (igraph only looks at the coordinates). Uses the
1985/// calling thread's default random number generator. The typical input is
1986/// the list of components from [`Graph::decompose`], each laid out on its
1987/// own. `graphs` is any collection of graphs or references to graphs (like
1988/// the multi-graph functions of [`operators`](crate::operators)), e.g. the
1989/// `&Vec<Graph>` returned by `decompose` or a `&[&Graph]`.
1990/// Binds [`igraph_layout_merge_dla`](https://igraph.org/c/html/latest/igraph-Layout.html#igraph_layout_merge_dla).
1991///
1992/// # Errors
1993/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if the number
1994/// of graphs and layouts differ, no layout is given, a layout is empty or
1995/// is not 2D, or a layout does not have one row per vertex of its graph.
1996///
1997/// # Examples
1998///
1999/// ```
2000/// use igraph::prelude::*;
2001/// use igraph::layout::layout_merge_dla;
2002/// // A triangle and a separate edge, laid out component by component.
2003/// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (3, 4)], 5, false).unwrap();
2004/// let parts = g.decompose(Connectedness::Weak, None, 1).unwrap();
2005/// let layouts: Vec<Matrix> = parts.iter().map(|p| p.layout_circle(..).unwrap()).collect();
2006/// rng::seed(42).unwrap();
2007/// // Any collection of graphs works: `&Vec<Graph>`, `&[&Graph]`, `Vec<Graph>`...
2008/// let merged = layout_merge_dla(&parts, &layouts).unwrap();
2009/// assert_eq!(merged.shape(), (5, 2));
2010/// // Each component is only scaled and translated: the triangle stays equilateral.
2011/// let d = |i: usize, j: usize| (merged[(i, 0)] - merged[(j, 0)]).hypot(merged[(i, 1)] - merged[(j, 1)]);
2012/// assert!((d(0, 1) - d(1, 2)).abs() < 1e-9 && (d(1, 2) - d(2, 0)).abs() < 1e-9);
2013/// ```
2014pub fn layout_merge_dla<G: AsRef<Graph>>(
2015 graphs: impl IntoIterator<Item = G>,
2016 coords: &[Matrix],
2017) -> Result<Matrix> {
2018 let graphs: Vec<G> = graphs.into_iter().collect();
2019 let graphs: Vec<&Graph> = graphs.iter().map(AsRef::as_ref).collect();
2020 if graphs.len() != coords.len() {
2021 return Err(Error::invalid(format!(
2022 "{} graphs but {} layouts given",
2023 graphs.len(),
2024 coords.len()
2025 )));
2026 }
2027 if coords.is_empty() {
2028 return Err(Error::invalid("at least one layout is needed"));
2029 }
2030 for (i, (g, m)) in graphs.iter().zip(coords).enumerate() {
2031 if m.ncol() != 2 || m.nrow() == 0 {
2032 return Err(Error::invalid(format!(
2033 "layout {i} is {}x{}, but only non-empty 2D layouts can be merged",
2034 m.nrow(),
2035 m.ncol()
2036 )));
2037 }
2038 if m.nrow() != g.vcount() {
2039 return Err(Error::invalid(format!(
2040 "layout {i} has {} rows, but its graph has {} vertices",
2041 m.nrow(),
2042 g.vcount()
2043 )));
2044 }
2045 }
2046 let graph_ptrs = GraphPtrs::new(&graphs)?;
2047 let mut list = MatrixList::new();
2048 for m in coords {
2049 list.push(m.clone());
2050 }
2051 let mut res = Matrix::new();
2052 igraph_call!(igraph_layout_merge_dla(&graph_ptrs.raw, &list, &mut res))?;
2053 Ok(res)
2054}
2055
2056/// A graph is trivially a reference to itself: this lets functions taking
2057/// several graphs, like [`layout_merge_dla`], accept collections of graphs
2058/// (`Vec<Graph>`, `&[Graph]`) as well as of references (`&[&Graph]`).
2059impl AsRef<Graph> for Graph {
2060 fn as_ref(&self) -> &Graph {
2061 self
2062 }
2063}
2064
2065/// An `igraph_vector_ptr_t` of borrowed graph pointers (never owning them).
2066struct GraphPtrs<'a> {
2067 raw: igraph_vector_ptr_t,
2068 _borrow: std::marker::PhantomData<&'a Graph>,
2069}
2070
2071impl<'a> GraphPtrs<'a> {
2072 fn new(graphs: &[&'a Graph]) -> Result<Self> {
2073 let mut raw = MaybeUninit::<igraph_vector_ptr_t>::uninit();
2074 igraph_call!(igraph_vector_ptr_init(raw.as_mut_ptr(), 0))?;
2075 // No item destructor is set: destroying the vector leaves the graphs alone.
2076 let mut ptrs = Self {
2077 raw: unsafe { raw.assume_init() },
2078 _borrow: std::marker::PhantomData,
2079 };
2080 for g in graphs {
2081 let p = *g as *const Graph as *mut c_void;
2082 igraph_call!(igraph_vector_ptr_push_back(&mut ptrs.raw, p))?;
2083 }
2084 Ok(ptrs)
2085 }
2086}
2087
2088impl Drop for GraphPtrs<'_> {
2089 fn drop(&mut self) {
2090 unsafe { igraph_vector_ptr_destroy(&mut self.raw) };
2091 }
2092}