1#![allow(clippy::cast_possible_truncation, clippy::needless_range_loop)]
35
36use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum AdjacencyMode {
43 Directed,
46 Undirected,
49 Max,
52 Min,
55 Plus,
58 Upper,
61 Lower,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum LoopsMode {
71 NoLoops,
73 Once,
75 Twice,
82}
83
84pub fn adjacency(matrix: &[&[i64]], mode: AdjacencyMode, loops: LoopsMode) -> IgraphResult<Graph> {
112 let nrow = matrix.len();
113
114 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 let directed = matches!(mode, AdjacencyMode::Directed);
126 return Graph::new(0, directed);
127 }
128
129 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
178fn 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
188fn 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 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 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 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 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 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 const M3: &[&[i64]] = &[&[4, 2, 0], &[3, 0, 4], &[0, 5, 6]];
360 const M3_SYM: &[&[i64]] = &[&[4, 2, 0], &[2, 0, 4], &[0, 4, 6]];
362 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 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 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 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 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 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 assert_eq!(g.ecount(), 2 + 4);
444 }
445
446 #[test]
447 fn three_by_three_undirected_loops_twice_halves_diagonal() {
448 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 assert_eq!(g.ecount(), 3 + 5);
472 }
473
474 #[test]
475 fn three_by_three_min_loops_once_matches_fixture() {
476 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 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 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 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 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 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 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 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 fn as_slice(m: &[Vec<i64>]) -> Vec<&[i64]> {
570 m.iter().map(|r| r.as_slice()).collect()
571 }
572
573 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 #[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 #[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 #[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 #[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 #[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 #[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}