1#![allow(clippy::cast_possible_truncation, clippy::needless_range_loop)]
48
49use crate::algorithms::constructors::adjacency::{AdjacencyMode, LoopsMode};
50use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
51
52pub fn weighted_adjacency(
87 matrix: &[&[f64]],
88 mode: AdjacencyMode,
89 loops: LoopsMode,
90) -> IgraphResult<(Graph, Vec<f64>)> {
91 let nrow = matrix.len();
92
93 if matrix.iter().any(|row| row.len() != nrow) {
95 return Err(IgraphError::InvalidArgument(
96 "weighted_adjacency: matrix must be square (every row has the same length as the row count)"
97 .into(),
98 ));
99 }
100
101 if nrow == 0 {
102 let directed = matches!(mode, AdjacencyMode::Directed);
103 return Ok((Graph::new(0, directed)?, Vec::new()));
104 }
105
106 let no_of_nodes = u32::try_from(nrow).map_err(|_| {
107 IgraphError::InvalidArgument("weighted_adjacency: vertex count exceeds u32::MAX".into())
108 })?;
109
110 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
111 let mut weights: Vec<f64> = Vec::new();
112
113 match mode {
114 AdjacencyMode::Directed => {
115 emit_directed(matrix, loops, &mut edges, &mut weights);
116 }
117 AdjacencyMode::Max => {
118 emit_max(matrix, loops, &mut edges, &mut weights);
119 }
120 AdjacencyMode::Undirected => {
121 emit_undirected(matrix, loops, &mut edges, &mut weights)?;
122 }
123 AdjacencyMode::Upper => {
124 emit_upper(matrix, loops, &mut edges, &mut weights);
125 }
126 AdjacencyMode::Lower => {
127 emit_lower(matrix, loops, &mut edges, &mut weights);
128 }
129 AdjacencyMode::Min => {
130 emit_min(matrix, loops, &mut edges, &mut weights);
131 }
132 AdjacencyMode::Plus => {
133 emit_plus(matrix, loops, &mut edges, &mut weights);
134 }
135 }
136
137 let directed = matches!(mode, AdjacencyMode::Directed);
138 let mut graph = Graph::new(no_of_nodes, directed)?;
139 graph.add_edges(edges)?;
140 Ok((graph, weights))
141}
142
143fn effective_loops(mode_collapses_twice: bool, loops: LoopsMode) -> LoopsMode {
146 if mode_collapses_twice && matches!(loops, LoopsMode::Twice) {
147 LoopsMode::Once
148 } else {
149 loops
150 }
151}
152
153fn adjust_loop_weight(weight: f64, loops: LoopsMode) -> f64 {
160 match loops {
161 LoopsMode::NoLoops => 0.0,
162 LoopsMode::Twice => weight / 2.0,
163 LoopsMode::Once => weight,
164 }
165}
166
167fn emit_directed(
168 matrix: &[&[f64]],
169 loops: LoopsMode,
170 edges: &mut Vec<(VertexId, VertexId)>,
171 weights: &mut Vec<f64>,
172) {
173 let n = matrix.len();
174 let loops = effective_loops(true, loops);
175
176 for j in 0..n {
179 for i in 0..n {
180 let mut m = matrix[i][j];
181 if m == 0.0 {
182 continue;
183 }
184 if i == j {
185 m = adjust_loop_weight(m, loops);
186 if m == 0.0 {
187 continue;
188 }
189 }
190 edges.push((i as VertexId, j as VertexId));
191 weights.push(m);
192 }
193 }
194}
195
196fn emit_plus(
197 matrix: &[&[f64]],
198 loops: LoopsMode,
199 edges: &mut Vec<(VertexId, VertexId)>,
200 weights: &mut Vec<f64>,
201) {
202 let n = matrix.len();
203 for i in 0..n {
204 if !matches!(loops, LoopsMode::NoLoops) {
205 let m = matrix[i][i];
206 if m != 0.0 {
207 let adj = adjust_loop_weight(m, loops);
208 edges.push((i as VertexId, i as VertexId));
209 weights.push(adj);
210 }
211 }
212 for j in (i + 1)..n {
213 let m = matrix[i][j] + matrix[j][i];
214 if m != 0.0 {
215 edges.push((i as VertexId, j as VertexId));
216 weights.push(m);
217 }
218 }
219 }
220}
221
222fn emit_max(
223 matrix: &[&[f64]],
224 loops: LoopsMode,
225 edges: &mut Vec<(VertexId, VertexId)>,
226 weights: &mut Vec<f64>,
227) {
228 let n = matrix.len();
229 for i in 0..n {
230 if !matches!(loops, LoopsMode::NoLoops) {
231 let m1 = matrix[i][i];
232 if m1 != 0.0 {
233 let adj = adjust_loop_weight(m1, loops);
234 edges.push((i as VertexId, i as VertexId));
235 weights.push(adj);
236 }
237 }
238 for j in (i + 1)..n {
239 let mut m1 = matrix[i][j];
240 let m2 = matrix[j][i];
241 if m1 < m2 || m2.is_nan() {
244 m1 = m2;
245 }
246 if m1 != 0.0 {
247 edges.push((i as VertexId, j as VertexId));
248 weights.push(m1);
249 }
250 }
251 }
252}
253
254fn emit_min(
255 matrix: &[&[f64]],
256 loops: LoopsMode,
257 edges: &mut Vec<(VertexId, VertexId)>,
258 weights: &mut Vec<f64>,
259) {
260 let n = matrix.len();
261 for i in 0..n {
262 if !matches!(loops, LoopsMode::NoLoops) {
263 let m1 = matrix[i][i];
264 if m1 != 0.0 {
265 let adj = adjust_loop_weight(m1, loops);
266 edges.push((i as VertexId, i as VertexId));
267 weights.push(adj);
268 }
269 }
270 for j in (i + 1)..n {
271 let mut m1 = matrix[i][j];
272 let m2 = matrix[j][i];
273 if m1 > m2 || m2.is_nan() {
276 m1 = m2;
277 }
278 if m1 != 0.0 {
279 edges.push((i as VertexId, j as VertexId));
280 weights.push(m1);
281 }
282 }
283 }
284}
285
286fn emit_undirected(
287 matrix: &[&[f64]],
288 loops: LoopsMode,
289 edges: &mut Vec<(VertexId, VertexId)>,
290 weights: &mut Vec<f64>,
291) -> IgraphResult<()> {
292 let n = matrix.len();
293 for i in 0..n {
298 if !matches!(loops, LoopsMode::NoLoops) {
299 let m = matrix[i][i];
300 if m != 0.0 {
301 let adj = adjust_loop_weight(m, loops);
302 edges.push((i as VertexId, i as VertexId));
303 weights.push(adj);
304 }
305 }
306 for j in 0..i {
307 let m1 = matrix[i][j];
308 let m2 = matrix[j][i];
309 #[allow(clippy::float_cmp)]
312 let asymmetric = m1 != m2 && !(m1.is_nan() && m2.is_nan());
313 if asymmetric {
314 return Err(IgraphError::InvalidArgument(
315 "weighted_adjacency: matrix must be symmetric for AdjacencyMode::Undirected"
316 .into(),
317 ));
318 }
319 if m1 != 0.0 {
320 edges.push((i as VertexId, j as VertexId));
324 weights.push(m1);
325 }
326 }
327 }
328 Ok(())
329}
330
331fn emit_upper(
332 matrix: &[&[f64]],
333 loops: LoopsMode,
334 edges: &mut Vec<(VertexId, VertexId)>,
335 weights: &mut Vec<f64>,
336) {
337 let n = matrix.len();
338 let loops = effective_loops(true, loops);
339 for j in 0..n {
342 for i in 0..j {
343 let m = matrix[i][j];
344 if m != 0.0 {
345 edges.push((i as VertexId, j as VertexId));
346 weights.push(m);
347 }
348 }
349 if !matches!(loops, LoopsMode::NoLoops) {
350 let m = matrix[j][j];
351 if m != 0.0 {
352 let adj = adjust_loop_weight(m, loops);
353 edges.push((j as VertexId, j as VertexId));
354 weights.push(adj);
355 }
356 }
357 }
358}
359
360fn emit_lower(
361 matrix: &[&[f64]],
362 loops: LoopsMode,
363 edges: &mut Vec<(VertexId, VertexId)>,
364 weights: &mut Vec<f64>,
365) {
366 let n = matrix.len();
367 let loops = effective_loops(true, loops);
368 for j in 0..n {
370 if !matches!(loops, LoopsMode::NoLoops) {
371 let m = matrix[j][j];
372 if m != 0.0 {
373 let adj = adjust_loop_weight(m, loops);
374 edges.push((j as VertexId, j as VertexId));
375 weights.push(adj);
376 }
377 }
378 for i in (j + 1)..n {
379 let m = matrix[i][j];
380 if m != 0.0 {
381 edges.push((i as VertexId, j as VertexId));
382 weights.push(m);
383 }
384 }
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 fn approx_eq(a: f64, b: f64) -> bool {
393 (a - b).abs() < 1e-12
394 }
395
396 fn edges_in_order(g: &Graph) -> Vec<(VertexId, VertexId)> {
397 let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
398 (0..m)
399 .map(|e| g.edge(e).expect("edge id in bounds"))
400 .collect()
401 }
402
403 const M3: &[&[f64]] = &[&[2.0, 0.5, 0.0], &[1.5, 0.0, 2.0], &[0.0, 2.5, 3.0]];
405
406 const M3_SYM: &[&[f64]] = &[&[2.0, 0.5, 0.0], &[0.5, 0.0, 2.0], &[0.0, 2.0, 3.0]];
408
409 #[test]
410 fn empty_matrix_yields_empty_graph() {
411 let m: &[&[f64]] = &[];
412 let (g, w) = weighted_adjacency(m, AdjacencyMode::Directed, LoopsMode::Once).unwrap();
413 assert_eq!(g.vcount(), 0);
414 assert_eq!(g.ecount(), 0);
415 assert!(g.is_directed());
416 assert!(w.is_empty());
417
418 let (g2, w2) = weighted_adjacency(m, AdjacencyMode::Undirected, LoopsMode::Once).unwrap();
419 assert_eq!(g2.vcount(), 0);
420 assert!(!g2.is_directed());
421 assert!(w2.is_empty());
422 }
423
424 #[test]
425 fn one_by_one_directed_no_loops() {
426 let m: &[&[f64]] = &[&[1.25]];
427 let (g, w) = weighted_adjacency(m, AdjacencyMode::Directed, LoopsMode::NoLoops).unwrap();
428 assert_eq!(g.vcount(), 1);
429 assert_eq!(g.ecount(), 0);
430 assert!(w.is_empty());
431 }
432
433 #[test]
434 fn one_by_one_directed_loops_once() {
435 let m: &[&[f64]] = &[&[1.25]];
436 let (g, w) = weighted_adjacency(m, AdjacencyMode::Directed, LoopsMode::Once).unwrap();
437 assert_eq!(edges_in_order(&g), vec![(0, 0)]);
438 assert_eq!(w.len(), 1);
439 assert!(approx_eq(w[0], 1.25));
440 }
441
442 #[test]
443 fn one_by_one_directed_loops_twice_collapses() {
444 let m: &[&[f64]] = &[&[1.25]];
446 let (g, w) = weighted_adjacency(m, AdjacencyMode::Directed, LoopsMode::Twice).unwrap();
447 assert_eq!(edges_in_order(&g), vec![(0, 0)]);
448 assert!(approx_eq(w[0], 1.25));
449 }
450
451 #[test]
452 fn directed_no_loops_emits_in_column_major_order() {
453 let (g, w) = weighted_adjacency(M3, AdjacencyMode::Directed, LoopsMode::NoLoops).unwrap();
454 assert_eq!(g.vcount(), 3);
455 assert_eq!(edges_in_order(&g), vec![(1, 0), (0, 1), (2, 1), (1, 2)]);
461 assert_eq!(w.len(), 4);
462 assert!(approx_eq(w[0], 1.5));
463 assert!(approx_eq(w[1], 0.5));
464 assert!(approx_eq(w[2], 2.5));
465 assert!(approx_eq(w[3], 2.0));
466 }
467
468 #[test]
469 fn directed_loops_once_includes_diagonal() {
470 let (g, w) = weighted_adjacency(M3, AdjacencyMode::Directed, LoopsMode::Once).unwrap();
471 assert_eq!(
475 edges_in_order(&g),
476 vec![(0, 0), (1, 0), (0, 1), (2, 1), (1, 2), (2, 2)]
477 );
478 assert_eq!(w, vec![2.0, 1.5, 0.5, 2.5, 2.0, 3.0]);
479 }
480
481 #[test]
482 fn undirected_lower_triangle_walk() {
483 let (g, w) =
484 weighted_adjacency(M3_SYM, AdjacencyMode::Undirected, LoopsMode::Once).unwrap();
485 assert_eq!(edges_in_order(&g), vec![(0, 0), (0, 1), (2, 2), (1, 2)]);
492 assert_eq!(w, vec![2.0, 0.5, 3.0, 2.0]);
493 assert!(!g.is_directed());
494 }
495
496 #[test]
497 fn undirected_rejects_asymmetric() {
498 let res = weighted_adjacency(M3, AdjacencyMode::Undirected, LoopsMode::NoLoops);
499 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
500 }
501
502 #[test]
503 fn undirected_accepts_nan_symmetric() {
504 let m: &[&[f64]] = &[
511 &[0.0, f64::NAN, 0.0],
512 &[f64::NAN, 0.0, 1.0],
513 &[0.0, 1.0, 0.0],
514 ];
515 let (g, w) = weighted_adjacency(m, AdjacencyMode::Undirected, LoopsMode::NoLoops).unwrap();
516 assert_eq!(g.ecount(), 2);
517 assert!(w[0].is_nan());
518 assert!(approx_eq(w[1], 1.0));
519 }
520
521 #[test]
522 fn undirected_rejects_one_sided_nan() {
523 let m: &[&[f64]] = &[&[0.0, f64::NAN, 0.0], &[1.0, 0.0, 0.0], &[0.0, 0.0, 0.0]];
525 let res = weighted_adjacency(m, AdjacencyMode::Undirected, LoopsMode::NoLoops);
526 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
527 }
528
529 #[test]
530 fn max_picks_larger_of_two_sides() {
531 let (g, w) = weighted_adjacency(M3, AdjacencyMode::Max, LoopsMode::NoLoops).unwrap();
532 assert_eq!(edges_in_order(&g), vec![(0, 1), (1, 2)]);
537 assert_eq!(w, vec![1.5, 2.5]);
538 }
539
540 #[test]
541 fn max_nan_propagates() {
542 let m: &[&[f64]] = &[&[0.0, 1.0], &[f64::NAN, 0.0]];
544 let (_, w) = weighted_adjacency(m, AdjacencyMode::Max, LoopsMode::NoLoops).unwrap();
545 assert_eq!(w.len(), 1);
549 assert!(w[0].is_nan());
550 }
551
552 #[test]
553 fn min_picks_smaller_of_two_sides() {
554 let (g, w) = weighted_adjacency(M3, AdjacencyMode::Min, LoopsMode::NoLoops).unwrap();
555 assert_eq!(edges_in_order(&g), vec![(0, 1), (1, 2)]);
559 assert_eq!(w, vec![0.5, 2.0]);
560 }
561
562 #[test]
563 fn min_nan_propagates() {
564 let m: &[&[f64]] = &[&[0.0, 1.0], &[f64::NAN, 0.0]];
565 let (_, w) = weighted_adjacency(m, AdjacencyMode::Min, LoopsMode::NoLoops).unwrap();
566 assert_eq!(w.len(), 1);
567 assert!(w[0].is_nan());
568 }
569
570 #[test]
571 fn plus_sums_both_sides() {
572 let (g, w) = weighted_adjacency(M3, AdjacencyMode::Plus, LoopsMode::NoLoops).unwrap();
573 assert_eq!(edges_in_order(&g), vec![(0, 1), (1, 2)]);
577 assert_eq!(w, vec![2.0, 4.5]);
578 }
579
580 #[test]
581 fn plus_loops_once_includes_diagonal() {
582 let (_, w) = weighted_adjacency(M3, AdjacencyMode::Plus, LoopsMode::Once).unwrap();
583 assert_eq!(w, vec![2.0, 2.0, 4.5, 3.0]);
587 }
588
589 #[test]
590 fn plus_loops_twice_halves_diagonal() {
591 let (_, w) = weighted_adjacency(M3, AdjacencyMode::Plus, LoopsMode::Twice).unwrap();
592 assert!(approx_eq(w[0], 1.0));
594 assert!(approx_eq(w[3], 1.5));
595 }
596
597 #[test]
598 fn upper_column_major_walk() {
599 let (g, w) = weighted_adjacency(M3, AdjacencyMode::Upper, LoopsMode::NoLoops).unwrap();
600 assert_eq!(edges_in_order(&g), vec![(0, 1), (1, 2)]);
604 assert_eq!(w, vec![0.5, 2.0]);
605 }
606
607 #[test]
608 fn upper_loops_twice_collapses_to_once() {
609 let (_, w) = weighted_adjacency(M3, AdjacencyMode::Upper, LoopsMode::Twice).unwrap();
611 assert_eq!(w, vec![2.0, 0.5, 2.0, 3.0]);
617 }
618
619 #[test]
620 fn lower_column_major_walk() {
621 let (g, w) = weighted_adjacency(M3, AdjacencyMode::Lower, LoopsMode::NoLoops).unwrap();
622 assert_eq!(edges_in_order(&g), vec![(0, 1), (1, 2)]);
628 assert_eq!(w, vec![1.5, 2.5]);
629 }
630
631 #[test]
632 fn lower_loops_twice_collapses_to_once() {
633 let (_, w) = weighted_adjacency(M3, AdjacencyMode::Lower, LoopsMode::Twice).unwrap();
634 assert_eq!(w, vec![2.0, 1.5, 2.5, 3.0]);
640 }
641
642 #[test]
643 fn negative_weights_pass_through() {
644 let m: &[&[f64]] = &[&[0.0, -1.0], &[-2.0, 0.0]];
645 let (g, w) = weighted_adjacency(m, AdjacencyMode::Directed, LoopsMode::NoLoops).unwrap();
646 assert_eq!(g.ecount(), 2);
647 assert_eq!(w, vec![-2.0, -1.0]);
649 }
650
651 #[test]
652 fn ragged_matrix_rejected() {
653 let m: &[&[f64]] = &[&[1.0, 2.0], &[3.0]];
654 let res = weighted_adjacency(m, AdjacencyMode::Directed, LoopsMode::NoLoops);
655 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
656 }
657
658 #[test]
659 fn weights_length_equals_ecount_always() {
660 for mode in [
661 AdjacencyMode::Directed,
662 AdjacencyMode::Max,
663 AdjacencyMode::Min,
664 AdjacencyMode::Plus,
665 AdjacencyMode::Upper,
666 AdjacencyMode::Lower,
667 ] {
668 for loops in [LoopsMode::NoLoops, LoopsMode::Once, LoopsMode::Twice] {
669 let (g, w) = weighted_adjacency(M3, mode, loops).unwrap();
670 assert_eq!(g.ecount(), w.len(), "mode={mode:?}, loops={loops:?}");
671 }
672 }
673 }
674}
675
676#[cfg(all(test, feature = "proptest-harness"))]
677mod proptests {
678 use super::*;
679 use proptest::prelude::*;
680
681 fn arb_dense_matrix(max_n: usize) -> impl Strategy<Value = Vec<Vec<f64>>> {
682 (0..=max_n).prop_flat_map(|n| {
683 let cell = (-4..=4i32).prop_map(|x| f64::from(x) * 0.5);
686 prop::collection::vec(prop::collection::vec(cell, n), n)
687 })
688 }
689
690 fn rows_view(matrix: &[Vec<f64>]) -> Vec<&[f64]> {
691 matrix.iter().map(Vec::as_slice).collect()
692 }
693
694 proptest! {
695 #[test]
696 fn vcount_equals_matrix_side(matrix in arb_dense_matrix(6)) {
697 let rows = rows_view(&matrix);
698 for mode in [
699 AdjacencyMode::Directed,
700 AdjacencyMode::Max,
701 AdjacencyMode::Min,
702 AdjacencyMode::Plus,
703 AdjacencyMode::Upper,
704 AdjacencyMode::Lower,
705 ] {
706 for loops in [LoopsMode::NoLoops, LoopsMode::Once, LoopsMode::Twice] {
707 let (g, _) = weighted_adjacency(&rows, mode, loops).unwrap();
708 prop_assert_eq!(g.vcount() as usize, matrix.len());
709 }
710 }
711 }
712
713 #[test]
714 fn weights_length_equals_ecount(matrix in arb_dense_matrix(6)) {
715 let rows = rows_view(&matrix);
716 for mode in [
717 AdjacencyMode::Directed,
718 AdjacencyMode::Max,
719 AdjacencyMode::Min,
720 AdjacencyMode::Plus,
721 AdjacencyMode::Upper,
722 AdjacencyMode::Lower,
723 ] {
724 for loops in [LoopsMode::NoLoops, LoopsMode::Once, LoopsMode::Twice] {
725 let (g, w) = weighted_adjacency(&rows, mode, loops).unwrap();
726 prop_assert_eq!(g.ecount(), w.len());
727 }
728 }
729 }
730
731 #[test]
732 fn upper_lower_twice_equals_once(matrix in arb_dense_matrix(5)) {
733 let rows = rows_view(&matrix);
736 for mode in [
737 AdjacencyMode::Upper,
738 AdjacencyMode::Lower,
739 AdjacencyMode::Directed,
740 ] {
741 let (g_once, w_once) =
742 weighted_adjacency(&rows, mode, LoopsMode::Once).unwrap();
743 let (g_twice, w_twice) =
744 weighted_adjacency(&rows, mode, LoopsMode::Twice).unwrap();
745 prop_assert_eq!(g_once.ecount(), g_twice.ecount());
746 prop_assert_eq!(w_once, w_twice);
747 }
748 }
749
750 #[test]
751 fn plus_twice_halves_diagonal(matrix in arb_dense_matrix(5)) {
752 let rows = rows_view(&matrix);
755 let (g_once, w_once) =
756 weighted_adjacency(&rows, AdjacencyMode::Plus, LoopsMode::Once).unwrap();
757 let (g_twice, w_twice) =
758 weighted_adjacency(&rows, AdjacencyMode::Plus, LoopsMode::Twice).unwrap();
759 prop_assert_eq!(g_once.ecount(), g_twice.ecount());
760 for k in 0..g_once.ecount() {
763 let (u, v) = g_once.edge(u32::try_from(k).unwrap()).unwrap();
764 if u == v {
765 let diff = w_once[k] - 2.0 * w_twice[k];
766 prop_assert!(diff.abs() < 1e-9);
767 } else {
768 let diff = w_once[k] - w_twice[k];
769 prop_assert!(diff.abs() < 1e-12);
770 }
771 }
772 }
773
774 #[test]
775 fn directed_no_loops_excludes_diagonal(matrix in arb_dense_matrix(5)) {
776 let rows = rows_view(&matrix);
777 let (g, _) =
778 weighted_adjacency(&rows, AdjacencyMode::Directed, LoopsMode::NoLoops).unwrap();
779 for k in 0..g.ecount() {
780 let (u, v) = g.edge(u32::try_from(k).unwrap()).unwrap();
781 prop_assert!(u != v, "self-loop emitted under NoLoops");
782 }
783 }
784
785 #[test]
786 fn max_min_bounded_by_off_diagonal_pairs(matrix in arb_dense_matrix(5)) {
787 let rows = rows_view(&matrix);
794 let n = matrix.len();
795 let cap = n * n.saturating_sub(1) / 2;
796 let (g_max, _) =
797 weighted_adjacency(&rows, AdjacencyMode::Max, LoopsMode::NoLoops).unwrap();
798 let (g_min, _) =
799 weighted_adjacency(&rows, AdjacencyMode::Min, LoopsMode::NoLoops).unwrap();
800 prop_assert!(g_max.ecount() <= cap);
801 prop_assert!(g_min.ecount() <= cap);
802 }
803 }
804}