Skip to main content

rust_igraph/algorithms/constructors/
adjacency.rs

1//! Dense integer adjacency-matrix constructor (ALGO-CN-029).
2//!
3//! Counterpart of `igraph_adjacency()` in
4//! `references/igraph/src/constructors/adjacency.c:335-386`.
5//!
6//! Builds a [`Graph`] from a square `n × n` integer matrix whose
7//! entries are **edge multiplicities** (non-negative). The shape of the
8//! resulting graph (directed / undirected, which triangle drives the
9//! edge count, how the diagonal becomes self-loops) is controlled by
10//! two enums:
11//!
12//! * [`AdjacencyMode`] — one of seven dispatch flavours
13//!   (`Directed`, `Undirected`, `Max`, `Min`, `Plus`, `Upper`, `Lower`).
14//! * [`LoopsMode`] — how to interpret the diagonal: `NoLoops` zeroes
15//!   it, `Once` treats `A(i,i)` as the loop count, `Twice` treats it
16//!   as twice the loop count (must be even).
17//!
18//! For consistency with upstream igraph the `Twice` request is silently
19//! collapsed to `Once` for the `Directed`, `Upper` and `Lower` modes —
20//! the matrix only stores one copy of each loop in those layouts.
21//!
22//! Matrix layout: `&[&[i64]]` — a slice of equal-length rows in
23//! row-major form. Every row must have the same length as the outer
24//! slice; ragged input is rejected. A `0 × 0` matrix produces an empty
25//! graph (matching the C semantics for an `IGRAPH_MATRIX_NULL` of
26//! shape `0 × 0`).
27//!
28//! Time complexity: `O(|V|² + |E|)`.
29
30// `i as VertexId` casts in the per-mode emitters are safe because the
31// caller validates `nrow ≤ u32::MAX` before any index is produced.
32// The double-loop traversals over the square matrix are clearer with
33// explicit `i, j` indices than with iterator+enumerate chains.
34#![allow(clippy::cast_possible_truncation, clippy::needless_range_loop)]
35
36use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
37
38/// How to interpret the input matrix as an adjacency matrix.
39///
40/// Matches `igraph_adjacency_t` from `include/igraph_constructors.h`.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum AdjacencyMode {
43    /// Directed graph; `A(i, j)` is the multiplicity of the arc
44    /// `i → j`.
45    Directed,
46    /// Undirected graph; `A(i, j)` is the multiplicity of the edge
47    /// `{i, j}`. The matrix must be symmetric.
48    Undirected,
49    /// Undirected; the edge multiplicity of `{i, j}` is
50    /// `max(A(i, j), A(j, i))`.
51    Max,
52    /// Undirected; the edge multiplicity of `{i, j}` is
53    /// `min(A(i, j), A(j, i))`.
54    Min,
55    /// Undirected; the edge multiplicity of `{i, j}` is
56    /// `A(i, j) + A(j, i)`.
57    Plus,
58    /// Undirected; only entries with `i < j` (the strict upper
59    /// triangle) plus the diagonal contribute edges.
60    Upper,
61    /// Undirected; only entries with `i > j` (the strict lower
62    /// triangle) plus the diagonal contribute edges.
63    Lower,
64}
65
66/// How to convert diagonal entries into self-loops.
67///
68/// Matches `igraph_loops_t` from `include/igraph_constructors.h`.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum LoopsMode {
71    /// Ignore the diagonal; the result contains no self-loops.
72    NoLoops,
73    /// `A(i, i)` is the number of self-loops at vertex `i`.
74    Once,
75    /// `A(i, i)` is **twice** the number of self-loops at vertex `i`
76    /// (must be even; odd values are rejected with
77    /// [`IgraphError::InvalidArgument`]). Collapses to [`Once`] for
78    /// the `Directed`, `Upper` and `Lower` modes per upstream.
79    ///
80    /// [`Once`]: LoopsMode::Once
81    Twice,
82}
83
84/// Build a graph from a dense integer adjacency matrix.
85///
86/// Matches `igraph_adjacency()` semantics exactly. The matrix is
87/// passed as `&[&[i64]]` — outer slice indexed by row, inner slice by
88/// column. The matrix must be square (each row length equal to the
89/// outer length); negative entries are rejected.
90///
91/// # Errors
92///
93/// * [`IgraphError::InvalidArgument`] — the matrix is non-square (a
94///   row's length differs from the row count), contains a negative
95///   entry, or (`Undirected` only) is not symmetric, or
96///   (`LoopsMode::Twice` with a symmetric mode) has an odd diagonal
97///   entry, or the vertex count exceeds [`u32::MAX`].
98///
99/// # Examples
100///
101/// ```
102/// use rust_igraph::{adjacency, AdjacencyMode, LoopsMode};
103///
104/// // Directed K₃ with no loops.
105/// let m: &[&[i64]] = &[&[0, 1, 1], &[1, 0, 1], &[1, 1, 0]];
106/// let g = adjacency(m, AdjacencyMode::Directed, LoopsMode::NoLoops).unwrap();
107/// assert_eq!(g.vcount(), 3);
108/// assert_eq!(g.ecount(), 6);
109/// assert!(g.is_directed());
110/// ```
111pub fn adjacency(matrix: &[&[i64]], mode: AdjacencyMode, loops: LoopsMode) -> IgraphResult<Graph> {
112    let nrow = matrix.len();
113
114    // Square check — every row must have exactly `nrow` columns.
115    if matrix.iter().any(|row| row.len() != nrow) {
116        return Err(IgraphError::InvalidArgument(
117            "adjacency: matrix must be square (every row has the same length as the row count)"
118                .into(),
119        ));
120    }
121
122    if nrow == 0 {
123        // Empty graph — matches C's behaviour for a 0×0 matrix. Mode
124        // still selects directedness.
125        let directed = matches!(mode, AdjacencyMode::Directed);
126        return Graph::new(0, directed);
127    }
128
129    // Non-negative check.
130    for row in matrix {
131        if let Some(&min) = row.iter().min() {
132            if min < 0 {
133                return Err(IgraphError::InvalidArgument(format!(
134                    "adjacency: edge counts must be non-negative, found {min}"
135                )));
136            }
137        }
138    }
139
140    let no_of_nodes = u32::try_from(nrow).map_err(|_| {
141        IgraphError::InvalidArgument("adjacency: vertex count exceeds u32::MAX".into())
142    })?;
143
144    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
145
146    match mode {
147        AdjacencyMode::Directed | AdjacencyMode::Plus => {
148            emit_directed_or_plus(matrix, mode, loops, &mut edges)?;
149        }
150        AdjacencyMode::Max => {
151            emit_max(matrix, loops, &mut edges)?;
152        }
153        AdjacencyMode::Undirected => {
154            if !is_symmetric(matrix) {
155                return Err(IgraphError::InvalidArgument(
156                    "adjacency: matrix must be symmetric for AdjacencyMode::Undirected".into(),
157                ));
158            }
159            emit_max(matrix, loops, &mut edges)?;
160        }
161        AdjacencyMode::Upper => {
162            emit_upper(matrix, loops, &mut edges)?;
163        }
164        AdjacencyMode::Lower => {
165            emit_lower(matrix, loops, &mut edges)?;
166        }
167        AdjacencyMode::Min => {
168            emit_min(matrix, loops, &mut edges)?;
169        }
170    }
171
172    let directed = matches!(mode, AdjacencyMode::Directed);
173    let mut graph = Graph::new(no_of_nodes, directed)?;
174    graph.add_edges(edges)?;
175    Ok(graph)
176}
177
178/// Per-mode loop-handling collapse: `Directed`, `Upper`, `Lower` all
179/// flatten `Twice` to `Once` (matches upstream lines 84, 168, 209).
180fn effective_loops(mode_collapses_twice: bool, loops: LoopsMode) -> LoopsMode {
181    if mode_collapses_twice && matches!(loops, LoopsMode::Twice) {
182        LoopsMode::Once
183    } else {
184        loops
185    }
186}
187
188/// Diagonal entry → number of self-loops to emit at that vertex.
189/// Returns an error if `Twice` is requested and the entry is odd.
190fn adjust_loop_edge_count(count: i64, loops: LoopsMode) -> IgraphResult<i64> {
191    match loops {
192        LoopsMode::NoLoops => Ok(0),
193        LoopsMode::Twice => {
194            if count & 1 == 1 {
195                Err(IgraphError::InvalidArgument(
196                    "adjacency: odd number found on the diagonal under LoopsMode::Twice".into(),
197                ))
198            } else {
199                Ok(count >> 1)
200            }
201        }
202        LoopsMode::Once => Ok(count),
203    }
204}
205
206fn push_multi(edges: &mut Vec<(VertexId, VertexId)>, from: VertexId, to: VertexId, count: i64) {
207    // `count` is guaranteed non-negative at the call site (checked by
208    // adjust_loop_edge_count or by the non-negativity validation).
209    for _ in 0..count {
210        edges.push((from, to));
211    }
212}
213
214fn emit_directed_or_plus(
215    matrix: &[&[i64]],
216    mode: AdjacencyMode,
217    loops: LoopsMode,
218    edges: &mut Vec<(VertexId, VertexId)>,
219) -> IgraphResult<()> {
220    let n = matrix.len();
221    let collapse = matches!(mode, AdjacencyMode::Directed);
222    let loops = effective_loops(collapse, loops);
223
224    // Upstream walks j (column) outer, i (row) inner — matching
225    // column-major storage. We mirror that order so the resulting
226    // edge sequence matches the .out fixtures byte-for-byte.
227    for j in 0..n {
228        for (i, row) in matrix.iter().enumerate() {
229            let mut m = row[j];
230            if i == j {
231                m = adjust_loop_edge_count(m, loops)?;
232            }
233            // Safe casts: `n ≤ u32::MAX` enforced by caller.
234            let from = i as VertexId;
235            let to = j as VertexId;
236            push_multi(edges, from, to, m);
237        }
238    }
239    Ok(())
240}
241
242fn emit_max(
243    matrix: &[&[i64]],
244    loops: LoopsMode,
245    edges: &mut Vec<(VertexId, VertexId)>,
246) -> IgraphResult<()> {
247    let n = matrix.len();
248    for i in 0..n {
249        let m1 = matrix[i][i];
250        if m1 != 0 {
251            let count = adjust_loop_edge_count(m1, loops)?;
252            push_multi(edges, i as VertexId, i as VertexId, count);
253        }
254        for j in (i + 1)..n {
255            let a = matrix[i][j];
256            let b = matrix[j][i];
257            let m = a.max(b);
258            push_multi(edges, i as VertexId, j as VertexId, m);
259        }
260    }
261    Ok(())
262}
263
264fn emit_min(
265    matrix: &[&[i64]],
266    loops: LoopsMode,
267    edges: &mut Vec<(VertexId, VertexId)>,
268) -> IgraphResult<()> {
269    let n = matrix.len();
270    for i in 0..n {
271        let m1 = matrix[i][i];
272        if m1 != 0 {
273            let count = adjust_loop_edge_count(m1, loops)?;
274            push_multi(edges, i as VertexId, i as VertexId, count);
275        }
276        for j in (i + 1)..n {
277            let a = matrix[i][j];
278            let b = matrix[j][i];
279            let m = a.min(b);
280            push_multi(edges, i as VertexId, j as VertexId, m);
281        }
282    }
283    Ok(())
284}
285
286fn emit_upper(
287    matrix: &[&[i64]],
288    loops: LoopsMode,
289    edges: &mut Vec<(VertexId, VertexId)>,
290) -> IgraphResult<()> {
291    let n = matrix.len();
292    let loops = effective_loops(true, loops);
293    // Outer over column j, inner over rows i < j; loops appended once
294    // per outer step. Matches upstream lines 172-190.
295    for j in 0..n {
296        for i in 0..j {
297            let m = matrix[i][j];
298            push_multi(edges, i as VertexId, j as VertexId, m);
299        }
300        let diag = matrix[j][j];
301        if diag != 0 {
302            let count = adjust_loop_edge_count(diag, loops)?;
303            push_multi(edges, j as VertexId, j as VertexId, count);
304        }
305    }
306    Ok(())
307}
308
309fn emit_lower(
310    matrix: &[&[i64]],
311    loops: LoopsMode,
312    edges: &mut Vec<(VertexId, VertexId)>,
313) -> IgraphResult<()> {
314    let n = matrix.len();
315    let loops = effective_loops(true, loops);
316    // Outer over column j, loops first then rows i > j. Matches
317    // upstream lines 213-231.
318    for j in 0..n {
319        let diag = matrix[j][j];
320        if diag != 0 {
321            let count = adjust_loop_edge_count(diag, loops)?;
322            push_multi(edges, j as VertexId, j as VertexId, count);
323        }
324        for i in (j + 1)..n {
325            let m = matrix[i][j];
326            push_multi(edges, i as VertexId, j as VertexId, m);
327        }
328    }
329    Ok(())
330}
331
332fn is_symmetric(matrix: &[&[i64]]) -> bool {
333    let n = matrix.len();
334    for i in 0..n {
335        for j in (i + 1)..n {
336            if matrix[i][j] != matrix[j][i] {
337                return false;
338            }
339        }
340    }
341    true
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    fn edges_in_order(g: &Graph) -> Vec<(VertexId, VertexId)> {
349        let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
350        (0..m)
351            .map(|e| g.edge(e).expect("edge id in bounds"))
352            .collect()
353    }
354
355    /// Canonical 3×3 from `tests/unit/igraph_adjacency.c`:
356    ///   `{4, 2, 0,
357    ///     3, 0, 4,
358    ///     0, 5, 6}`.
359    const M3: &[&[i64]] = &[&[4, 2, 0], &[3, 0, 4], &[0, 5, 6]];
360    /// Symmetric 3×3 used by the `IGRAPH_ADJ_UNDIRECTED` test cases.
361    const M3_SYM: &[&[i64]] = &[&[4, 2, 0], &[2, 0, 4], &[0, 4, 6]];
362    /// 3×3 used by the `IGRAPH_ADJ_MIN, NO_LOOPS` test case.
363    const M3_MIN_NL: &[&[i64]] = &[&[4, 2, 0], &[3, 0, 5], &[0, 4, 6]];
364
365    #[test]
366    fn empty_matrix_yields_empty_graph() {
367        let m: &[&[i64]] = &[];
368        let g = adjacency(m, AdjacencyMode::Directed, LoopsMode::Once).unwrap();
369        assert_eq!(g.vcount(), 0);
370        assert_eq!(g.ecount(), 0);
371        assert!(g.is_directed());
372
373        // Same matrix but undirected → still empty undirected graph.
374        let g2 = adjacency(m, AdjacencyMode::Undirected, LoopsMode::Once).unwrap();
375        assert_eq!(g2.vcount(), 0);
376        assert!(!g2.is_directed());
377    }
378
379    #[test]
380    fn one_by_one_directed_no_loops() {
381        let m: &[&[i64]] = &[&[1]];
382        let g = adjacency(m, AdjacencyMode::Directed, LoopsMode::NoLoops).unwrap();
383        assert_eq!(g.vcount(), 1);
384        assert_eq!(g.ecount(), 0);
385    }
386
387    #[test]
388    fn one_by_one_directed_loops_once() {
389        let m: &[&[i64]] = &[&[1]];
390        let g = adjacency(m, AdjacencyMode::Directed, LoopsMode::Once).unwrap();
391        assert_eq!(g.vcount(), 1);
392        assert_eq!(edges_in_order(&g), vec![(0, 0)]);
393    }
394
395    #[test]
396    fn one_by_one_directed_loops_twice_collapses_to_once() {
397        // Upstream: LOOPS_TWICE treated as LOOPS_ONCE for DIRECTED,
398        // so `A(0,0) = 1` becomes a single self-loop (NOT halved).
399        let m: &[&[i64]] = &[&[1]];
400        let g = adjacency(m, AdjacencyMode::Directed, LoopsMode::Twice).unwrap();
401        assert_eq!(edges_in_order(&g), vec![(0, 0)]);
402    }
403
404    #[test]
405    fn three_by_three_directed_no_loops_matches_fixture() {
406        let g = adjacency(M3, AdjacencyMode::Directed, LoopsMode::NoLoops).unwrap();
407        assert_eq!(g.vcount(), 3);
408        // Expected edge multi-set (from .out file, lines 31-45):
409        // 0->1 ×2, 1->0 ×3, 1->2 ×4, 2->1 ×5.
410        assert_eq!(g.ecount(), 14);
411        let edges = edges_in_order(&g);
412        let count = |from: u32, to: u32| edges.iter().filter(|&&e| e == (from, to)).count();
413        assert_eq!(count(0, 1), 2);
414        assert_eq!(count(1, 0), 3);
415        assert_eq!(count(1, 2), 4);
416        assert_eq!(count(2, 1), 5);
417    }
418
419    #[test]
420    fn three_by_three_directed_loops_once_matches_fixture() {
421        let g = adjacency(M3, AdjacencyMode::Directed, LoopsMode::Once).unwrap();
422        // Loops: 0->0 ×4, 2->2 ×6 (diagonal entries 4, 0, 6).
423        assert_eq!(g.ecount(), 14 + 4 + 6);
424        let edges = edges_in_order(&g);
425        let count = |from: u32, to: u32| edges.iter().filter(|&&e| e == (from, to)).count();
426        assert_eq!(count(0, 0), 4);
427        assert_eq!(count(2, 2), 6);
428    }
429
430    #[test]
431    fn three_by_three_directed_loops_twice_equals_loops_once() {
432        // Collapse behaviour: LOOPS_TWICE → LOOPS_ONCE for DIRECTED.
433        let g_once = adjacency(M3, AdjacencyMode::Directed, LoopsMode::Once).unwrap();
434        let g_twice = adjacency(M3, AdjacencyMode::Directed, LoopsMode::Twice).unwrap();
435        assert_eq!(edges_in_order(&g_once), edges_in_order(&g_twice));
436    }
437
438    #[test]
439    fn three_by_three_undirected_no_loops() {
440        let g = adjacency(M3_SYM, AdjacencyMode::Undirected, LoopsMode::NoLoops).unwrap();
441        assert!(!g.is_directed());
442        // Off-diagonals: 0-1 ×2, 1-2 ×4.
443        assert_eq!(g.ecount(), 2 + 4);
444    }
445
446    #[test]
447    fn three_by_three_undirected_loops_twice_halves_diagonal() {
448        // Diagonal {4, 0, 6} under TWICE → 2, 0, 3 loops.
449        let g = adjacency(M3_SYM, AdjacencyMode::Undirected, LoopsMode::Twice).unwrap();
450        let edges = edges_in_order(&g);
451        let count = |from: u32, to: u32| edges.iter().filter(|&&e| e == (from, to)).count();
452        assert_eq!(count(0, 0), 2);
453        assert_eq!(count(2, 2), 3);
454    }
455
456    #[test]
457    fn three_by_three_undirected_rejects_nonsymmetric() {
458        let err = adjacency(M3, AdjacencyMode::Undirected, LoopsMode::Once)
459            .expect_err("non-symmetric must error");
460        match err {
461            IgraphError::InvalidArgument(_) => {}
462            other => panic!("expected InvalidArgument, got {other:?}"),
463        }
464    }
465
466    #[test]
467    fn three_by_three_max_no_loops_matches_fixture() {
468        let g = adjacency(M3, AdjacencyMode::Max, LoopsMode::NoLoops).unwrap();
469        // For pair (i,j) edge count = max(M[i,j], M[j,i]):
470        // (0,1): max(2, 3) = 3; (0,2): max(0, 0) = 0; (1,2): max(4, 5) = 5.
471        assert_eq!(g.ecount(), 3 + 5);
472    }
473
474    #[test]
475    fn three_by_three_min_loops_once_matches_fixture() {
476        // .out shows the M3 fixture under MIN+LOOPS_ONCE:
477        // diag {4, 0, 6} → 4 + 6 = 10 loops.
478        // off-diag (0,1) min(2,3)=2, (0,2) min(0,0)=0, (1,2) min(4,5)=4.
479        let g = adjacency(M3, AdjacencyMode::Min, LoopsMode::Once).unwrap();
480        assert_eq!(g.ecount(), 4 + 6 + 2 + 4);
481    }
482
483    #[test]
484    fn three_by_three_min_no_loops_matches_fixture() {
485        // .out fixture uses a different matrix for MIN/NO_LOOPS:
486        // pair (0,1): min(2,3)=2; (1,2): min(5,4)=4; (0,2): min(0,0)=0.
487        let g = adjacency(M3_MIN_NL, AdjacencyMode::Min, LoopsMode::NoLoops).unwrap();
488        assert_eq!(g.ecount(), 2 + 4);
489    }
490
491    #[test]
492    fn three_by_three_plus_no_loops_matches_fixture() {
493        // PLUS: A(i,j)+A(j,i) per pair. Diagonal ignored under NoLoops.
494        // (0,1): 2+3=5; (0,2): 0+0=0; (1,2): 4+5=9.
495        let g = adjacency(M3, AdjacencyMode::Plus, LoopsMode::NoLoops).unwrap();
496        assert_eq!(g.ecount(), 5 + 9);
497    }
498
499    #[test]
500    fn three_by_three_upper_no_loops_matches_fixture() {
501        // Upper triangle (i<j): (0,1)=2, (0,2)=0, (1,2)=4.
502        let g = adjacency(M3, AdjacencyMode::Upper, LoopsMode::NoLoops).unwrap();
503        assert_eq!(g.ecount(), 2 + 4);
504    }
505
506    #[test]
507    fn three_by_three_upper_loops_twice_collapses_to_once() {
508        // For UPPER, Twice collapses to Once → diag {4, 0, 6} = 10 loops.
509        let g = adjacency(M3, AdjacencyMode::Upper, LoopsMode::Twice).unwrap();
510        assert_eq!(g.ecount(), 2 + 4 + 4 + 6);
511    }
512
513    #[test]
514    fn three_by_three_lower_no_loops_matches_fixture() {
515        // Lower triangle (i>j): (1,0)=3, (2,0)=0, (2,1)=5.
516        let g = adjacency(M3, AdjacencyMode::Lower, LoopsMode::NoLoops).unwrap();
517        assert_eq!(g.ecount(), 3 + 5);
518    }
519
520    #[test]
521    fn rejects_non_square_matrix() {
522        let m: &[&[i64]] = &[&[1, 2, 0]];
523        let err = adjacency(m, AdjacencyMode::Directed, LoopsMode::NoLoops)
524            .expect_err("non-square must error");
525        match err {
526            IgraphError::InvalidArgument(_) => {}
527            other => panic!("expected InvalidArgument, got {other:?}"),
528        }
529    }
530
531    #[test]
532    fn rejects_negative_entry() {
533        let m: &[&[i64]] = &[&[1, 2, 0], &[-3, 0, 4], &[0, 5, 6]];
534        let err = adjacency(m, AdjacencyMode::Directed, LoopsMode::NoLoops)
535            .expect_err("negative entry must error");
536        match err {
537            IgraphError::InvalidArgument(_) => {}
538            other => panic!("expected InvalidArgument, got {other:?}"),
539        }
540    }
541
542    #[test]
543    fn rejects_odd_diagonal_under_loops_twice() {
544        // Under TWICE for a symmetric mode (UNDIRECTED), diagonal must
545        // be even. Here {1, 0, 6} is symmetric+positive but A(0,0)=1
546        // is odd.
547        let m: &[&[i64]] = &[&[1, 2, 0], &[2, 0, 4], &[0, 4, 6]];
548        let err = adjacency(m, AdjacencyMode::Undirected, LoopsMode::Twice)
549            .expect_err("odd diagonal under Twice must error");
550        match err {
551            IgraphError::InvalidArgument(_) => {}
552            other => panic!("expected InvalidArgument, got {other:?}"),
553        }
554    }
555}
556
557#[cfg(all(test, feature = "proptest-harness"))]
558mod proptests {
559    use super::*;
560    use proptest::prelude::*;
561
562    /// Generate a random square non-negative matrix `n × n` with
563    /// `1 ≤ n ≤ 6` and entries in `[0, 4]`.
564    fn small_square_matrix() -> impl Strategy<Value = Vec<Vec<i64>>> {
565        (1usize..=6).prop_flat_map(|n| prop::collection::vec(prop::collection::vec(0i64..=4, n), n))
566    }
567
568    /// View a `Vec<Vec<i64>>` as `&[&[i64]]`.
569    fn as_slice(m: &[Vec<i64>]) -> Vec<&[i64]> {
570        m.iter().map(|r| r.as_slice()).collect()
571    }
572
573    /// Symmetrise a matrix into `(A + A^T) / 2 — wait, we need integer.
574    /// Just enforce `A(j,i) = A(i,j)` from the upper triangle.
575    fn symmetrise(m: &mut [Vec<i64>]) {
576        let n = m.len();
577        for i in 0..n {
578            for j in 0..i {
579                m[i][j] = m[j][i];
580            }
581        }
582    }
583
584    proptest! {
585        #![proptest_config(ProptestConfig::with_cases(48))]
586
587        /// DIRECTED with NoLoops: edge count equals sum of off-diagonal entries.
588        #[test]
589        fn directed_no_loops_edge_count_matches(m in small_square_matrix()) {
590            let view = as_slice(&m);
591            let g = adjacency(&view, AdjacencyMode::Directed, LoopsMode::NoLoops).unwrap();
592            let expected: i64 = (0..m.len())
593                .flat_map(|i| (0..m.len()).map(move |j| (i, j)))
594                .filter(|(i, j)| i != j)
595                .map(|(i, j)| m[i][j])
596                .sum();
597            prop_assert_eq!(g.ecount() as i64, expected);
598            prop_assert!(g.is_directed());
599        }
600
601        /// PLUS with NoLoops: edge count equals sum over i<j of A(i,j)+A(j,i).
602        #[test]
603        fn plus_no_loops_edge_count_matches(m in small_square_matrix()) {
604            let view = as_slice(&m);
605            let g = adjacency(&view, AdjacencyMode::Plus, LoopsMode::NoLoops).unwrap();
606            let n = m.len();
607            let expected: i64 = (0..n)
608                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
609                .map(|(i, j)| m[i][j] + m[j][i])
610                .sum();
611            prop_assert_eq!(g.ecount() as i64, expected);
612            prop_assert!(!g.is_directed());
613        }
614
615        /// MAX with NoLoops: edge count equals sum over i<j of max(A(i,j), A(j,i)).
616        #[test]
617        fn max_no_loops_edge_count_matches(m in small_square_matrix()) {
618            let view = as_slice(&m);
619            let g = adjacency(&view, AdjacencyMode::Max, LoopsMode::NoLoops).unwrap();
620            let n = m.len();
621            let expected: i64 = (0..n)
622                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
623                .map(|(i, j)| m[i][j].max(m[j][i]))
624                .sum();
625            prop_assert_eq!(g.ecount() as i64, expected);
626        }
627
628        /// MIN with NoLoops: edge count equals sum over i<j of min(A(i,j), A(j,i)).
629        #[test]
630        fn min_no_loops_edge_count_matches(m in small_square_matrix()) {
631            let view = as_slice(&m);
632            let g = adjacency(&view, AdjacencyMode::Min, LoopsMode::NoLoops).unwrap();
633            let n = m.len();
634            let expected: i64 = (0..n)
635                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
636                .map(|(i, j)| m[i][j].min(m[j][i]))
637                .sum();
638            prop_assert_eq!(g.ecount() as i64, expected);
639        }
640
641        /// UNDIRECTED on a symmetric matrix == MAX on the same matrix
642        /// (in edge count and edge multi-set, since both go through emit_max).
643        #[test]
644        fn undirected_equals_max_on_symmetric(mut m in small_square_matrix()) {
645            symmetrise(&mut m);
646            let view = as_slice(&m);
647            let g_und = adjacency(&view, AdjacencyMode::Undirected, LoopsMode::Once).unwrap();
648            let g_max = adjacency(&view, AdjacencyMode::Max, LoopsMode::Once).unwrap();
649            prop_assert_eq!(g_und.ecount(), g_max.ecount());
650        }
651
652        /// UPPER + LOWER edges on the same matrix together cover every
653        /// non-diagonal A(i,j): edge_count(upper, NoLoops) = sum over
654        /// i<j of A(i,j); edge_count(lower, NoLoops) = sum over i>j.
655        #[test]
656        fn upper_lower_partition_off_diagonal(m in small_square_matrix()) {
657            let view = as_slice(&m);
658            let g_up = adjacency(&view, AdjacencyMode::Upper, LoopsMode::NoLoops).unwrap();
659            let g_lo = adjacency(&view, AdjacencyMode::Lower, LoopsMode::NoLoops).unwrap();
660            let n = m.len();
661            let total_offdiag: i64 = (0..n)
662                .flat_map(|i| (0..n).map(move |j| (i, j)))
663                .filter(|(i, j)| i != j)
664                .map(|(i, j)| m[i][j])
665                .sum();
666            prop_assert_eq!((g_up.ecount() + g_lo.ecount()) as i64, total_offdiag);
667        }
668    }
669}