use pounce_common::types::{Index, Number};
#[derive(Debug, Clone)]
pub struct EqRow {
pub cols: Vec<Index>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LicqVerdict {
Full,
EmptyRow(Index),
OverDetermined { m_eq: Index, n: Index },
StructuralRank(Index),
}
pub fn licq_check(rows: &[EqRow], n: Index) -> LicqVerdict {
let m = rows.len() as Index;
if m == 0 {
return LicqVerdict::Full;
}
if m > n {
return LicqVerdict::OverDetermined { m_eq: m, n };
}
for (i, r) in rows.iter().enumerate() {
if r.cols.is_empty() {
return LicqVerdict::EmptyRow(i as Index);
}
}
let rank = bipartite_matching_rank(rows, n as usize);
if rank == rows.len() {
LicqVerdict::Full
} else {
LicqVerdict::StructuralRank(rank as Index)
}
}
fn bipartite_matching_rank(rows: &[EqRow], n: usize) -> usize {
use crate::incidence::EqualityIncidence;
use crate::matching::hopcroft_karp;
let mut adj_ptr: Vec<usize> = Vec::with_capacity(rows.len() + 1);
let mut vars: Vec<usize> = Vec::new();
let mut scratch: Vec<usize> = Vec::new();
adj_ptr.push(0);
for row in rows {
scratch.clear();
scratch.extend(row.cols.iter().filter_map(|&c| {
let c = c as usize;
(c < n).then_some(c)
}));
scratch.sort_unstable();
scratch.dedup();
vars.extend_from_slice(&scratch);
adj_ptr.push(vars.len());
}
let inc = EqualityIncidence {
n_vars: n,
eq_row_inner_idx: (0..rows.len()).collect(),
adj_ptr,
vars,
};
hopcroft_karp(&inc).size
}
pub fn eq_rows_from_triples(
eq_row_indices: &[usize],
triples: &[(Index, Index, Number)],
inner_m: usize,
) -> Vec<EqRow> {
use std::collections::BTreeSet;
let mut by_row: Vec<BTreeSet<Index>> = vec![BTreeSet::new(); inner_m];
for &(i, j, v) in triples {
if v == 0.0 {
continue;
}
let i = i as usize;
if i < inner_m {
by_row[i].insert(j);
}
}
eq_row_indices
.iter()
.map(|&i| EqRow {
cols: by_row[i].iter().copied().collect(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn row(cols: &[Index]) -> EqRow {
EqRow {
cols: cols.to_vec(),
}
}
#[test]
fn no_equality_rows_is_full_rank() {
assert_eq!(licq_check(&[], 5), LicqVerdict::Full);
}
#[test]
fn over_determined_caught() {
let rows = vec![row(&[0]), row(&[0]), row(&[0])];
assert!(matches!(
licq_check(&rows, 2),
LicqVerdict::OverDetermined { m_eq: 3, n: 2 }
));
}
#[test]
fn empty_row_caught() {
let rows = vec![row(&[0]), row(&[])];
assert!(matches!(licq_check(&rows, 5), LicqVerdict::EmptyRow(1)));
}
#[test]
fn duplicate_singletons_dropped_by_matching() {
let rows = vec![row(&[0]), row(&[0])];
assert!(matches!(
licq_check(&rows, 5),
LicqVerdict::StructuralRank(1)
));
}
#[test]
fn distinct_singletons_full_rank() {
let rows = vec![row(&[0]), row(&[1]), row(&[2])];
assert_eq!(licq_check(&rows, 5), LicqVerdict::Full);
}
#[test]
fn matching_via_augmenting_path() {
let rows = vec![row(&[0, 1]), row(&[0])];
assert_eq!(licq_check(&rows, 2), LicqVerdict::Full);
}
#[test]
fn long_chain_does_not_overflow_stack() {
let m = 50_000usize;
let mut rows: Vec<EqRow> = Vec::with_capacity(m);
rows.push(row(&[0]));
for i in 1..(m - 1) {
rows.push(row(&[(i - 1) as Index, i as Index]));
}
rows.push(row(&[(m - 2) as Index]));
assert_eq!(
licq_check(&rows, m as Index),
LicqVerdict::StructuralRank((m - 1) as Index)
);
}
#[test]
fn long_chain_full_rank() {
let m = 20_000usize;
let mut rows: Vec<EqRow> = Vec::with_capacity(m);
rows.push(row(&[0]));
for i in 1..m {
rows.push(row(&[(i - 1) as Index, i as Index]));
}
assert_eq!(licq_check(&rows, m as Index), LicqVerdict::Full);
}
}