use faer::perm::Perm;
use faer::sparse::{SparseColMat, SymbolicSparseColMatRef};
use super::matching::{Mc64Job, mc64_matching};
use crate::error::SparseError;
pub struct MatchOrderResult {
pub ordering: Perm<usize>,
pub scaling: Vec<f64>,
pub matched: usize,
pub condensed_dim: usize,
pub singletons: usize,
pub two_cycles: usize,
}
pub fn match_order_metis(
matrix: &SparseColMat<usize, f64>,
) -> Result<MatchOrderResult, SparseError> {
let n = matrix.nrows();
if n <= 1 {
let scaling = if n == 1 { vec![1.0] } else { vec![] };
return Ok(MatchOrderResult {
ordering: identity_perm(n),
scaling,
matched: n,
condensed_dim: n,
singletons: n,
two_cycles: 0,
});
}
let mc64_result = mc64_matching(matrix, Mc64Job::MaximumProduct)?;
let matching_fwd = mc64_result.matching.as_ref().arrays().0;
let decomp = split_matching_cycles(matching_fwd, &mc64_result.is_matched, n);
let (mut xadj, mut adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp)?;
let cdim = decomp.condensed_dim;
let condensed_order = if cdim <= 1 || adjncy.is_empty() {
(0..cdim as i32).collect::<Vec<_>>()
} else {
if cdim > i32::MAX as usize {
return Err(SparseError::InvalidInput {
reason: format!(
"Condensed dimension {} exceeds i32::MAX; METIS requires 32-bit indices",
cdim
),
});
}
let (_perm, iperm) = call_metis_node_nd(cdim, &mut xadj, &mut adjncy)?;
iperm
};
let ordering = expand_ordering(&condensed_order, &decomp, n);
Ok(MatchOrderResult {
ordering,
scaling: mc64_result.scaling,
matched: mc64_result.matched,
condensed_dim: decomp.condensed_dim,
singletons: decomp.singletons,
two_cycles: decomp.two_cycles,
})
}
pub fn metis_ordering(
matrix: SymbolicSparseColMatRef<'_, usize>,
) -> Result<Perm<usize>, SparseError> {
let n = matrix.nrows();
if n > i32::MAX as usize {
return Err(SparseError::InvalidInput {
reason: format!(
"Matrix dimension {} exceeds i32::MAX ({}); METIS requires 32-bit indices",
n,
i32::MAX
),
});
}
if n <= 1 {
return Ok(identity_perm(n));
}
let (mut xadj, mut adjncy) = extract_adjacency(matrix)?;
if adjncy.is_empty() {
return Ok(identity_perm(n));
}
let (perm_i32, iperm_i32) = call_metis_node_nd(n, &mut xadj, &mut adjncy)?;
let fwd: Box<[usize]> = perm_i32.iter().map(|&v| v as usize).collect();
let inv: Box<[usize]> = iperm_i32.iter().map(|&v| v as usize).collect();
Ok(Perm::new_checked(fwd, inv, n))
}
struct CycleDecomposition {
partner: Vec<isize>,
old_to_new: Vec<usize>,
new_to_old: Vec<usize>,
condensed_dim: usize,
singletons: usize,
two_cycles: usize,
}
fn split_matching_cycles(
matching_fwd: &[usize],
is_matched: &[bool],
n: usize,
) -> CycleDecomposition {
let mut partner = vec![isize::MIN; n];
let mut old_to_new = vec![0usize; n];
let mut new_to_old = Vec::new();
let mut visited = vec![false; n];
let mut singletons = 0usize;
let mut two_cycles = 0usize;
for i in 0..n {
if !is_matched[i] {
partner[i] = -2;
visited[i] = true;
}
}
let mut condensed_idx = 0usize;
for i in 0..n {
if visited[i] && partner[i] != -2 {
continue;
}
if partner[i] == -2 {
old_to_new[i] = condensed_idx;
new_to_old.push(i);
condensed_idx += 1;
singletons += 1;
continue;
}
if matching_fwd[i] == i {
partner[i] = -1;
old_to_new[i] = condensed_idx;
new_to_old.push(i);
condensed_idx += 1;
singletons += 1;
visited[i] = true;
continue;
}
let mut j = i;
loop {
let k = matching_fwd[j];
if visited[k] || k == i {
if !visited[j] {
partner[j] = -1;
old_to_new[j] = condensed_idx;
new_to_old.push(j);
condensed_idx += 1;
singletons += 1;
visited[j] = true;
}
break;
}
partner[j] = k as isize;
partner[k] = j as isize;
old_to_new[j] = condensed_idx;
old_to_new[k] = condensed_idx;
new_to_old.push(j);
condensed_idx += 1;
two_cycles += 1;
visited[j] = true;
visited[k] = true;
let next = matching_fwd[k];
if next == i {
break;
}
j = next;
if visited[j] {
break;
}
}
}
CycleDecomposition {
partner,
old_to_new,
new_to_old,
condensed_dim: condensed_idx,
singletons,
two_cycles,
}
}
fn build_condensed_adjacency(
matrix: SymbolicSparseColMatRef<'_, usize>,
decomp: &CycleDecomposition,
) -> Result<(Vec<i32>, Vec<i32>), SparseError> {
let n = matrix.nrows();
let col_ptrs = matrix.col_ptr();
let row_indices = matrix.row_idx();
let cdim = decomp.condensed_dim;
let mut neighbors: Vec<Vec<i32>> = vec![Vec::new(); cdim];
let mut marker = vec![usize::MAX; cdim];
for col in 0..n {
let c_col = decomp.old_to_new[col];
marker[c_col] = c_col;
let start = col_ptrs[col];
let end = col_ptrs[col + 1];
for &row in &row_indices[start..end] {
let c_row = decomp.old_to_new[row];
if marker[c_row] != c_col {
marker[c_row] = c_col;
neighbors[c_col].push(c_row as i32);
neighbors[c_row].push(c_col as i32);
}
}
}
for nbrs in &mut neighbors {
nbrs.sort_unstable();
nbrs.dedup();
}
let total_edges: usize = neighbors.iter().map(|v| v.len()).sum();
if total_edges > i32::MAX as usize {
return Err(SparseError::InvalidInput {
reason: format!(
"Condensed adjacency entries {} exceeds i32::MAX",
total_edges
),
});
}
let mut xadj = Vec::with_capacity(cdim + 1);
xadj.push(0i32);
let mut adjncy = Vec::with_capacity(total_edges);
for nbrs in &neighbors {
adjncy.extend_from_slice(nbrs);
xadj.push(adjncy.len() as i32);
}
Ok((xadj, adjncy))
}
fn expand_ordering(condensed_order: &[i32], decomp: &CycleDecomposition, n: usize) -> Perm<usize> {
let cdim = decomp.condensed_dim;
let mut inv_order = vec![0usize; cdim];
for (cnode, &pos) in condensed_order.iter().enumerate() {
inv_order[pos as usize] = cnode;
}
let mut fwd = Vec::with_capacity(n); for &cnode in &inv_order {
let orig = decomp.new_to_old[cnode];
fwd.push(orig);
if decomp.partner[orig] >= 0 {
fwd.push(decomp.partner[orig] as usize);
}
}
debug_assert_eq!(fwd.len(), n);
let mut inv = vec![0usize; n];
for (new_pos, &old_idx) in fwd.iter().enumerate() {
inv[old_idx] = new_pos;
}
Perm::new_checked(fwd.into_boxed_slice(), inv.into_boxed_slice(), n)
}
fn identity_perm(n: usize) -> Perm<usize> {
let id: Vec<usize> = (0..n).collect();
Perm::new_checked(id.clone().into_boxed_slice(), id.into_boxed_slice(), n)
}
fn call_metis_node_nd(
n: usize,
xadj: &mut [i32],
adjncy: &mut [i32],
) -> Result<(Vec<i32>, Vec<i32>), SparseError> {
let mut options = [0i32; metis_sys::METIS_NOPTIONS as usize];
unsafe {
metis_sys::METIS_SetDefaultOptions(options.as_mut_ptr());
}
options[metis_sys::moptions_et_METIS_OPTION_NUMBERING as usize] = 0;
let mut nvtxs = n as i32;
let mut perm_i32 = vec![0i32; n];
let mut iperm_i32 = vec![0i32; n];
let ret = unsafe {
metis_sys::METIS_NodeND(
&mut nvtxs,
xadj.as_mut_ptr(),
adjncy.as_mut_ptr(),
std::ptr::null_mut(),
options.as_mut_ptr(),
perm_i32.as_mut_ptr(),
iperm_i32.as_mut_ptr(),
)
};
if ret != metis_sys::rstatus_et_METIS_OK {
let msg = match ret {
x if x == metis_sys::rstatus_et_METIS_ERROR_INPUT => "METIS_ERROR_INPUT: invalid input",
x if x == metis_sys::rstatus_et_METIS_ERROR_MEMORY => {
"METIS_ERROR_MEMORY: allocation failure"
}
x if x == metis_sys::rstatus_et_METIS_ERROR => "METIS_ERROR: general error",
_ => "METIS: unknown error code",
};
return Err(SparseError::AnalysisFailure {
reason: format!("{} (return code: {}, dim: {})", msg, ret, n),
});
}
Ok((perm_i32, iperm_i32))
}
fn extract_adjacency(
matrix: SymbolicSparseColMatRef<'_, usize>,
) -> Result<(Vec<i32>, Vec<i32>), SparseError> {
let n = matrix.nrows();
let col_ptrs = matrix.col_ptr();
let row_indices = matrix.row_idx();
let mut neighbors: Vec<Vec<usize>> = vec![Vec::new(); n];
for j in 0..n {
let start = col_ptrs[j];
let end = col_ptrs[j + 1];
for &i in &row_indices[start..end] {
if i != j {
neighbors[j].push(i);
neighbors[i].push(j);
}
}
}
for nbrs in &mut neighbors {
nbrs.sort_unstable();
nbrs.dedup();
}
let total_edges: usize = neighbors.iter().map(|v| v.len()).sum();
if total_edges > i32::MAX as usize {
return Err(SparseError::InvalidInput {
reason: format!(
"Total adjacency entries {} exceeds i32::MAX ({}); METIS requires 32-bit indices",
total_edges,
i32::MAX
),
});
}
let mut xadj = Vec::with_capacity(n + 1);
xadj.push(0i32);
let mut adjncy = Vec::with_capacity(total_edges);
for nbrs in &neighbors {
for &neighbor in nbrs {
adjncy.push(neighbor as i32);
}
xadj.push(adjncy.len() as i32);
}
Ok((xadj, adjncy))
}
#[cfg(test)]
mod tests {
use super::*;
use faer::sparse::{SparseColMat, Triplet};
fn make_tridiagonal(n: usize) -> SparseColMat<usize, f64> {
let mut triplets = Vec::new();
for i in 0..n {
triplets.push(Triplet::new(i, i, 4.0));
}
for i in 0..n - 1 {
triplets.push(Triplet::new(i, i + 1, 1.0));
triplets.push(Triplet::new(i + 1, i, 1.0));
}
SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
}
fn make_diagonal(n: usize) -> SparseColMat<usize, f64> {
let triplets: Vec<_> = (0..n).map(|i| Triplet::new(i, i, (i + 1) as f64)).collect();
SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
}
fn make_arrow(n: usize) -> SparseColMat<usize, f64> {
let mut triplets = Vec::new();
for i in 0..n {
triplets.push(Triplet::new(i, i, 10.0));
}
for i in 1..n {
triplets.push(Triplet::new(0, i, 1.0));
triplets.push(Triplet::new(i, 0, 1.0));
}
SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
}
fn make_upper_triangle_only(n: usize) -> SparseColMat<usize, f64> {
let mut triplets = Vec::new();
for i in 0..n {
triplets.push(Triplet::new(i, i, 4.0));
}
for i in 0..n - 1 {
triplets.push(Triplet::new(i, i + 1, 1.0));
}
SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
}
#[test]
fn test_metis_ordering_tridiagonal_5() {
let matrix = make_tridiagonal(5);
let perm = metis_ordering(matrix.symbolic()).expect("METIS ordering should succeed");
let (fwd, inv) = perm.as_ref().arrays();
assert_eq!(fwd.len(), 5);
assert_eq!(inv.len(), 5);
let mut seen_fwd = [false; 5];
let mut seen_inv = [false; 5];
for i in 0..5 {
assert!(fwd[i] < 5, "fwd[{}] = {} out of range", i, fwd[i]);
assert!(inv[i] < 5, "inv[{}] = {} out of range", i, inv[i]);
seen_fwd[fwd[i]] = true;
seen_inv[inv[i]] = true;
}
assert!(
seen_fwd.iter().all(|&s| s),
"fwd is not a valid permutation"
);
assert!(
seen_inv.iter().all(|&s| s),
"inv is not a valid permutation"
);
}
#[test]
fn test_metis_ordering_fwd_inv_consistency() {
let matrix = make_tridiagonal(10);
let perm = metis_ordering(matrix.symbolic()).expect("METIS ordering should succeed");
let (fwd, inv) = perm.as_ref().arrays();
for i in 0..10 {
assert_eq!(fwd[inv[i]], i, "fwd[inv[{}]] = {} != {}", i, fwd[inv[i]], i);
assert_eq!(inv[fwd[i]], i, "inv[fwd[{}]] = {} != {}", i, inv[fwd[i]], i);
}
}
#[test]
fn test_metis_ordering_dim_0() {
let triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
let matrix = SparseColMat::try_new_from_triplets(0, 0, &triplets).unwrap();
let perm = metis_ordering(matrix.symbolic()).expect("dim-0 should succeed");
let (fwd, inv) = perm.as_ref().arrays();
assert_eq!(fwd.len(), 0);
assert_eq!(inv.len(), 0);
}
#[test]
fn test_metis_ordering_dim_1() {
let matrix = make_diagonal(1);
let perm = metis_ordering(matrix.symbolic()).expect("dim-1 should succeed");
let (fwd, inv) = perm.as_ref().arrays();
assert_eq!(fwd, &[0]);
assert_eq!(inv, &[0]);
}
#[test]
fn test_metis_ordering_diagonal_identity() {
let matrix = make_diagonal(5);
let perm = metis_ordering(matrix.symbolic()).expect("diagonal should succeed");
let (fwd, inv) = perm.as_ref().arrays();
for i in 0..5 {
assert_eq!(
fwd[i], i,
"diagonal perm should be identity: fwd[{}] = {}",
i, fwd[i]
);
assert_eq!(
inv[i], i,
"diagonal perm should be identity: inv[{}] = {}",
i, inv[i]
);
}
}
#[test]
fn test_adjacency_tridiagonal_3x3() {
let matrix = make_tridiagonal(3);
let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
assert_eq!(xadj, vec![0, 1, 3, 4]);
assert_eq!(adjncy.len(), 4);
let n0: Vec<i32> = adjncy[xadj[0] as usize..xadj[1] as usize].to_vec();
let n1: Vec<i32> = adjncy[xadj[1] as usize..xadj[2] as usize].to_vec();
let n2: Vec<i32> = adjncy[xadj[2] as usize..xadj[3] as usize].to_vec();
assert_eq!(n0, vec![1]);
assert_eq!(n1, vec![0, 2]);
assert_eq!(n2, vec![1]);
}
#[test]
fn test_adjacency_arrow_5x5() {
let matrix = make_arrow(5);
let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
assert_eq!(xadj, vec![0, 4, 5, 6, 7, 8]);
assert_eq!(adjncy.len(), 8);
let n0: Vec<i32> = adjncy[xadj[0] as usize..xadj[1] as usize].to_vec();
assert_eq!(n0, vec![1, 2, 3, 4]);
for v in 1..5i32 {
let start = xadj[v as usize] as usize;
let end = xadj[v as usize + 1] as usize;
assert_eq!(
&adjncy[start..end],
&[0],
"vertex {} should only neighbor 0",
v
);
}
}
#[test]
fn test_adjacency_diagonal_only() {
let matrix = make_diagonal(4);
let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
assert_eq!(xadj, vec![0, 0, 0, 0, 0]);
assert!(adjncy.is_empty());
}
#[test]
fn test_adjacency_upper_triangle_only() {
let matrix = make_upper_triangle_only(4);
let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
assert_eq!(xadj, vec![0, 1, 3, 5, 6]);
assert_eq!(adjncy.len(), 6);
let n = 4;
for i in 0..n {
let start = xadj[i] as usize;
let end = xadj[i + 1] as usize;
for &j in &adjncy[start..end] {
let j_start = xadj[j as usize] as usize;
let j_end = xadj[j as usize + 1] as usize;
assert!(
adjncy[j_start..j_end].contains(&(i as i32)),
"symmetry violated: ({}, {}) exists but ({}, {}) does not",
i,
j,
j,
i
);
}
}
}
#[test]
fn test_adjacency_invariants() {
for (name, matrix) in [
("tridiag3", make_tridiagonal(3)),
("arrow5", make_arrow(5)),
("diag4", make_diagonal(4)),
("upper4", make_upper_triangle_only(4)),
] {
let n = matrix.nrows();
let (xadj, adjncy) = extract_adjacency(matrix.symbolic()).unwrap();
assert_eq!(xadj[0], 0, "{}: xadj[0] should be 0", name);
assert_eq!(xadj.len(), n + 1, "{}: xadj length should be n+1", name);
for i in 0..n {
assert!(
xadj[i + 1] >= xadj[i],
"{}: xadj not monotonic at {}: {} > {}",
name,
i,
xadj[i],
xadj[i + 1]
);
}
for i in 0..n {
let start = xadj[i] as usize;
let end = xadj[i + 1] as usize;
for &j in &adjncy[start..end] {
assert_ne!(j, i as i32, "{}: self-loop at vertex {}", name, i);
}
}
for &j in &adjncy {
assert!(
j >= 0 && (j as usize) < n,
"{}: adjncy index {} out of range [0, {})",
name,
j,
n
);
}
for i in 0..n {
let start = xadj[i] as usize;
let end = xadj[i + 1] as usize;
for &j in &adjncy[start..end] {
let j_start = xadj[j as usize] as usize;
let j_end = xadj[j as usize + 1] as usize;
assert!(
adjncy[j_start..j_end].contains(&(i as i32)),
"{}: symmetry violated: ({}, {}) without ({}, {})",
name,
i,
j,
j,
i
);
}
}
}
}
#[test]
fn test_split_all_singletons() {
let fwd = vec![0, 1, 2, 3];
let is_matched = vec![true, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
assert_eq!(decomp.singletons, 4);
assert_eq!(decomp.two_cycles, 0);
assert_eq!(decomp.condensed_dim, 4);
for i in 0..4 {
assert_eq!(decomp.partner[i], -1, "index {} should be singleton", i);
}
}
#[test]
fn test_split_pure_2_cycles() {
let fwd = vec![1, 0, 3, 2];
let is_matched = vec![true, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
assert_eq!(decomp.singletons, 0);
assert_eq!(decomp.two_cycles, 2);
assert_eq!(decomp.condensed_dim, 2);
assert_eq!(decomp.partner[0], 1);
assert_eq!(decomp.partner[1], 0);
assert_eq!(decomp.partner[2], 3);
assert_eq!(decomp.partner[3], 2);
assert_eq!(decomp.old_to_new[0], decomp.old_to_new[1]);
assert_eq!(decomp.old_to_new[2], decomp.old_to_new[3]);
assert_ne!(decomp.old_to_new[0], decomp.old_to_new[2]);
}
#[test]
fn test_split_3_cycle() {
let fwd = vec![1, 2, 0];
let is_matched = vec![true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 3);
assert_eq!(decomp.two_cycles, 1);
assert_eq!(decomp.singletons, 1);
assert_eq!(decomp.condensed_dim, 2);
let pairs: Vec<_> = (0..3).filter(|&i| decomp.partner[i] >= 0).collect();
let sings: Vec<_> = (0..3).filter(|&i| decomp.partner[i] == -1).collect();
assert_eq!(pairs.len(), 2);
assert_eq!(sings.len(), 1);
}
#[test]
fn test_split_4_cycle() {
let fwd = vec![1, 2, 3, 0];
let is_matched = vec![true, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
assert_eq!(decomp.two_cycles, 2);
assert_eq!(decomp.singletons, 0);
assert_eq!(decomp.condensed_dim, 2);
}
#[test]
fn test_split_5_cycle() {
let fwd = vec![1, 2, 3, 4, 0];
let is_matched = vec![true, true, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 5);
assert_eq!(decomp.two_cycles, 2);
assert_eq!(decomp.singletons, 1);
assert_eq!(decomp.condensed_dim, 3);
}
#[test]
fn test_split_mixed_with_unmatched() {
let fwd = vec![1, 0, 2, 4, 3, 5]; let is_matched = vec![true, true, true, true, true, false];
let decomp = split_matching_cycles(&fwd, &is_matched, 6);
assert_eq!(decomp.two_cycles, 2);
assert_eq!(decomp.singletons, 2); assert_eq!(decomp.condensed_dim, 4); assert_eq!(decomp.partner[5], -2); assert_eq!(decomp.partner[2], -1); assert!(
decomp.old_to_new[5] < decomp.condensed_dim,
"unmatched node should have valid condensed index"
);
}
#[test]
fn test_split_trivial_n0() {
let decomp = split_matching_cycles(&[], &[], 0);
assert_eq!(decomp.condensed_dim, 0);
assert_eq!(decomp.singletons, 0);
assert_eq!(decomp.two_cycles, 0);
}
#[test]
fn test_split_trivial_n1() {
let decomp = split_matching_cycles(&[0], &[true], 1);
assert_eq!(decomp.condensed_dim, 1);
assert_eq!(decomp.singletons, 1);
assert_eq!(decomp.two_cycles, 0);
assert_eq!(decomp.partner[0], -1);
}
fn make_upper_tri(n: usize, entries: &[(usize, usize, f64)]) -> SparseColMat<usize, f64> {
let triplets: Vec<_> = entries
.iter()
.map(|&(i, j, v)| Triplet::new(i, j, v))
.collect();
SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
}
#[test]
fn test_condensed_adjacency_arrow_with_2cycle() {
let matrix = make_arrow(4);
let fwd = vec![1, 0, 2, 3];
let is_matched = vec![true, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
assert_eq!(decomp.condensed_dim, 3);
let (xadj, adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp).unwrap();
assert_eq!(xadj.len(), 4);
for i in 0..3 {
let start = xadj[i] as usize;
let end = xadj[i + 1] as usize;
for &j in &adjncy[start..end] {
assert_ne!(j, i as i32, "self-loop at condensed vertex {}", i);
}
}
let total = adjncy.len();
assert!(total > 0, "condensed graph should have edges");
}
#[test]
fn test_condensed_adjacency_diagonal() {
let matrix = make_diagonal(4);
let fwd = vec![0, 1, 2, 3];
let is_matched = vec![true, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
let (xadj, adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp).unwrap();
assert!(
adjncy.is_empty(),
"diagonal should have zero condensed edges"
);
assert_eq!(xadj, vec![0, 0, 0, 0, 0]);
}
#[test]
fn test_condensed_adjacency_full_4x4_two_pairs() {
let matrix = make_upper_tri(
4,
&[
(0, 0, 1.0),
(0, 1, 1.0),
(0, 2, 1.0),
(0, 3, 1.0),
(1, 1, 1.0),
(1, 2, 1.0),
(1, 3, 1.0),
(2, 2, 1.0),
(2, 3, 1.0),
(3, 3, 1.0),
],
);
let fwd = vec![1, 0, 3, 2];
let is_matched = vec![true, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
assert_eq!(decomp.condensed_dim, 2);
let (xadj, adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp).unwrap();
assert_eq!(xadj.len(), 3);
assert_eq!(adjncy.len(), 2);
let n0_start = xadj[0] as usize;
let n0_end = xadj[1] as usize;
let n1_start = xadj[1] as usize;
let n1_end = xadj[2] as usize;
assert!(adjncy[n0_start..n0_end].contains(&1));
assert!(adjncy[n1_start..n1_end].contains(&0));
}
#[test]
fn test_expand_ordering_with_pairs() {
let fwd = vec![1, 0, 3, 2];
let is_matched = vec![true, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
let condensed_order = vec![1i32, 0i32];
let perm = expand_ordering(&condensed_order, &decomp, 4);
let (expanded_fwd, expanded_inv) = perm.as_ref().arrays();
assert_eq!(expanded_fwd.len(), 4);
let mut seen = [false; 4];
for &v in expanded_fwd {
assert!(v < 4);
seen[v] = true;
}
assert!(seen.iter().all(|&s| s), "not a valid permutation");
let pos_0 = expanded_inv[0];
let pos_1 = expanded_inv[1];
assert_eq!(pos_0.abs_diff(pos_1), 1, "pair (0,1) not consecutive");
let pos_2 = expanded_inv[2];
let pos_3 = expanded_inv[3];
assert_eq!(pos_2.abs_diff(pos_3), 1, "pair (2,3) not consecutive");
}
#[test]
fn test_expand_ordering_mixed_singletons_pairs() {
let fwd = vec![1, 0, 2, 4, 3];
let is_matched = vec![true, true, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 5);
assert_eq!(decomp.condensed_dim, 3);
let condensed_order = vec![0i32, 1i32, 2i32];
let perm = expand_ordering(&condensed_order, &decomp, 5);
let (expanded_fwd, expanded_inv) = perm.as_ref().arrays();
assert_eq!(expanded_fwd.len(), 5);
let mut seen = [false; 5];
for &v in expanded_fwd {
seen[v] = true;
}
assert!(seen.iter().all(|&s| s));
let diff = (expanded_inv[0] as isize - expanded_inv[1] as isize).unsigned_abs();
assert_eq!(diff, 1, "pair (0,1) not consecutive");
let diff = (expanded_inv[3] as isize - expanded_inv[4] as isize).unsigned_abs();
assert_eq!(diff, 1, "pair (3,4) not consecutive");
}
#[test]
fn test_expand_ordering_with_unmatched() {
let fwd = vec![1, 0, 2, 3, 4];
let is_matched = vec![true, true, true, false, false];
let decomp = split_matching_cycles(&fwd, &is_matched, 5);
assert_eq!(decomp.condensed_dim, 4);
let condensed_order = vec![0i32, 1i32, 2i32, 3i32];
let perm = expand_ordering(&condensed_order, &decomp, 5);
let (expanded_fwd, expanded_inv) = perm.as_ref().arrays();
assert_eq!(expanded_fwd.len(), 5);
let mut seen = [false; 5];
for &v in expanded_fwd {
assert!(v < 5);
seen[v] = true;
}
assert!(seen.iter().all(|&s| s), "not a valid permutation");
let diff = (expanded_inv[0] as isize - expanded_inv[1] as isize).unsigned_abs();
assert_eq!(diff, 1, "pair (0,1) not consecutive");
}
#[test]
fn test_split_unmatched_get_condensed_indices() {
let fwd = vec![1, 0, 2, 3]; let is_matched = vec![true, true, false, false];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
assert_eq!(decomp.condensed_dim, 3); assert!(decomp.old_to_new[2] < decomp.condensed_dim);
assert!(decomp.old_to_new[3] < decomp.condensed_dim);
assert_ne!(decomp.old_to_new[2], decomp.old_to_new[3]);
assert_eq!(decomp.old_to_new[0], decomp.old_to_new[1]);
}
#[test]
fn test_condensed_adjacency_includes_unmatched_edges() {
let matrix = make_upper_tri(
4,
&[
(0, 0, 1.0),
(0, 1, 1.0),
(0, 2, 1.0),
(1, 1, 1.0),
(2, 2, 1.0),
(3, 3, 1.0),
],
);
let fwd = vec![1, 0, 2, 3];
let is_matched = vec![true, true, false, false];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
let (xadj, adjncy) = build_condensed_adjacency(matrix.symbolic(), &decomp).unwrap();
assert_eq!(xadj.len(), 4);
let c_pair = decomp.old_to_new[0];
let c_unmatched_2 = decomp.old_to_new[2];
let pair_start = xadj[c_pair] as usize;
let pair_end = xadj[c_pair + 1] as usize;
let pair_neighbors: Vec<i32> = adjncy[pair_start..pair_end].to_vec();
assert!(
pair_neighbors.contains(&(c_unmatched_2 as i32)),
"pair should neighbor unmatched node 2; neighbors = {:?}",
pair_neighbors
);
let c_unmatched_3 = decomp.old_to_new[3];
let n3_start = xadj[c_unmatched_3] as usize;
let n3_end = xadj[c_unmatched_3 + 1] as usize;
assert_eq!(
n3_end - n3_start,
0,
"isolated unmatched node 3 should have no neighbors"
);
}
#[test]
fn test_expand_unmatched_not_at_end() {
let fwd = vec![1, 0, 2, 3, 5, 4];
let is_matched = vec![true, true, false, true, true, true];
let decomp = split_matching_cycles(&fwd, &is_matched, 6);
let cdim = decomp.condensed_dim;
let c_unmatched = decomp.old_to_new[2];
let mut condensed_order = vec![0i32; cdim];
condensed_order[c_unmatched] = 0;
let mut pos = 1i32;
for (cnode, order) in condensed_order.iter_mut().enumerate().take(cdim) {
if cnode == c_unmatched {
continue;
}
*order = pos;
pos += 1;
}
let perm = expand_ordering(&condensed_order, &decomp, 6);
let (expanded_fwd, expanded_inv) = perm.as_ref().arrays();
assert_eq!(
expanded_inv[2], 0,
"unmatched node should be at position 0, got {}",
expanded_inv[2]
);
let mut seen = [false; 6];
for &v in expanded_fwd {
assert!(v < 6);
seen[v] = true;
}
assert!(seen.iter().all(|&s| s));
}
#[test]
fn test_all_unmatched_gets_ordering() {
let fwd = vec![0, 1, 2, 3];
let is_matched = vec![false, false, false, false];
let decomp = split_matching_cycles(&fwd, &is_matched, 4);
assert_eq!(decomp.condensed_dim, 4);
assert_eq!(decomp.singletons, 4);
assert_eq!(decomp.two_cycles, 0);
for i in 0..4 {
assert!(decomp.old_to_new[i] < 4);
}
let condensed_order = vec![0i32, 1, 2, 3];
let perm = expand_ordering(&condensed_order, &decomp, 4);
let (fwd_expanded, _) = perm.as_ref().arrays();
assert_eq!(fwd_expanded.len(), 4);
let mut seen = [false; 4];
for &v in fwd_expanded {
seen[v] = true;
}
assert!(seen.iter().all(|&s| s));
}
}