igraph/tutorial.rs
1//! Translations of the [igraph C tutorial](https://igraph.org/c/html/latest/igraph-Tutorial.html)
2//! into Rust.
3//!
4//! The igraph C reference manual opens with a tutorial made of three short
5//! lessons, whose programs live in `examples/tutorial/tutorial{1,2,3}.c` of
6//! the igraph sources. This module translates each of them, one public
7//! function per lesson, keeping the *same steps in the same order* (and hence
8//! the same pseudo-random draws), so that the Rust versions compute exactly
9//! the numbers printed by the C programs with igraph 1.0.1:
10//!
11//! | Lesson | Rust | C program prints |
12//! |--------|------|------------------|
13//! | [1. Compiling programs using igraph](https://igraph.org/c/html/latest/igraph-Tutorial.html#tut-lesson-1) | [`example_1`] → [`RandomGraphStats`] | `Diameter of a random graph with average degree 2: 23` |
14//! | [2. Creating your first graphs](https://igraph.org/c/html/latest/igraph-Tutorial.html#tut-lesson-2) | [`example_2`] → [`LatticePathLengths`] | `Average path length (lattice): 15.0167`, then `11.8142` once randomized |
15//! | [3. Calculating various properties of graphs](https://igraph.org/c/html/latest/igraph-Tutorial.html#tut-lesson-3) | [`example_3`] → [`KarateCentralities`] | maximum degree 17, closeness 0.0172414, betweenness 231.071 |
16//!
17//! Every result struct implements [`Display`](std::fmt::Display) reproducing,
18//! character by character, the lines printed by the corresponding C program
19//! (including C's `%g` number formatting), so the translation can be checked
20//! against the original at a glance:
21//!
22//! ```
23//! use igraph::tutorial;
24//!
25//! println!("{}", tutorial::example_1()?);
26//! println!("{}", tutorial::example_2()?);
27//! println!("{}", tutorial::example_3()?);
28//! assert_eq!(
29//! tutorial::example_1()?.to_string(),
30//! "Diameter of a random graph with average degree 2: 23"
31//! );
32//! # Ok::<(), igraph::Error>(())
33//! ```
34//!
35//! # Lesson 1: compiling programs using igraph
36//!
37//! The first program generates a random graph and prints its diameter and
38//! mean degree. The C tutorial uses it to illustrate a few points, and each
39//! of them has a Rust counterpart:
40//!
41//! - C programs include `igraph.h`; in Rust, `use igraph::prelude::*;` brings
42//! the graph type, the containers and the enums into scope.
43//! - C programs must call `igraph_setup()` before anything else. The Rust
44//! bindings do it for you: every call into igraph first makes sure the
45//! library (and the calling thread's error handler and random number
46//! generator) is initialized.
47//! - igraph uses `igraph_int_t` for integers and `igraph_real_t` for reals:
48//! these are `i64` and `f64` in Rust. Vertex and edge ids are
49//! [`VertexId`] and [`EdgeId`](crate::EdgeId) (both `i64`), counts are `usize`.
50//! - Graphs are `igraph_t` objects, which is exactly what [`Graph`] is (a type
51//! alias enriched with methods). Generators such as
52//! [`Graph::erdos_renyi_game_gnm`] create them; where C calls
53//! `igraph_destroy()`, Rust frees the graph automatically when it goes out
54//! of scope.
55//! - C functions return an error code; the Rust methods return a
56//! [`Result`], propagated with `?`.
57//!
58//! The igraph tutorial then explains how to compile the program with CMake or
59//! `pkg-config`; here `cargo build` takes care of it (the build script finds
60//! the installed igraph library and generates the raw bindings).
61//!
62//! # Lesson 2: creating your first graphs
63//!
64//! Functions creating graphs are called *generators*; randomized ones are
65//! called *games*. Deterministic regular structures include stars
66//! ([`Graph::star`]), cycles ([`Graph::cycle_graph`]), lattices
67//! ([`Graph::square_lattice`]) and trees ([`Graph::kary_tree`]). Most
68//! generators, and most other functions, handle both directed and undirected
69//! graphs.
70//!
71//! The second program builds a 30 × 30 periodic square lattice (a torus),
72//! computes the average shortest path length, adds ten random edges and
73//! computes it again: a handful of random "shortcuts" shrinks the average
74//! distance a lot (from about 15 to about 11.8), the essence of the
75//! *small-world* effect.
76//!
77//! In C, igraph uses its own vector types (`igraph_vector_t`,
78//! `igraph_vector_int_t`, `igraph_vector_bool_t`, ...) instead of plain
79//! arrays, initialized with `igraph_vector_init()` and destroyed with
80//! `igraph_vector_destroy()`. The Rust bindings accept slices as inputs and
81//! return `Vec`s as outputs (the owned igraph vectors, such as
82//! [`VectorInt`](crate::vector::VectorInt), exist too and free themselves on
83//! drop). Vertices are identified by ids `0..n`, where `n` is
84//! [`Graph::vcount`]. [`Graph::add_edges`] takes `(from, to)` pairs, whereas
85//! the C `igraph_add_edges()` takes a flat vector of endpoints
86//! ([`Graph::add_edges_from_vector`] is its literal counterpart).
87//!
88//! As the tutorial warns, drawing random endpoints may create *loop edges*
89//! (from a vertex to itself) and *multi-edges* (several edges between the same
90//! pair of vertices). igraph graphs can represent them, but some functions
91//! expect simple graphs: [`Graph::simplify`] removes them. (With seed 42 the
92//! ten edges drawn by lesson 2, see [`LatticePathLengths::random_edges`],
93//! happen to keep the lattice simple, although vertex 885 is drawn twice in a
94//! row, as the endpoint of two different edges.)
95//!
96//! # Lesson 3: calculating various properties of graphs
97//!
98//! The third program computes three *centrality* measures on the friendship
99//! network of Zachary's karate club: how central the position of every member
100//! is. It builds the graph from a plain array of endpoints
101//! ([`ZACHARY_KARATE_EDGES`], with [`Graph::from_flat_edges`]; C creates a
102//! non-owning *view* of the array with `igraph_vector_int_view()`, which the
103//! Rust bindings do internally), then computes
104//!
105//! - the degree of every vertex ([`Graph::degree`]),
106//! - the closeness centrality ([`Graph::closeness`]),
107//! - the betweenness centrality ([`Graph::betweenness`]),
108//!
109//! and prints the largest value of each, with the vertex attaining it. The
110//! instructor (vertex 0) and the administrator (vertex 33) stand out.
111//!
112//! In C, the argument `igraph_vss_all()` is a *vertex selector* asking for
113//! the property of every vertex; in Rust it is [`VertexSelector::All`], or
114//! simply the full range `..` (see [`crate::selector`] for the other
115//! selectors).
116
117use std::fmt;
118
119use crate::constants::{EdgeTypeSw, Loops, NeighborMode};
120use crate::error::{Error, ErrorKind, Result};
121use crate::graph::{Graph, VertexId};
122use crate::rng;
123use crate::selector::VertexSelector;
124
125/// The numbers computed by lesson 1 (see [`example_1`]).
126///
127/// Its [`Display`](fmt::Display) implementation prints the same line as the
128/// C program, e.g. `Diameter of a random graph with average degree 2: 23`.
129#[derive(Debug, Clone, Copy, PartialEq)]
130pub struct RandomGraphStats {
131 /// Length of the longest geodesic, considering every connected component
132 /// (the C program passes `unconn = true`).
133 pub diameter: f64,
134 /// Average degree of the vertices, `2m / n` (self-loops counted, as with
135 /// `IGRAPH_LOOPS` in C).
136 pub mean_degree: f64,
137}
138
139impl fmt::Display for RandomGraphStats {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 write!(
142 f,
143 "Diameter of a random graph with average degree {}: {}",
144 format_g(self.mean_degree),
145 format_g(self.diameter)
146 )
147 }
148}
149
150/// The numbers computed by lesson 2 (see [`example_2`]).
151///
152/// Its [`Display`](fmt::Display) implementation prints the same two lines as
153/// the C program.
154#[derive(Debug, Clone, PartialEq)]
155pub struct LatticePathLengths {
156 /// Average shortest path length of the 30 × 30 periodic lattice.
157 pub lattice: f64,
158 /// Average shortest path length after adding [`random_edges`](Self::random_edges).
159 pub randomized: f64,
160 /// The ten random edges added to the lattice, in the order they were
161 /// drawn. Such random draws may in general produce loops and multi-edges;
162 /// the ten edges drawn after seeding with 42 happen to contain neither.
163 pub random_edges: Vec<(VertexId, VertexId)>,
164}
165
166impl fmt::Display for LatticePathLengths {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 writeln!(
169 f,
170 "Average path length (lattice): {}",
171 format_g(self.lattice)
172 )?;
173 write!(
174 f,
175 "Average path length (randomized lattice): {}",
176 format_g(self.randomized)
177 )
178 }
179}
180
181/// The maximum of a per-vertex measure and the vertex attaining it, the
182/// Rust counterpart of the C pair `igraph_vector_max()` /
183/// `igraph_vector_which_max()`.
184#[derive(Debug, Clone, Copy, PartialEq)]
185pub struct Maximum<T> {
186 /// The largest value.
187 pub value: T,
188 /// The (first) vertex attaining it.
189 pub vertex: VertexId,
190}
191
192impl<T: PartialOrd + Copy> Maximum<T> {
193 /// The largest element of `values` (indexed by vertex id) and its index,
194 /// or `None` for an empty slice.
195 ///
196 /// It behaves exactly like igraph's `igraph_vector_max()` and
197 /// `igraph_vector_which_max()`: the *first* maximal element wins ties,
198 /// and if `values` contains a `NaN` (for floating-point types: any
199 /// element not comparable with itself), the first `NaN` and its index are
200 /// returned.
201 ///
202 /// # Examples
203 /// ```
204 /// use igraph::tutorial::Maximum;
205 /// assert_eq!(Maximum::of(&[3, 7, 1, 7]), Some(Maximum { value: 7, vertex: 1 }));
206 /// assert_eq!(Maximum::<f64>::of(&[]), None);
207 /// // As in igraph, a NaN is the "maximum" of any vector containing one.
208 /// let m = Maximum::of(&[1.0, f64::NAN, 2.0]).unwrap();
209 /// assert!(m.value.is_nan() && m.vertex == 1);
210 /// ```
211 pub fn of(values: &[T]) -> Option<Self> {
212 let mut best: Option<Self> = None;
213 for (i, &value) in values.iter().enumerate() {
214 let candidate = Maximum {
215 value,
216 vertex: i as VertexId,
217 };
218 // `NaN` is the only value not comparable with itself.
219 if value.partial_cmp(&value).is_none() {
220 return Some(candidate);
221 }
222 if best.is_none_or(|b| value > b.value) {
223 best = Some(candidate);
224 }
225 }
226 best
227 }
228}
229
230/// The centrality maxima computed by lesson 3 (see [`example_3`]).
231///
232/// Its [`Display`](fmt::Display) implementation prints the same three lines
233/// as the C program.
234#[derive(Debug, Clone, Copy, PartialEq)]
235pub struct KarateCentralities {
236 /// Maximum degree.
237 pub degree: Maximum<i64>,
238 /// Maximum (non-normalized) closeness centrality.
239 pub closeness: Maximum<f64>,
240 /// Maximum (non-normalized) betweenness centrality.
241 pub betweenness: Maximum<f64>,
242}
243
244impl fmt::Display for KarateCentralities {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 writeln!(
247 f,
248 "Maximum degree is {:>10}, vertex {:>2}.",
249 self.degree.value, self.degree.vertex
250 )?;
251 writeln!(
252 f,
253 "Maximum closeness is {:>10}, vertex {:>2}.",
254 format_g(self.closeness.value),
255 self.closeness.vertex
256 )?;
257 write!(
258 f,
259 "Maximum betweenness is {:>10}, vertex {:>2}.",
260 format_g(self.betweenness.value),
261 self.betweenness.vertex
262 )
263 }
264}
265
266/// Lesson 1: the diameter and mean degree of a random graph.
267///
268/// Seeds the calling thread's default random number generator with 42 (as
269/// the C program does, "to ensure identical results across runs"), generates
270/// a simple undirected Erdős–Rényi `G(n, m)` graph with `n = 1000` vertices
271/// and `m = 1000` edges, and computes its diameter (over all components, the
272/// graph being disconnected) and its mean degree `2m / n = 2`.
273///
274/// It translates `examples/tutorial/tutorial1.c` of the
275/// [first lesson](https://igraph.org/c/html/latest/igraph-Tutorial.html#tut-lesson-1),
276/// and computes exactly the values printed by the C program with igraph
277/// 1.0.1: `Diameter of a random graph with average degree 2: 23`.
278///
279/// # C code
280///
281/// ```c
282/// #include <igraph.h>
283///
284/// int main(void) {
285/// igraph_int_t num_vertices = 1000;
286/// igraph_int_t num_edges = 1000;
287/// igraph_real_t diameter, mean_degree;
288/// igraph_t graph;
289///
290/// /* Initialize the library. */
291/// igraph_setup();
292///
293/// /* Ensure identical results across runs. */
294/// igraph_rng_seed(igraph_rng_default(), 42);
295///
296/// igraph_erdos_renyi_game_gnm(
297/// &graph, num_vertices, num_edges,
298/// IGRAPH_UNDIRECTED, IGRAPH_SIMPLE_SW, IGRAPH_EDGE_UNLABELED);
299///
300/// igraph_diameter(
301/// &graph, /* weights = */ NULL,
302/// &diameter,
303/// /* from = */ NULL, /* to = */ NULL,
304/// /* vertex_path = */ NULL, /* edge_path = */ NULL,
305/// IGRAPH_UNDIRECTED, /* unconn= */ true);
306///
307/// igraph_mean_degree(&graph, &mean_degree, IGRAPH_LOOPS);
308/// printf("Diameter of a random graph with average degree %g: %g\n",
309/// mean_degree, diameter);
310///
311/// igraph_destroy(&graph);
312///
313/// return 0;
314/// }
315/// ```
316///
317/// # Rust translation
318///
319/// The body of this function, step by step (no `igraph_setup()` and no
320/// `igraph_destroy()` are needed):
321///
322/// ```
323/// use igraph::prelude::*;
324///
325/// let (num_vertices, num_edges) = (1000, 1000);
326///
327/// // Ensure identical results across runs.
328/// rng::seed(42)?;
329///
330/// let graph = Graph::erdos_renyi_game_gnm(
331/// num_vertices,
332/// num_edges,
333/// false, // undirected
334/// EdgeTypeSw::Simple,
335/// false, // unlabeled edges
336/// )?;
337///
338/// let diameter = graph.diameter()?; // unweighted, over all components
339/// let mean_degree = graph.mean_degree(true)?; // loops counted
340/// println!("Diameter of a random graph with average degree {mean_degree}: {diameter}");
341///
342/// assert_eq!(diameter, 23.0);
343/// assert_eq!(mean_degree, 2.0);
344///
345/// // ... which is what `example_1` returns.
346/// let stats = igraph::tutorial::example_1()?;
347/// assert_eq!((stats.diameter, stats.mean_degree), (diameter, mean_degree));
348/// assert_eq!(
349/// stats.to_string(),
350/// "Diameter of a random graph with average degree 2: 23"
351/// );
352/// # Ok::<(), igraph::Error>(())
353/// ```
354///
355/// # Errors
356/// Propagates the errors of the igraph calls (none are expected).
357pub fn example_1() -> Result<RandomGraphStats> {
358 let num_vertices = 1000;
359 let num_edges = 1000;
360
361 // Ensure identical results across runs.
362 rng::seed(42)?;
363
364 let graph =
365 Graph::erdos_renyi_game_gnm(num_vertices, num_edges, false, EdgeTypeSw::Simple, false)?;
366
367 let diameter = graph.diameter()?;
368 let mean_degree = graph.mean_degree(true)?;
369
370 Ok(RandomGraphStats {
371 diameter,
372 mean_degree,
373 })
374}
375
376/// Lesson 2: average path length of a lattice, before and after adding a
377/// few random edges.
378///
379/// Builds the undirected 30 × 30 square lattice with periodic boundaries in
380/// both dimensions (a torus: every vertex has degree 4) and computes its
381/// average shortest path length, `15 · 900 / 899 ≈ 15.0167`. Then it seeds
382/// the thread's default random number generator with 42, draws 20 uniform
383/// vertex ids in `0..900` (pairing them up as ten edges; in general such
384/// draws may include loops and multi-edges, although with seed 42 they do
385/// not), adds them to the lattice and computes the average
386/// path length again, `≈ 11.8142`: ten random shortcuts reduce distances by
387/// more than 20%.
388///
389/// It translates `examples/tutorial/tutorial2.c` of the
390/// [second lesson](https://igraph.org/c/html/latest/igraph-Tutorial.html#tut-lesson-2),
391/// drawing the random numbers in the same order, so that the results are
392/// exactly those printed by the C program with igraph 1.0.1.
393///
394/// # C code
395///
396/// ```c
397/// #include <igraph.h>
398///
399/// int main(void) {
400/// igraph_t graph;
401/// igraph_vector_int_t dimvector;
402/// igraph_vector_int_t edges;
403/// igraph_vector_bool_t periodic;
404/// igraph_real_t avg_path_len;
405///
406/// /* Initialize the library. */
407/// igraph_setup();
408///
409/// igraph_vector_int_init(&dimvector, 2);
410/// VECTOR(dimvector)[0] = 30;
411/// VECTOR(dimvector)[1] = 30;
412///
413/// igraph_vector_bool_init(&periodic, 2);
414/// igraph_vector_bool_fill(&periodic, true);
415/// igraph_square_lattice(&graph, &dimvector, 0, IGRAPH_UNDIRECTED,
416/// /* mutual= */ false, &periodic);
417///
418/// igraph_average_path_length(&graph, NULL, &avg_path_len, NULL,
419/// IGRAPH_UNDIRECTED, /* unconn= */ true);
420/// printf("Average path length (lattice): %g\n", (double) avg_path_len);
421///
422/// /* Seed the RNG to ensure identical results across runs. */
423/// igraph_rng_seed(igraph_rng_default(), 42);
424///
425/// igraph_vector_int_init(&edges, 20);
426/// for (igraph_int_t i = 0; i < igraph_vector_int_size(&edges); i++) {
427/// VECTOR(edges)[i] = RNG_INTEGER(0, igraph_vcount(&graph) - 1);
428/// }
429///
430/// igraph_add_edges(&graph, &edges, NULL);
431/// igraph_average_path_length(&graph, NULL, &avg_path_len, NULL,
432/// IGRAPH_UNDIRECTED, /* unconn= */ true);
433/// printf("Average path length (randomized lattice): %g\n", (double) avg_path_len);
434///
435/// igraph_vector_bool_destroy(&periodic);
436/// igraph_vector_int_destroy(&dimvector);
437/// igraph_vector_int_destroy(&edges);
438/// igraph_destroy(&graph);
439///
440/// return 0;
441/// }
442/// ```
443///
444/// # Rust translation
445///
446/// The body of this function, step by step: plain arrays replace the
447/// `igraph_vector_*_t` objects, and nothing needs to be destroyed.
448///
449/// ```
450/// use igraph::prelude::*;
451///
452/// let mut graph = Graph::square_lattice(
453/// &[30, 30], // dimensions
454/// 0, // nei: only direct neighbors
455/// false, // undirected
456/// false, // mutual (directed graphs only)
457/// Some(&[true, true]), // periodic in both dimensions
458/// )?;
459///
460/// let lattice = graph.average_path_length(None, false, true)?;
461/// println!("Average path length (lattice): {lattice}");
462/// assert!((lattice - 15.0 * 900.0 / 899.0).abs() < 1e-12);
463///
464/// // Seed the RNG to ensure identical results across runs.
465/// rng::seed(42)?;
466///
467/// let n = graph.vcount() as i64;
468/// let edges: Vec<i64> = (0..20).map(|_| rng::integer(0, n - 1)).collect();
469///
470/// graph.add_edges_from_vector(&edges)?;
471/// let randomized = graph.average_path_length(None, false, true)?;
472/// println!("Average path length (randomized lattice): {randomized}");
473/// assert!((randomized - 11.814163885799037).abs() < 1e-12);
474///
475/// // ... which is what `example_2` returns.
476/// let res = igraph::tutorial::example_2()?;
477/// assert_eq!((res.lattice, res.randomized), (lattice, randomized));
478/// assert_eq!(res.to_string(), "\
479/// Average path length (lattice): 15.0167
480/// Average path length (randomized lattice): 11.8142");
481/// # Ok::<(), igraph::Error>(())
482/// ```
483///
484/// # Errors
485/// Propagates the errors of the igraph calls (none are expected).
486pub fn example_2() -> Result<LatticePathLengths> {
487 let mut graph = Graph::square_lattice(&[30, 30], 0, false, false, Some(&[true, true]))?;
488
489 let lattice = graph.average_path_length(None, false, true)?;
490
491 // Seed the RNG to ensure identical results across runs.
492 rng::seed(42)?;
493
494 // Twenty endpoints drawn one after the other, as in C (tuple fields are
495 // evaluated left to right), paired up as ten edges.
496 let n = graph.vcount() as VertexId;
497 let random_edges: Vec<(VertexId, VertexId)> = (0..10)
498 .map(|_| (rng::integer(0, n - 1), rng::integer(0, n - 1)))
499 .collect();
500
501 graph.add_edges(&random_edges)?;
502 let randomized = graph.average_path_length(None, false, true)?;
503
504 Ok(LatticePathLengths {
505 lattice,
506 randomized,
507 random_edges,
508 })
509}
510
511/// The friendship network of
512/// [Zachary's karate club](https://en.wikipedia.org/wiki/Zachary%27s_karate_club)
513/// as the flat endpoint array of the C tutorial (lesson 3): 78 undirected
514/// edges among 34 members, `[from0, to0, from1, to1, ...]`.
515///
516/// Vertex 0 is the instructor ("Mr. Hi") and vertex 33 the administrator
517/// ("John A."), whose conflict split the club in two.
518///
519/// # Examples
520/// ```
521/// use igraph::prelude::*;
522/// use igraph::tutorial::ZACHARY_KARATE_EDGES;
523///
524/// let karate = Graph::from_flat_edges(&ZACHARY_KARATE_EDGES, 0, false)?;
525/// assert_eq!((karate.vcount(), karate.ecount()), (34, 78));
526/// # Ok::<(), igraph::Error>(())
527/// ```
528#[rustfmt::skip]
529pub const ZACHARY_KARATE_EDGES: [VertexId; 156] = [
530 0,1, 0,2, 0,3, 0,4, 0,5, 0,6, 0,7, 0,8,
531 0,10, 0,11, 0,12, 0,13, 0,17, 0,19, 0,21, 0,31,
532 1, 2, 1, 3, 1, 7, 1,13, 1,17, 1,19, 1,21, 1,30,
533 2, 3, 2, 7, 2,27, 2,28, 2,32, 2, 9, 2, 8, 2,13,
534 3, 7, 3,12, 3,13, 4, 6, 4,10, 5, 6, 5,10, 5,16,
535 6,16, 8,30, 8,32, 8,33, 9,33, 13,33, 14,32, 14,33,
536 15,32, 15,33, 18,32, 18,33, 19,33, 20,32, 20,33,
537 22,32, 22,33, 23,25, 23,27, 23,32, 23,33, 23,29,
538 24,25, 24,27, 24,31, 25,31, 26,29, 26,33, 27,33,
539 28,31, 28,33, 29,32, 29,33, 30,32, 30,33, 31,32,
540 31,33, 32,33,
541];
542
543/// Lesson 3: degree, closeness and betweenness centrality in Zachary's
544/// karate club.
545///
546/// Creates the undirected friendship graph from [`ZACHARY_KARATE_EDGES`] and
547/// returns, for each of the three centrality measures, its maximum and the
548/// first vertex attaining it:
549///
550/// - degree (all neighbors, loops counted): 17, the administrator (vertex 33);
551/// - closeness (non-normalized, `1 / Σ distances`): `1/58 ≈ 0.0172414`, the
552/// instructor (vertex 0);
553/// - betweenness (non-normalized): `≈ 231.071`, the instructor again.
554///
555/// It translates `examples/tutorial/tutorial3.c` of the
556/// [third lesson](https://igraph.org/c/html/latest/igraph-Tutorial.html#tut-lesson-3).
557///
558/// # C code
559///
560/// ```c
561/// #include <igraph.h>
562///
563/// int main(void) {
564/// igraph_t graph;
565/// igraph_vector_int_t result;
566/// igraph_vector_t result_real;
567/// igraph_int_t edges_array[] = {
568/// 0,1, 0,2, 0,3, 0,4, 0,5, 0,6, 0,7, 0,8,
569/// 0,10, 0,11, 0,12, 0,13, 0,17, 0,19, 0,21, 0,31,
570/// 1, 2, 1, 3, 1, 7, 1,13, 1,17, 1,19, 1,21, 1,30,
571/// 2, 3, 2, 7, 2,27, 2,28, 2,32, 2, 9, 2, 8, 2,13,
572/// 3, 7, 3,12, 3,13, 4, 6, 4,10, 5, 6, 5,10, 5,16,
573/// 6,16, 8,30, 8,32, 8,33, 9,33, 13,33, 14,32, 14,33,
574/// 15,32, 15,33, 18,32, 18,33, 19,33, 20,32, 20,33,
575/// 22,32, 22,33, 23,25, 23,27, 23,32, 23,33, 23,29,
576/// 24,25, 24,27, 24,31, 25,31, 26,29, 26,33, 27,33,
577/// 28,31, 28,33, 29,32, 29,33, 30,32, 30,33, 31,32,
578/// 31,33, 32,33
579/// };
580/// igraph_vector_int_t edges =
581/// igraph_vector_int_view(edges_array, sizeof(edges_array) / sizeof(edges_array[0]));
582///
583/// /* Initialize the library. */
584/// igraph_setup();
585///
586/// igraph_create(&graph, &edges, 0, IGRAPH_UNDIRECTED);
587///
588/// igraph_vector_int_init(&result, 0);
589/// igraph_vector_init(&result_real, 0);
590///
591/// igraph_degree(&graph, &result, igraph_vss_all(), IGRAPH_ALL, IGRAPH_LOOPS);
592/// printf("Maximum degree is %10" IGRAPH_PRId ", vertex %2" IGRAPH_PRId ".\n",
593/// igraph_vector_int_max(&result),
594/// igraph_vector_int_which_max(&result));
595///
596/// igraph_closeness(&graph, &result_real, NULL, NULL, igraph_vss_all(),
597/// IGRAPH_ALL, /* weights= */ NULL, /* normalized= */ false);
598/// printf("Maximum closeness is %10g, vertex %2" IGRAPH_PRId ".\n",
599/// (double) igraph_vector_max(&result_real),
600/// igraph_vector_which_max(&result_real));
601///
602/// igraph_betweenness(&graph, /* weights= */ NULL, &result_real, igraph_vss_all(),
603/// IGRAPH_UNDIRECTED, /* normalized= */ false);
604/// printf("Maximum betweenness is %10g, vertex %2" IGRAPH_PRId ".\n",
605/// (double) igraph_vector_max(&result_real),
606/// igraph_vector_which_max(&result_real));
607///
608/// igraph_vector_int_destroy(&result);
609/// igraph_vector_destroy(&result_real);
610/// igraph_destroy(&graph);
611///
612/// return 0;
613/// }
614/// ```
615///
616/// # Rust translation
617///
618/// The body of this function, step by step: the result vectors are plain
619/// `Vec`s returned by the methods, and [`Maximum::of`] plays the role of
620/// `igraph_vector_max()` plus `igraph_vector_which_max()`.
621///
622/// ```
623/// use igraph::prelude::*;
624/// use igraph::tutorial::{Maximum, ZACHARY_KARATE_EDGES};
625///
626/// // `0` vertices: igraph infers the vertex count from the largest id.
627/// let graph = Graph::from_flat_edges(&ZACHARY_KARATE_EDGES, 0, false)?;
628///
629/// let degree = graph.degree(VertexSelector::All, NeighborMode::All, Loops::Twice)?;
630/// let max_degree = Maximum::of(°ree).unwrap();
631/// assert_eq!(max_degree, Maximum { value: 17, vertex: 33 });
632///
633/// let closeness = graph.closeness(VertexSelector::All, NeighborMode::All, None, false)?;
634/// let max_closeness = Maximum::of(&closeness).unwrap();
635/// assert_eq!(max_closeness.vertex, 0);
636/// assert!((max_closeness.value - 1.0 / 58.0).abs() < 1e-15);
637///
638/// let betweenness = graph.betweenness(None, VertexSelector::All, false, false)?;
639/// let max_betweenness = Maximum::of(&betweenness).unwrap();
640/// assert_eq!(max_betweenness.vertex, 0);
641/// assert!((max_betweenness.value - 231.0714285714286).abs() < 1e-9);
642///
643/// // ... which is what `example_3` returns.
644/// let res = igraph::tutorial::example_3()?;
645/// assert_eq!(res.degree, max_degree);
646/// assert_eq!(res.to_string(), "\
647/// Maximum degree is 17, vertex 33.
648/// Maximum closeness is 0.0172414, vertex 0.
649/// Maximum betweenness is 231.071, vertex 0.");
650/// # Ok::<(), igraph::Error>(())
651/// ```
652///
653/// # Errors
654/// Propagates the errors of the igraph calls (none are expected).
655pub fn example_3() -> Result<KarateCentralities> {
656 let graph = Graph::from_flat_edges(&ZACHARY_KARATE_EDGES, 0, false)?;
657
658 let degree = graph.degree(VertexSelector::All, NeighborMode::All, Loops::Twice)?;
659 let closeness = graph.closeness(VertexSelector::All, NeighborMode::All, None, false)?;
660 let betweenness = graph.betweenness(None, VertexSelector::All, false, false)?;
661
662 let max = |what: &str| {
663 Error::new(
664 ErrorKind::Internal,
665 format!("no {what} maximum in an empty graph"),
666 )
667 };
668 Ok(KarateCentralities {
669 degree: Maximum::of(°ree).ok_or_else(|| max("degree"))?,
670 closeness: Maximum::of(&closeness).ok_or_else(|| max("closeness"))?,
671 betweenness: Maximum::of(&betweenness).ok_or_else(|| max("betweenness"))?,
672 })
673}
674
675/// Formats a real number like C's `printf("%g", x)`: six significant digits,
676/// trailing zeros removed, scientific notation for exponents below -4 or
677/// above 5.
678fn format_g(x: f64) -> String {
679 if x.is_nan() {
680 return "nan".into();
681 }
682 if x.is_infinite() {
683 return if x > 0.0 { "inf" } else { "-inf" }.into();
684 }
685 if x == 0.0 {
686 return if x.is_sign_negative() { "-0" } else { "0" }.into();
687 }
688 const PRECISION: i32 = 6;
689 // Rounding to the significant digits first decides the exponent (e.g.
690 // 999999.5 becomes 1e+06).
691 let sci = format!("{:.*e}", (PRECISION - 1) as usize, x);
692 let (mantissa, exp) = sci.split_once('e').unwrap_or((&sci, "0"));
693 let exp: i32 = exp.parse().unwrap_or(0);
694 if (-4..PRECISION).contains(&exp) {
695 let fixed = format!("{:.*}", (PRECISION - 1 - exp) as usize, x);
696 strip_zeros(&fixed).to_string()
697 } else {
698 let sign = if exp < 0 { '-' } else { '+' };
699 format!("{}e{sign}{:02}", strip_zeros(mantissa), exp.abs())
700 }
701}
702
703/// Removes the trailing zeros of the fractional part (and a dangling `.`).
704fn strip_zeros(s: &str) -> &str {
705 if s.contains('.') {
706 s.trim_end_matches('0').trim_end_matches('.')
707 } else {
708 s
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::format_g;
715
716 #[test]
717 fn format_g_matches_printf() {
718 let cases = [
719 (2.0, "2"),
720 (23.0, "23"),
721 (15.016685205784205, "15.0167"),
722 (11.814163885799037, "11.8142"),
723 (0.017241379310344827, "0.0172414"),
724 (231.0714285714286, "231.071"),
725 (0.0001, "0.0001"),
726 (0.00001234, "1.234e-05"),
727 (123456.0, "123456"),
728 (1234567.0, "1.23457e+06"),
729 (999999.5, "1e+06"),
730 (-0.5, "-0.5"),
731 (1e100, "1e+100"),
732 (0.0, "0"),
733 (f64::NAN, "nan"),
734 (f64::NEG_INFINITY, "-inf"),
735 ];
736 for (x, expected) in cases {
737 assert_eq!(format_g(x), expected, "formatting {x}");
738 }
739 }
740}