1use faer::perm::Perm;
32use faer::sparse::{SparseColMat, SymbolicSparseColMatRef};
33
34use super::matching::{Mc64Job, mc64_matching};
35use crate::error::SparseError;
36
37pub struct MatchOrderResult {
63 pub ordering: Perm<usize>,
66
67 pub scaling: Vec<f64>,
70
71 pub matched: usize,
74
75 pub condensed_dim: usize,
78
79 pub singletons: usize,
81
82 pub two_cycles: usize,
84}
85
86pub fn match_order_metis(
119 matrix: &SparseColMat<usize, f64>,
120) -> Result<MatchOrderResult, SparseError> {
121 let n = matrix.nrows();
122
123 if n <= 1 {
125 let scaling = if n == 1 { vec![1.0] } else { vec![] };
126 return Ok(MatchOrderResult {
127 ordering: identity_perm(n),
128 scaling,
129 matched: n,
130 condensed_dim: n,
131 singletons: n,
132 two_cycles: 0,
133 });
134 }
135
136 let mc64_result = mc64_matching(matrix, Mc64Job::MaximumProduct)?;
138
139 let matching_fwd = mc64_result.matching.as_ref().arrays().0;
141 let decomp = split_matching_cycles(matching_fwd, &mc64_result.is_matched, n);
142
143 let (mut xadj, mut adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp)?;
145
146 let cdim = decomp.condensed_dim;
148 let condensed_order = if cdim <= 1 || adjncy.is_empty() {
149 (0..cdim as i32).collect::<Vec<_>>()
151 } else {
152 if cdim > i32::MAX as usize {
153 return Err(SparseError::InvalidInput {
154 reason: format!(
155 "Condensed dimension {} exceeds i32::MAX; METIS requires 32-bit indices",
156 cdim
157 ),
158 });
159 }
160
161 let (_perm, iperm) = call_metis_node_nd(cdim, &mut xadj, &mut adjncy)?;
163 iperm
164 };
165
166 let ordering = expand_ordering(&condensed_order, &decomp, n);
168
169 Ok(MatchOrderResult {
170 ordering,
171 scaling: mc64_result.scaling,
172 matched: mc64_result.matched,
173 condensed_dim: decomp.condensed_dim,
174 singletons: decomp.singletons,
175 two_cycles: decomp.two_cycles,
176 })
177}
178
179pub fn metis_ordering(
232 matrix: SymbolicSparseColMatRef<'_, usize>,
233) -> Result<Perm<usize>, SparseError> {
234 let n = matrix.nrows();
235
236 if n > i32::MAX as usize {
238 return Err(SparseError::InvalidInput {
239 reason: format!(
240 "Matrix dimension {} exceeds i32::MAX ({}); METIS requires 32-bit indices",
241 n,
242 i32::MAX
243 ),
244 });
245 }
246
247 if n <= 1 {
249 return Ok(identity_perm(n));
250 }
251
252 let (mut xadj, mut adjncy) = extract_adjacency(matrix)?;
254
255 if adjncy.is_empty() {
257 return Ok(identity_perm(n));
258 }
259
260 let (perm_i32, iperm_i32) = call_metis_node_nd(n, &mut xadj, &mut adjncy)?;
264
265 let fwd: Box<[usize]> = perm_i32.iter().map(|&v| v as usize).collect();
266 let inv: Box<[usize]> = iperm_i32.iter().map(|&v| v as usize).collect();
267
268 Ok(Perm::new_checked(fwd, inv, n))
269}
270
271struct CycleDecomposition {
279 partner: Vec<isize>,
284
285 old_to_new: Vec<usize>,
289
290 new_to_old: Vec<usize>,
294
295 condensed_dim: usize,
297
298 singletons: usize,
300
301 two_cycles: usize,
303}
304
305fn split_matching_cycles(
318 matching_fwd: &[usize],
319 is_matched: &[bool],
320 n: usize,
321) -> CycleDecomposition {
322 let mut partner = vec![isize::MIN; n];
323 let mut old_to_new = vec![0usize; n];
324 let mut new_to_old = Vec::new();
325 let mut visited = vec![false; n];
326 let mut singletons = 0usize;
327 let mut two_cycles = 0usize;
328
329 for i in 0..n {
331 if !is_matched[i] {
332 partner[i] = -2;
333 visited[i] = true;
334 }
335 }
336
337 let mut condensed_idx = 0usize;
341 for i in 0..n {
342 if visited[i] && partner[i] != -2 {
343 continue;
345 }
346
347 if partner[i] == -2 {
349 old_to_new[i] = condensed_idx;
350 new_to_old.push(i);
351 condensed_idx += 1;
352 singletons += 1;
353 continue;
354 }
355
356 if matching_fwd[i] == i {
358 partner[i] = -1;
359 old_to_new[i] = condensed_idx;
360 new_to_old.push(i);
361 condensed_idx += 1;
362 singletons += 1;
363 visited[i] = true;
364 continue;
365 }
366
367 let mut j = i;
369 loop {
370 let k = matching_fwd[j];
371 if visited[k] || k == i {
372 if !visited[j] {
374 partner[j] = -1;
375 old_to_new[j] = condensed_idx;
376 new_to_old.push(j);
377 condensed_idx += 1;
378 singletons += 1;
379 visited[j] = true;
380 }
381 break;
382 }
383
384 partner[j] = k as isize;
386 partner[k] = j as isize;
387 old_to_new[j] = condensed_idx;
388 old_to_new[k] = condensed_idx;
389 new_to_old.push(j);
390 condensed_idx += 1;
391 two_cycles += 1;
392 visited[j] = true;
393 visited[k] = true;
394
395 let next = matching_fwd[k];
397 if next == i {
398 break;
399 }
400 j = next;
401 if visited[j] {
406 break;
407 }
408 }
409 }
410
411 CycleDecomposition {
412 partner,
413 old_to_new,
414 new_to_old,
415 condensed_dim: condensed_idx,
416 singletons,
417 two_cycles,
418 }
419}
420
421fn build_condensed_adjacency(
433 matrix: SymbolicSparseColMatRef<'_, usize>,
434 decomp: &CycleDecomposition,
435) -> Result<(Vec<i32>, Vec<i32>), SparseError> {
436 let n = matrix.nrows();
437 let col_ptrs = matrix.col_ptr();
438 let row_indices = matrix.row_idx();
439 let cdim = decomp.condensed_dim;
440
441 let mut neighbors: Vec<Vec<i32>> = vec![Vec::new(); cdim];
443 let mut marker = vec![usize::MAX; cdim]; for col in 0..n {
446 let c_col = decomp.old_to_new[col];
447
448 marker[c_col] = c_col;
454
455 let start = col_ptrs[col];
456 let end = col_ptrs[col + 1];
457 for &row in &row_indices[start..end] {
458 let c_row = decomp.old_to_new[row];
459 if marker[c_row] != c_col {
460 marker[c_row] = c_col;
461 neighbors[c_col].push(c_row as i32);
463 neighbors[c_row].push(c_col as i32);
464 }
465 }
466 }
467
468 for nbrs in &mut neighbors {
471 nbrs.sort_unstable();
472 nbrs.dedup();
473 }
474
475 let total_edges: usize = neighbors.iter().map(|v| v.len()).sum();
477 if total_edges > i32::MAX as usize {
478 return Err(SparseError::InvalidInput {
479 reason: format!(
480 "Condensed adjacency entries {} exceeds i32::MAX",
481 total_edges
482 ),
483 });
484 }
485
486 let mut xadj = Vec::with_capacity(cdim + 1);
488 xadj.push(0i32);
489 let mut adjncy = Vec::with_capacity(total_edges);
490
491 for nbrs in &neighbors {
492 adjncy.extend_from_slice(nbrs);
493 xadj.push(adjncy.len() as i32);
494 }
495
496 Ok((xadj, adjncy))
497}
498
499fn expand_ordering(condensed_order: &[i32], decomp: &CycleDecomposition, n: usize) -> Perm<usize> {
511 let cdim = decomp.condensed_dim;
512
513 let mut inv_order = vec![0usize; cdim];
515 for (cnode, &pos) in condensed_order.iter().enumerate() {
516 inv_order[pos as usize] = cnode;
517 }
518
519 let mut fwd = Vec::with_capacity(n); for &cnode in &inv_order {
522 let orig = decomp.new_to_old[cnode];
523 fwd.push(orig);
524
525 if decomp.partner[orig] >= 0 {
527 fwd.push(decomp.partner[orig] as usize);
528 }
529 }
530
531 debug_assert_eq!(fwd.len(), n);
535
536 let mut inv = vec![0usize; n];
538 for (new_pos, &old_idx) in fwd.iter().enumerate() {
539 inv[old_idx] = new_pos;
540 }
541
542 Perm::new_checked(fwd.into_boxed_slice(), inv.into_boxed_slice(), n)
543}
544
545fn identity_perm(n: usize) -> Perm<usize> {
547 let id: Vec<usize> = (0..n).collect();
548 Perm::new_checked(id.clone().into_boxed_slice(), id.into_boxed_slice(), n)
549}
550
551fn call_metis_node_nd(
560 n: usize,
561 xadj: &mut [i32],
562 adjncy: &mut [i32],
563) -> Result<(Vec<i32>, Vec<i32>), SparseError> {
564 let mut options = [0i32; metis_sys::METIS_NOPTIONS as usize];
565 unsafe {
566 metis_sys::METIS_SetDefaultOptions(options.as_mut_ptr());
567 }
568 options[metis_sys::moptions_et_METIS_OPTION_NUMBERING as usize] = 0;
569
570 let mut nvtxs = n as i32;
571 let mut perm_i32 = vec![0i32; n];
572 let mut iperm_i32 = vec![0i32; n];
573
574 let ret = unsafe {
575 metis_sys::METIS_NodeND(
576 &mut nvtxs,
577 xadj.as_mut_ptr(),
578 adjncy.as_mut_ptr(),
579 std::ptr::null_mut(),
580 options.as_mut_ptr(),
581 perm_i32.as_mut_ptr(),
582 iperm_i32.as_mut_ptr(),
583 )
584 };
585
586 if ret != metis_sys::rstatus_et_METIS_OK {
587 let msg = match ret {
588 x if x == metis_sys::rstatus_et_METIS_ERROR_INPUT => "METIS_ERROR_INPUT: invalid input",
589 x if x == metis_sys::rstatus_et_METIS_ERROR_MEMORY => {
590 "METIS_ERROR_MEMORY: allocation failure"
591 }
592 x if x == metis_sys::rstatus_et_METIS_ERROR => "METIS_ERROR: general error",
593 _ => "METIS: unknown error code",
594 };
595 return Err(SparseError::AnalysisFailure {
596 reason: format!("{} (return code: {}, dim: {})", msg, ret, n),
597 });
598 }
599
600 Ok((perm_i32, iperm_i32))
601}
602
603fn extract_adjacency(
626 matrix: SymbolicSparseColMatRef<'_, usize>,
627) -> Result<(Vec<i32>, Vec<i32>), SparseError> {
628 let n = matrix.nrows();
629 let col_ptrs = matrix.col_ptr();
630 let row_indices = matrix.row_idx();
631
632 let mut neighbors: Vec<Vec<usize>> = vec![Vec::new(); n];
635
636 for j in 0..n {
639 let start = col_ptrs[j];
640 let end = col_ptrs[j + 1];
641 for &i in &row_indices[start..end] {
642 if i != j {
643 neighbors[j].push(i);
644 neighbors[i].push(j);
645 }
646 }
647 }
648
649 for nbrs in &mut neighbors {
651 nbrs.sort_unstable();
652 nbrs.dedup();
653 }
654
655 let total_edges: usize = neighbors.iter().map(|v| v.len()).sum();
657 if total_edges > i32::MAX as usize {
658 return Err(SparseError::InvalidInput {
659 reason: format!(
660 "Total adjacency entries {} exceeds i32::MAX ({}); METIS requires 32-bit indices",
661 total_edges,
662 i32::MAX
663 ),
664 });
665 }
666
667 let mut xadj = Vec::with_capacity(n + 1);
669 xadj.push(0i32);
670
671 let mut adjncy = Vec::with_capacity(total_edges);
672
673 for nbrs in &neighbors {
674 for &neighbor in nbrs {
675 adjncy.push(neighbor as i32);
676 }
677 xadj.push(adjncy.len() as i32);
678 }
679
680 Ok((xadj, adjncy))
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 use faer::sparse::{SparseColMat, Triplet};
688
689 fn make_tridiagonal(n: usize) -> SparseColMat<usize, f64> {
691 let mut triplets = Vec::new();
692 for i in 0..n {
693 triplets.push(Triplet::new(i, i, 4.0));
694 }
695 for i in 0..n - 1 {
696 triplets.push(Triplet::new(i, i + 1, 1.0));
697 triplets.push(Triplet::new(i + 1, i, 1.0));
698 }
699 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
700 }
701
702 fn make_diagonal(n: usize) -> SparseColMat<usize, f64> {
704 let triplets: Vec<_> = (0..n).map(|i| Triplet::new(i, i, (i + 1) as f64)).collect();
705 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
706 }
707
708 fn make_arrow(n: usize) -> SparseColMat<usize, f64> {
710 let mut triplets = Vec::new();
711 for i in 0..n {
712 triplets.push(Triplet::new(i, i, 10.0));
713 }
714 for i in 1..n {
715 triplets.push(Triplet::new(0, i, 1.0));
716 triplets.push(Triplet::new(i, 0, 1.0));
717 }
718 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
719 }
720
721 fn make_upper_triangle_only(n: usize) -> SparseColMat<usize, f64> {
723 let mut triplets = Vec::new();
724 for i in 0..n {
725 triplets.push(Triplet::new(i, i, 4.0));
726 }
727 for i in 0..n - 1 {
728 triplets.push(Triplet::new(i, i + 1, 1.0));
730 }
731 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
732 }
733
734 #[test]
737 fn test_metis_ordering_tridiagonal_5() {
738 let matrix = make_tridiagonal(5);
739 let perm = metis_ordering(matrix.symbolic()).expect("METIS ordering should succeed");
740
741 let (fwd, inv) = perm.as_ref().arrays();
743 assert_eq!(fwd.len(), 5);
744 assert_eq!(inv.len(), 5);
745
746 let mut seen_fwd = [false; 5];
747 let mut seen_inv = [false; 5];
748 for i in 0..5 {
749 assert!(fwd[i] < 5, "fwd[{}] = {} out of range", i, fwd[i]);
750 assert!(inv[i] < 5, "inv[{}] = {} out of range", i, inv[i]);
751 seen_fwd[fwd[i]] = true;
752 seen_inv[inv[i]] = true;
753 }
754 assert!(
755 seen_fwd.iter().all(|&s| s),
756 "fwd is not a valid permutation"
757 );
758 assert!(
759 seen_inv.iter().all(|&s| s),
760 "inv is not a valid permutation"
761 );
762 }
763
764 #[test]
765 fn test_metis_ordering_fwd_inv_consistency() {
766 let matrix = make_tridiagonal(10);
767 let perm = metis_ordering(matrix.symbolic()).expect("METIS ordering should succeed");
768
769 let (fwd, inv) = perm.as_ref().arrays();
770 for i in 0..10 {
771 assert_eq!(fwd[inv[i]], i, "fwd[inv[{}]] = {} != {}", i, fwd[inv[i]], i);
772 assert_eq!(inv[fwd[i]], i, "inv[fwd[{}]] = {} != {}", i, inv[fwd[i]], i);
773 }
774 }
775
776 #[test]
777 fn test_metis_ordering_dim_0() {
778 let triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
779 let matrix = SparseColMat::try_new_from_triplets(0, 0, &triplets).unwrap();
780 let perm = metis_ordering(matrix.symbolic()).expect("dim-0 should succeed");
781 let (fwd, inv) = perm.as_ref().arrays();
782 assert_eq!(fwd.len(), 0);
783 assert_eq!(inv.len(), 0);
784 }
785
786 #[test]
787 fn test_metis_ordering_dim_1() {
788 let matrix = make_diagonal(1);
789 let perm = metis_ordering(matrix.symbolic()).expect("dim-1 should succeed");
790 let (fwd, inv) = perm.as_ref().arrays();
791 assert_eq!(fwd, &[0]);
792 assert_eq!(inv, &[0]);
793 }
794
795 #[test]
796 fn test_metis_ordering_diagonal_identity() {
797 let matrix = make_diagonal(5);
799 let perm = metis_ordering(matrix.symbolic()).expect("diagonal should succeed");
800 let (fwd, inv) = perm.as_ref().arrays();
801 for i in 0..5 {
802 assert_eq!(
803 fwd[i], i,
804 "diagonal perm should be identity: fwd[{}] = {}",
805 i, fwd[i]
806 );
807 assert_eq!(
808 inv[i], i,
809 "diagonal perm should be identity: inv[{}] = {}",
810 i, inv[i]
811 );
812 }
813 }
814
815 #[test]
818 fn test_adjacency_tridiagonal_3x3() {
819 let matrix = make_tridiagonal(3);
821 let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
822
823 assert_eq!(xadj, vec![0, 1, 3, 4]);
828 assert_eq!(adjncy.len(), 4);
829
830 let n0: Vec<i32> = adjncy[xadj[0] as usize..xadj[1] as usize].to_vec();
832 let n1: Vec<i32> = adjncy[xadj[1] as usize..xadj[2] as usize].to_vec();
833 let n2: Vec<i32> = adjncy[xadj[2] as usize..xadj[3] as usize].to_vec();
834 assert_eq!(n0, vec![1]);
835 assert_eq!(n1, vec![0, 2]);
836 assert_eq!(n2, vec![1]);
837 }
838
839 #[test]
840 fn test_adjacency_arrow_5x5() {
841 let matrix = make_arrow(5);
843 let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
844
845 assert_eq!(xadj, vec![0, 4, 5, 6, 7, 8]);
851 assert_eq!(adjncy.len(), 8);
852
853 let n0: Vec<i32> = adjncy[xadj[0] as usize..xadj[1] as usize].to_vec();
854 assert_eq!(n0, vec![1, 2, 3, 4]);
855 for v in 1..5i32 {
856 let start = xadj[v as usize] as usize;
857 let end = xadj[v as usize + 1] as usize;
858 assert_eq!(
859 &adjncy[start..end],
860 &[0],
861 "vertex {} should only neighbor 0",
862 v
863 );
864 }
865 }
866
867 #[test]
868 fn test_adjacency_diagonal_only() {
869 let matrix = make_diagonal(4);
871 let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
872
873 assert_eq!(xadj, vec![0, 0, 0, 0, 0]);
874 assert!(adjncy.is_empty());
875 }
876
877 #[test]
878 fn test_adjacency_upper_triangle_only() {
879 let matrix = make_upper_triangle_only(4);
881 let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
882
883 assert_eq!(xadj, vec![0, 1, 3, 5, 6]);
890 assert_eq!(adjncy.len(), 6);
891
892 let n = 4;
894 for i in 0..n {
895 let start = xadj[i] as usize;
896 let end = xadj[i + 1] as usize;
897 for &j in &adjncy[start..end] {
898 let j_start = xadj[j as usize] as usize;
899 let j_end = xadj[j as usize + 1] as usize;
900 assert!(
901 adjncy[j_start..j_end].contains(&(i as i32)),
902 "symmetry violated: ({}, {}) exists but ({}, {}) does not",
903 i,
904 j,
905 j,
906 i
907 );
908 }
909 }
910 }
911
912 #[test]
913 fn test_adjacency_invariants() {
914 for (name, matrix) in [
916 ("tridiag3", make_tridiagonal(3)),
917 ("arrow5", make_arrow(5)),
918 ("diag4", make_diagonal(4)),
919 ("upper4", make_upper_triangle_only(4)),
920 ] {
921 let n = matrix.nrows();
922 let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
923
924 assert_eq!(xadj[0], 0, "{}: xadj[0] should be 0", name);
926
927 assert_eq!(xadj.len(), n + 1, "{}: xadj length should be n+1", name);
929
930 for i in 0..n {
932 assert!(
933 xadj[i + 1] >= xadj[i],
934 "{}: xadj not monotonic at {}: {} > {}",
935 name,
936 i,
937 xadj[i],
938 xadj[i + 1]
939 );
940 }
941
942 for i in 0..n {
944 let start = xadj[i] as usize;
945 let end = xadj[i + 1] as usize;
946 for &j in &adjncy[start..end] {
947 assert_ne!(j, i as i32, "{}: self-loop at vertex {}", name, i);
948 }
949 }
950
951 for &j in &adjncy {
953 assert!(
954 j >= 0 && (j as usize) < n,
955 "{}: adjncy index {} out of range [0, {})",
956 name,
957 j,
958 n
959 );
960 }
961
962 for i in 0..n {
964 let start = xadj[i] as usize;
965 let end = xadj[i + 1] as usize;
966 for &j in &adjncy[start..end] {
967 let j_start = xadj[j as usize] as usize;
968 let j_end = xadj[j as usize + 1] as usize;
969 assert!(
970 adjncy[j_start..j_end].contains(&(i as i32)),
971 "{}: symmetry violated: ({}, {}) without ({}, {})",
972 name,
973 i,
974 j,
975 j,
976 i
977 );
978 }
979 }
980 }
981 }
982
983 #[test]
986 fn test_split_all_singletons() {
987 let fwd = vec![0, 1, 2, 3];
989 let is_matched = vec![true, true, true, true];
990 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
991
992 assert_eq!(decomp.singletons, 4);
993 assert_eq!(decomp.two_cycles, 0);
994 assert_eq!(decomp.condensed_dim, 4);
995 for i in 0..4 {
996 assert_eq!(decomp.partner[i], -1, "index {} should be singleton", i);
997 }
998 }
999
1000 #[test]
1001 fn test_split_pure_2_cycles() {
1002 let fwd = vec![1, 0, 3, 2];
1004 let is_matched = vec![true, true, true, true];
1005 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
1006
1007 assert_eq!(decomp.singletons, 0);
1008 assert_eq!(decomp.two_cycles, 2);
1009 assert_eq!(decomp.condensed_dim, 2);
1010 assert_eq!(decomp.partner[0], 1);
1011 assert_eq!(decomp.partner[1], 0);
1012 assert_eq!(decomp.partner[2], 3);
1013 assert_eq!(decomp.partner[3], 2);
1014 assert_eq!(decomp.old_to_new[0], decomp.old_to_new[1]);
1016 assert_eq!(decomp.old_to_new[2], decomp.old_to_new[3]);
1017 assert_ne!(decomp.old_to_new[0], decomp.old_to_new[2]);
1018 }
1019
1020 #[test]
1021 fn test_split_3_cycle() {
1022 let fwd = vec![1, 2, 0];
1024 let is_matched = vec![true, true, true];
1025 let decomp = split_matching_cycles(&fwd, &is_matched, 3);
1026
1027 assert_eq!(decomp.two_cycles, 1);
1029 assert_eq!(decomp.singletons, 1);
1030 assert_eq!(decomp.condensed_dim, 2);
1031
1032 let pairs: Vec<_> = (0..3).filter(|&i| decomp.partner[i] >= 0).collect();
1034 let sings: Vec<_> = (0..3).filter(|&i| decomp.partner[i] == -1).collect();
1035 assert_eq!(pairs.len(), 2);
1036 assert_eq!(sings.len(), 1);
1037 }
1038
1039 #[test]
1040 fn test_split_4_cycle() {
1041 let fwd = vec![1, 2, 3, 0];
1043 let is_matched = vec![true, true, true, true];
1044 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
1045
1046 assert_eq!(decomp.two_cycles, 2);
1048 assert_eq!(decomp.singletons, 0);
1049 assert_eq!(decomp.condensed_dim, 2);
1050 }
1051
1052 #[test]
1053 fn test_split_5_cycle() {
1054 let fwd = vec![1, 2, 3, 4, 0];
1056 let is_matched = vec![true, true, true, true, true];
1057 let decomp = split_matching_cycles(&fwd, &is_matched, 5);
1058
1059 assert_eq!(decomp.two_cycles, 2);
1061 assert_eq!(decomp.singletons, 1);
1062 assert_eq!(decomp.condensed_dim, 3);
1063 }
1064
1065 #[test]
1066 fn test_split_mixed_with_unmatched() {
1067 let fwd = vec![1, 0, 2, 4, 3, 5]; let is_matched = vec![true, true, true, true, true, false];
1070 let decomp = split_matching_cycles(&fwd, &is_matched, 6);
1071
1072 assert_eq!(decomp.two_cycles, 2);
1073 assert_eq!(decomp.singletons, 2); assert_eq!(decomp.condensed_dim, 4); assert_eq!(decomp.partner[5], -2); assert_eq!(decomp.partner[2], -1); assert!(
1080 decomp.old_to_new[5] < decomp.condensed_dim,
1081 "unmatched node should have valid condensed index"
1082 );
1083 }
1084
1085 #[test]
1086 fn test_split_trivial_n0() {
1087 let decomp = split_matching_cycles(&[], &[], 0);
1088 assert_eq!(decomp.condensed_dim, 0);
1089 assert_eq!(decomp.singletons, 0);
1090 assert_eq!(decomp.two_cycles, 0);
1091 }
1092
1093 #[test]
1094 fn test_split_trivial_n1() {
1095 let decomp = split_matching_cycles(&[0], &[true], 1);
1096 assert_eq!(decomp.condensed_dim, 1);
1097 assert_eq!(decomp.singletons, 1);
1098 assert_eq!(decomp.two_cycles, 0);
1099 assert_eq!(decomp.partner[0], -1);
1100 }
1101
1102 fn make_upper_tri(n: usize, entries: &[(usize, usize, f64)]) -> SparseColMat<usize, f64> {
1106 let triplets: Vec<_> = entries
1107 .iter()
1108 .map(|&(i, j, v)| Triplet::new(i, j, v))
1109 .collect();
1110 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
1111 }
1112
1113 #[test]
1114 fn test_condensed_adjacency_arrow_with_2cycle() {
1115 let matrix = make_arrow(4);
1120
1121 let fwd = vec![1, 0, 2, 3];
1123 let is_matched = vec![true, true, true, true];
1124 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
1125
1126 assert_eq!(decomp.condensed_dim, 3);
1127
1128 let (xadj, adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp).unwrap();
1129
1130 assert_eq!(xadj.len(), 4); for i in 0..3 {
1135 let start = xadj[i] as usize;
1136 let end = xadj[i + 1] as usize;
1137 for &j in &adjncy[start..end] {
1138 assert_ne!(j, i as i32, "self-loop at condensed vertex {}", i);
1139 }
1140 }
1141
1142 let total = adjncy.len();
1146 assert!(total > 0, "condensed graph should have edges");
1147 }
1148
1149 #[test]
1150 fn test_condensed_adjacency_diagonal() {
1151 let matrix = make_diagonal(4);
1153 let fwd = vec![0, 1, 2, 3];
1154 let is_matched = vec![true, true, true, true];
1155 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
1156
1157 let (xadj, adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp).unwrap();
1158 assert!(
1159 adjncy.is_empty(),
1160 "diagonal should have zero condensed edges"
1161 );
1162 assert_eq!(xadj, vec![0, 0, 0, 0, 0]);
1163 }
1164
1165 #[test]
1166 fn test_condensed_adjacency_full_4x4_two_pairs() {
1167 let matrix = make_upper_tri(
1169 4,
1170 &[
1171 (0, 0, 1.0),
1172 (0, 1, 1.0),
1173 (0, 2, 1.0),
1174 (0, 3, 1.0),
1175 (1, 1, 1.0),
1176 (1, 2, 1.0),
1177 (1, 3, 1.0),
1178 (2, 2, 1.0),
1179 (2, 3, 1.0),
1180 (3, 3, 1.0),
1181 ],
1182 );
1183 let fwd = vec![1, 0, 3, 2];
1184 let is_matched = vec![true, true, true, true];
1185 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
1186
1187 assert_eq!(decomp.condensed_dim, 2);
1188
1189 let (xadj, adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp).unwrap();
1190
1191 assert_eq!(xadj.len(), 3);
1193 assert_eq!(adjncy.len(), 2);
1195
1196 let n0_start = xadj[0] as usize;
1198 let n0_end = xadj[1] as usize;
1199 let n1_start = xadj[1] as usize;
1200 let n1_end = xadj[2] as usize;
1201 assert!(adjncy[n0_start..n0_end].contains(&1));
1202 assert!(adjncy[n1_start..n1_end].contains(&0));
1203 }
1204
1205 #[test]
1208 fn test_expand_ordering_with_pairs() {
1209 let fwd = vec![1, 0, 3, 2];
1212 let is_matched = vec![true, true, true, true];
1213 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
1214
1215 let condensed_order = vec![1i32, 0i32];
1218 let perm = expand_ordering(&condensed_order, &decomp, 4);
1219
1220 let (expanded_fwd, expanded_inv) = perm.as_ref().arrays();
1221 assert_eq!(expanded_fwd.len(), 4);
1222
1223 let mut seen = [false; 4];
1225 for &v in expanded_fwd {
1226 assert!(v < 4);
1227 seen[v] = true;
1228 }
1229 assert!(seen.iter().all(|&s| s), "not a valid permutation");
1230
1231 let pos_0 = expanded_inv[0];
1233 let pos_1 = expanded_inv[1];
1234 assert_eq!(pos_0.abs_diff(pos_1), 1, "pair (0,1) not consecutive");
1235
1236 let pos_2 = expanded_inv[2];
1237 let pos_3 = expanded_inv[3];
1238 assert_eq!(pos_2.abs_diff(pos_3), 1, "pair (2,3) not consecutive");
1239 }
1240
1241 #[test]
1242 fn test_expand_ordering_mixed_singletons_pairs() {
1243 let fwd = vec![1, 0, 2, 4, 3];
1245 let is_matched = vec![true, true, true, true, true];
1246 let decomp = split_matching_cycles(&fwd, &is_matched, 5);
1247
1248 assert_eq!(decomp.condensed_dim, 3);
1249
1250 let condensed_order = vec![0i32, 1i32, 2i32];
1252 let perm = expand_ordering(&condensed_order, &decomp, 5);
1253
1254 let (expanded_fwd, expanded_inv) = perm.as_ref().arrays();
1255 assert_eq!(expanded_fwd.len(), 5);
1256
1257 let mut seen = [false; 5];
1259 for &v in expanded_fwd {
1260 seen[v] = true;
1261 }
1262 assert!(seen.iter().all(|&s| s));
1263
1264 let diff = (expanded_inv[0] as isize - expanded_inv[1] as isize).unsigned_abs();
1266 assert_eq!(diff, 1, "pair (0,1) not consecutive");
1267
1268 let diff = (expanded_inv[3] as isize - expanded_inv[4] as isize).unsigned_abs();
1270 assert_eq!(diff, 1, "pair (3,4) not consecutive");
1271 }
1272
1273 #[test]
1274 fn test_expand_ordering_with_unmatched() {
1275 let fwd = vec![1, 0, 2, 3, 4];
1277 let is_matched = vec![true, true, true, false, false];
1278 let decomp = split_matching_cycles(&fwd, &is_matched, 5);
1279
1280 assert_eq!(decomp.condensed_dim, 4); let condensed_order = vec![0i32, 1i32, 2i32, 3i32];
1284 let perm = expand_ordering(&condensed_order, &decomp, 5);
1285
1286 let (expanded_fwd, expanded_inv) = perm.as_ref().arrays();
1287 assert_eq!(expanded_fwd.len(), 5);
1288
1289 let mut seen = [false; 5];
1291 for &v in expanded_fwd {
1292 assert!(v < 5);
1293 seen[v] = true;
1294 }
1295 assert!(seen.iter().all(|&s| s), "not a valid permutation");
1296
1297 let diff = (expanded_inv[0] as isize - expanded_inv[1] as isize).unsigned_abs();
1299 assert_eq!(diff, 1, "pair (0,1) not consecutive");
1300
1301 }
1305
1306 #[test]
1309 fn test_split_unmatched_get_condensed_indices() {
1310 let fwd = vec![1, 0, 2, 3]; let is_matched = vec![true, true, false, false];
1313 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
1314
1315 assert_eq!(decomp.condensed_dim, 3); assert!(decomp.old_to_new[2] < decomp.condensed_dim);
1318 assert!(decomp.old_to_new[3] < decomp.condensed_dim);
1319
1320 assert_ne!(decomp.old_to_new[2], decomp.old_to_new[3]);
1322
1323 assert_eq!(decomp.old_to_new[0], decomp.old_to_new[1]);
1325 }
1326
1327 #[test]
1328 fn test_condensed_adjacency_includes_unmatched_edges() {
1329 let matrix = make_upper_tri(
1335 4,
1336 &[
1337 (0, 0, 1.0),
1338 (0, 1, 1.0),
1339 (0, 2, 1.0),
1340 (1, 1, 1.0),
1341 (2, 2, 1.0),
1342 (3, 3, 1.0),
1343 ],
1344 );
1345
1346 let fwd = vec![1, 0, 2, 3];
1347 let is_matched = vec![true, true, false, false];
1348 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
1349
1350 let (xadj, adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp).unwrap();
1351
1352 assert_eq!(xadj.len(), 4); let c_pair = decomp.old_to_new[0];
1358 let c_unmatched_2 = decomp.old_to_new[2];
1359
1360 let pair_start = xadj[c_pair] as usize;
1361 let pair_end = xadj[c_pair + 1] as usize;
1362 let pair_neighbors: Vec<i32> = adjncy[pair_start..pair_end].to_vec();
1363 assert!(
1364 pair_neighbors.contains(&(c_unmatched_2 as i32)),
1365 "pair should neighbor unmatched node 2; neighbors = {:?}",
1366 pair_neighbors
1367 );
1368
1369 let c_unmatched_3 = decomp.old_to_new[3];
1371 let n3_start = xadj[c_unmatched_3] as usize;
1372 let n3_end = xadj[c_unmatched_3 + 1] as usize;
1373 assert_eq!(
1374 n3_end - n3_start,
1375 0,
1376 "isolated unmatched node 3 should have no neighbors"
1377 );
1378 }
1379
1380 #[test]
1381 fn test_expand_unmatched_not_at_end() {
1382 let fwd = vec![1, 0, 2, 3, 5, 4];
1385 let is_matched = vec![true, true, false, true, true, true];
1386 let decomp = split_matching_cycles(&fwd, &is_matched, 6);
1387
1388 let cdim = decomp.condensed_dim;
1390
1391 let c_unmatched = decomp.old_to_new[2];
1393 let mut condensed_order = vec![0i32; cdim];
1394 condensed_order[c_unmatched] = 0;
1396 let mut pos = 1i32;
1398 for (cnode, order) in condensed_order.iter_mut().enumerate().take(cdim) {
1399 if cnode == c_unmatched {
1400 continue;
1401 }
1402 *order = pos;
1403 pos += 1;
1404 }
1405
1406 let perm = expand_ordering(&condensed_order, &decomp, 6);
1407 let (expanded_fwd, expanded_inv) = perm.as_ref().arrays();
1408
1409 assert_eq!(
1411 expanded_inv[2], 0,
1412 "unmatched node should be at position 0, got {}",
1413 expanded_inv[2]
1414 );
1415
1416 let mut seen = [false; 6];
1418 for &v in expanded_fwd {
1419 assert!(v < 6);
1420 seen[v] = true;
1421 }
1422 assert!(seen.iter().all(|&s| s));
1423 }
1424
1425 #[test]
1426 fn test_all_unmatched_gets_ordering() {
1427 let fwd = vec![0, 1, 2, 3];
1429 let is_matched = vec![false, false, false, false];
1430 let decomp = split_matching_cycles(&fwd, &is_matched, 4);
1431
1432 assert_eq!(decomp.condensed_dim, 4);
1434 assert_eq!(decomp.singletons, 4);
1435 assert_eq!(decomp.two_cycles, 0);
1436
1437 for i in 0..4 {
1439 assert!(decomp.old_to_new[i] < 4);
1440 }
1441
1442 let condensed_order = vec![0i32, 1, 2, 3];
1444 let perm = expand_ordering(&condensed_order, &decomp, 4);
1445 let (fwd_expanded, _) = perm.as_ref().arrays();
1446 assert_eq!(fwd_expanded.len(), 4);
1447
1448 let mut seen = [false; 4];
1449 for &v in fwd_expanded {
1450 seen[v] = true;
1451 }
1452 assert!(seen.iter().all(|&s| s));
1453 }
1454}