use crate::factor::count_f64;
use crate::perm::idx32;
use crate::qgraph::{self, Input};
use crate::real::lt;
use crate::{Pattern, Perm, SparseError};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OrderingTarget {
Symmetric,
Columns,
}
pub trait Ordering {
fn order(&self, pattern: &Pattern, target: OrderingTarget) -> Result<Perm, SparseError>;
fn name(&self) -> &'static str;
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Natural;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Amd {
pub dense: Option<f64>,
pub aggressive: bool,
}
impl Default for Amd {
fn default() -> Self {
Self {
dense: None,
aggressive: true,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Colamd {
pub dense_row: Option<f64>,
pub dense_col: Option<f64>,
}
const DENSE_DEFAULT: f64 = 10.0;
const DENSE_MIN: usize = 16;
fn dense_threshold(alpha: Option<f64>, big_n: usize) -> usize {
let a = alpha.unwrap_or(DENSE_DEFAULT);
if lt(a, 0.0) {
return usize::MAX;
}
floor_usize(a * count_f64(big_n).sqrt()).max(DENSE_MIN)
}
#[inline]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn floor_usize(x: f64) -> usize {
x as usize
}
fn square(pattern: &Pattern) -> Result<usize, SparseError> {
let n = pattern.n_cols();
if pattern.n_rows == n {
Ok(n)
} else {
Err(SparseError::Dimension {
what: "pattern (a symmetric ordering needs a square one)",
expected: n,
found: pattern.n_rows,
})
}
}
fn adjacency(g: &Pattern) -> Vec<Vec<u32>> {
(0..g.n_cols())
.map(|j| {
g.col(j)
.iter()
.copied()
.filter(|&i| i as usize != j)
.collect()
})
.collect()
}
fn sym_adjacency(s: &Pattern) -> Vec<Vec<u32>> {
let t = s.transpose();
(0..s.n_cols())
.map(|j| {
let (a, b) = (s.col(j), t.col(j));
let jj = idx32(j);
let mut out = Vec::with_capacity(a.len() + b.len());
let (mut p, mut q) = (0, 0);
while p < a.len() && q < b.len() {
let (x, y) = (a[p], b[q]);
let next = x.min(y);
p += usize::from(x == next);
q += usize::from(y == next);
if next != jj {
out.push(next);
}
}
out.extend(a[p..].iter().chain(&b[q..]).copied().filter(|&i| i != jj));
out
})
.collect()
}
fn finish(n: usize, mut fwd: Vec<u32>, groups: &[&[bool]]) -> Perm {
for g in groups {
fwd.extend((0..n).filter(|&j| g[j]).map(idx32));
}
debug_assert_eq!(fwd.len(), n);
let p = Perm::from_fwd(fwd).expect("(O1): the ordering is a permutation");
debug_assert_eq!(p.check_invariants(), Ok(()));
p
}
impl Ordering for Natural {
fn order(&self, pattern: &Pattern, target: OrderingTarget) -> Result<Perm, SparseError> {
pattern.check_invariants()?;
let n = match target {
OrderingTarget::Symmetric => square(pattern)?,
OrderingTarget::Columns => pattern.n_cols(),
};
Ok(Perm::identity(n))
}
fn name(&self) -> &'static str {
"natural"
}
}
impl Ordering for Amd {
fn order(&self, pattern: &Pattern, target: OrderingTarget) -> Result<Perm, SparseError> {
pattern.check_invariants()?;
let adj = match target {
OrderingTarget::Symmetric => {
square(pattern)?;
sym_adjacency(pattern)
}
OrderingTarget::Columns => adjacency(&pattern.ata()),
};
let n = adj.len();
let delta = dense_threshold(self.dense, n);
let excluded: Vec<bool> = adj.iter().map(|a| a.len() > delta).collect();
let fwd = qgraph::order(Input {
n,
adj,
elems: Vec::new(),
excluded: excluded.clone(),
aggressive: self.aggressive,
previous_degree: true,
column_order: false,
});
Ok(finish(n, fwd, &[excluded.as_slice()]))
}
fn name(&self) -> &'static str {
"amd"
}
}
impl Ordering for Colamd {
fn order(&self, pattern: &Pattern, target: OrderingTarget) -> Result<Perm, SparseError> {
pattern.check_invariants()?;
match target {
OrderingTarget::Columns => Ok(self.columns(pattern)),
OrderingTarget::Symmetric => {
square(pattern)?;
Ok(self.symmetric(&pattern.symmetric()?))
}
}
}
fn name(&self) -> &'static str {
"colamd"
}
}
impl Colamd {
fn columns(self, s: &Pattern) -> Perm {
let (m, n) = (s.n_rows, s.n_cols());
let kc = dense_threshold(self.dense_col, m.min(n));
let dense: Vec<bool> = (0..n).map(|j| s.col(j).len() > kc).collect();
let t = s.transpose();
let kr = dense_threshold(self.dense_row, n);
let mut elems = Vec::with_capacity(m);
let mut has_row = vec![false; n];
for i in 0..m {
let row: Vec<u32> = t
.col(i)
.iter()
.copied()
.filter(|&j| !dense[j as usize])
.collect();
if row.is_empty() || row.len() > kr {
continue;
}
for &j in &row {
has_row[j as usize] = true;
}
elems.push(row);
}
let null: Vec<bool> = (0..n).map(|j| !dense[j] && !has_row[j]).collect();
let excluded: Vec<bool> = (0..n).map(|j| dense[j] || null[j]).collect();
let fwd = qgraph::order(Input {
n,
adj: vec![Vec::new(); n],
elems,
excluded,
aggressive: true,
previous_degree: false,
column_order: true,
});
finish(n, fwd, &[null.as_slice(), dense.as_slice()])
}
fn symmetric(self, g: &Pattern) -> Perm {
let n = g.n_cols();
let adj = adjacency(g);
let kc = dense_threshold(self.dense_col, n);
let dense: Vec<bool> = adj.iter().map(|a| a.len() > kc).collect();
let mut elems = Vec::new();
for (j, a) in adj.iter().enumerate() {
if dense[j] {
continue;
}
for &i in a {
if (i as usize) < j && !dense[i as usize] {
elems.push(vec![i, idx32(j)]);
}
}
}
let fwd = qgraph::order(Input {
n,
adj: vec![Vec::new(); n],
elems,
excluded: dense.clone(),
aggressive: true,
previous_degree: false,
column_order: true,
});
finish(n, fwd, &[dense.as_slice()])
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(seed | 1)
}
fn below(&mut self, n: usize) -> usize {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
usize::try_from(self.0.wrapping_mul(0x2545_F491_4F6C_DD1D) % (n as u64)).unwrap()
}
fn shuffle(&mut self, v: &mut [u32]) {
for i in (1..v.len()).rev() {
let j = self.below(i + 1);
v.swap(i, j);
}
}
}
fn pat(m: usize, n: usize, e: &[(usize, usize)]) -> Pattern {
let mut cols: Vec<Vec<u32>> = vec![Vec::new(); n];
for &(i, j) in e {
cols[j].push(idx32(i));
}
let mut col_ptr = vec![0];
let mut row_idx = Vec::new();
for mut c in cols {
c.sort_unstable();
c.dedup();
row_idx.extend(c);
col_ptr.push(row_idx.len());
}
Pattern::from_raw_parts(m, col_ptr, row_idx).unwrap()
}
fn graph(n: usize, edges: &[(usize, usize)], label: &[u32]) -> Pattern {
let mut e: Vec<(usize, usize)> = (0..n).map(|i| (i, i)).collect();
for &(a, b) in edges {
let (a, b) = (label[a] as usize, label[b] as usize);
e.push((a, b));
e.push((b, a));
}
pat(n, n, &e)
}
fn random_label(rng: &mut Rng, n: usize) -> Vec<u32> {
let mut l: Vec<u32> = (0..n).map(idx32).collect();
rng.shuffle(&mut l);
l
}
fn tree_edges(rng: &mut Rng, n: usize) -> Vec<(usize, usize)> {
(1..n).map(|i| (i, rng.below(i))).collect()
}
fn grid_edges(k: usize) -> Vec<(usize, usize)> {
let mut e = Vec::new();
for r in 0..k {
for c in 0..k {
let v = r * k + c;
if c + 1 < k {
e.push((v, v + 1));
}
if r + 1 < k {
e.push((v, v + k));
}
}
}
e
}
fn incidence(n: usize, edges: &[(usize, usize)], label: &[u32]) -> Pattern {
let mut e = Vec::new();
for (r, &(a, b)) in edges.iter().enumerate() {
e.push((r, label[a] as usize));
e.push((r, label[b] as usize));
}
pat(edges.len().max(1), n, &e)
}
fn fill(s: &Pattern, p: &Perm) -> usize {
s.cholesky_nnz(p).unwrap()
}
fn exact_minimum_degree(s: &Pattern) -> Perm {
let n = s.n_cols();
let g = s.symmetric().unwrap();
let mut adj: Vec<Vec<bool>> = vec![vec![false; n]; n];
for (j, row) in adj.iter_mut().enumerate() {
for &i in g.col(j) {
if i as usize != j {
row[i as usize] = true;
}
}
}
let mut alive = vec![true; n];
let mut fwd = Vec::with_capacity(n);
for _ in 0..n {
let degree = |v: usize| (0..n).filter(|&w| alive[w] && adj[v][w]).count();
let p = (0..n)
.filter(|&v| alive[v])
.min_by_key(|&v| (degree(v), v))
.unwrap();
let nb: Vec<usize> = (0..n).filter(|&w| alive[w] && adj[p][w]).collect();
for &a in &nb {
for &b in &nb {
if a != b {
adj[a][b] = true;
}
}
}
alive[p] = false;
fwd.push(idx32(p));
}
Perm::from_fwd(fwd).unwrap()
}
fn optimal_fill(s: &Pattern) -> usize {
fn rec(k: usize, v: &mut Vec<u32>, s: &Pattern, best: &mut usize) {
if k == v.len() {
let f = s.cholesky_nnz(&Perm::from_fwd(v.clone()).unwrap()).unwrap();
*best = (*best).min(f);
return;
}
for i in k..v.len() {
v.swap(k, i);
rec(k + 1, v, s, best);
v.swap(k, i);
}
}
let mut v: Vec<u32> = (0..s.n_cols()).map(idx32).collect();
let mut best = usize::MAX;
rec(0, &mut v, s, &mut best);
best
}
fn random_graph(rng: &mut Rng, n: usize, pct: usize) -> Pattern {
let mut e = Vec::new();
for a in 0..n {
for b in a + 1..n {
if rng.below(100) < pct {
e.push((a, b));
}
}
}
let id: Vec<u32> = (0..n).map(idx32).collect();
graph(n, &e, &id)
}
#[test]
fn natural_and_errors() {
let s = pat(2, 3, &[(0, 0), (1, 2)]);
assert!(
Natural
.order(&s, OrderingTarget::Columns)
.unwrap()
.is_identity()
);
assert!(Natural.order(&s, OrderingTarget::Symmetric).is_err());
assert!(Amd::default().order(&s, OrderingTarget::Symmetric).is_err());
assert!(
Colamd::default()
.order(&s, OrderingTarget::Symmetric)
.is_err()
);
assert_eq!(
Amd::default()
.order(&s, OrderingTarget::Columns)
.unwrap()
.len(),
3
);
assert_eq!(
Colamd::default()
.order(&s, OrderingTarget::Columns)
.unwrap()
.len(),
3
);
let empty = Pattern::empty(0, 0);
for o in [
&Natural as &dyn Ordering,
&Amd::default(),
&Colamd::default(),
] {
assert!(
o.order(&empty, OrderingTarget::Symmetric)
.unwrap()
.is_empty()
);
assert_ne!(o.name(), "");
}
assert_eq!(dense_threshold(None, 100), 100);
assert_eq!(dense_threshold(Some(1.0), 4), DENSE_MIN);
assert_eq!(dense_threshold(Some(-1.0), 4), usize::MAX);
}
#[test]
fn long_path_restarts_the_flag_and_stays_perfect() {
let n = 100_000;
let edges: Vec<(usize, usize)> = (1..n).map(|i| (i, i - 1)).collect();
let s = pat(n, n, &edges).symmetric().unwrap();
for o in [&Amd::default() as &dyn Ordering, &Colamd::default()] {
let p = o.order(&s, OrderingTarget::Symmetric).unwrap();
assert_eq!(s.cholesky_nnz(&p).unwrap(), 2 * n - 1, "{}", o.name());
}
}
#[test]
fn ex_5_10_1_arrowhead() {
for n in [8_usize, 30, 400] {
let edges: Vec<(usize, usize)> = (1..n).map(|i| (0, i)).collect();
let id: Vec<u32> = (0..n).map(idx32).collect();
let s = graph(n, &edges, &id);
let nat = fill(&s, &Natural.order(&s, OrderingTarget::Symmetric).unwrap());
assert_eq!(nat, n * (n + 1) / 2);
for o in [&Amd::default() as &dyn Ordering, &Colamd::default()] {
let p = o.order(&s, OrderingTarget::Symmetric).unwrap();
assert_eq!(fill(&s, &p), 2 * n - 1, "{} n = {n}", o.name());
assert!(
p.inv[0] as usize + 2 >= n,
"{}: hub at {}",
o.name(),
p.inv[0]
);
}
let m = incidence(n, &edges, &id);
let p = Colamd::default()
.order(&m, OrderingTarget::Columns)
.unwrap();
assert_eq!(fill(&s, &p), 2 * n - 1);
}
}
#[test]
fn ex_5_10_2_grids_beat_the_band() {
let mut rng = Rng::new(7);
for k in [6_usize, 10, 20, 30] {
let n = k * k;
let e = grid_edges(k);
let id: Vec<u32> = (0..n).map(idx32).collect();
let band = fill(&graph(n, &e, &id), &Perm::identity(n));
let label = random_label(&mut rng, n);
let s = graph(n, &e, &label);
let amd = fill(
&s,
&Amd::default().order(&s, OrderingTarget::Symmetric).unwrap(),
);
let sym = fill(
&s,
&Colamd::default()
.order(&s, OrderingTarget::Symmetric)
.unwrap(),
);
let m = incidence(n, &e, &label);
let col = fill(
&s,
&Colamd::default()
.order(&m, OrderingTarget::Columns)
.unwrap(),
);
assert!(
amd <= band && sym <= band && col <= band,
"k {k}: band {band}, amd {amd}, symamd {sym}, colamd {col}"
);
}
}
#[test]
fn brute_force_against_minimum_degree_and_optimum() {
let mut rng = Rng::new(2026);
let (mut amd_s, mut col_s, mut md_s, mut opt_s) = (0, 0, 0, 0);
for case in 0..300 {
let n = 2 + case % 6;
let s = random_graph(&mut rng, n, 20 + case % 50);
let opt = optimal_fill(&s);
let amd = fill(
&s,
&Amd::default().order(&s, OrderingTarget::Symmetric).unwrap(),
);
let mut edges = Vec::new();
for j in 0..n {
for &i in s.col(j) {
if (i as usize) < j {
edges.push((i as usize, j));
}
}
}
let id: Vec<u32> = (0..n).map(idx32).collect();
let m = incidence(n, &edges, &id);
let col = fill(
&s,
&Colamd::default()
.order(&m, OrderingTarget::Columns)
.unwrap(),
);
let md = fill(&s, &exact_minimum_degree(&s));
assert!(amd >= opt && col >= opt && md >= opt);
amd_s += amd;
col_s += col;
md_s += md;
opt_s += opt;
}
let r = |a: usize, b: usize| count_f64(a) / count_f64(b);
assert!(r(amd_s, md_s) <= 1.10, "AMD/MD {}", r(amd_s, md_s));
assert!(r(col_s, md_s) <= 1.10, "COLAMD/MD {}", r(col_s, md_s));
assert!(r(amd_s, opt_s) <= 1.25 && r(col_s, opt_s) <= 1.25);
}
#[test]
fn amd_tracks_exact_minimum_degree() {
let mut rng = Rng::new(11);
let (mut amd_s, mut md_s) = (0, 0);
for case in 0..40 {
let n = 20 + case;
let s = random_graph(&mut rng, n, 3 + case % 8);
amd_s += fill(
&s,
&Amd::default().order(&s, OrderingTarget::Symmetric).unwrap(),
);
md_s += fill(&s, &exact_minimum_degree(&s));
}
assert!(
count_f64(amd_s) <= 1.10 * count_f64(md_s),
"AMD {amd_s} MD {md_s}"
);
}
proptest! {
#[test]
fn orderings_are_permutations(m in 1_usize..40, n in 1_usize..40,
e in prop::collection::vec((0_usize..40, 0_usize..40), 0..300),
dense_line in 0_usize..3, knob in -1.0_f64..2.0) {
let mut e: Vec<(usize, usize)> = e.into_iter().map(|(i, j)| (i % m, j % n)).collect();
if dense_line == 1 {
e.extend((0..n).map(|j| (0, j)));
} else if dense_line == 2 {
e.extend((0..m).map(|i| (i, 0)));
}
let s = pat(m, n, &e);
let sq = pat(n, n, &e.iter().map(|&(i, j)| (i % n, j)).collect::<Vec<_>>());
let orderings: [&dyn Ordering; 5] = [
&Natural,
&Amd::default(),
&Amd { dense: Some(knob), aggressive: false },
&Colamd::default(),
&Colamd { dense_row: Some(knob), dense_col: Some(knob) },
];
for o in orderings {
let p = o.order(&s, OrderingTarget::Columns).unwrap();
prop_assert_eq!(p.len(), n);
prop_assert!(p.check_invariants().is_ok());
let q = o.order(&sq, OrderingTarget::Symmetric).unwrap();
prop_assert_eq!(q.len(), n);
prop_assert!(q.check_invariants().is_ok());
prop_assert_eq!(o.order(&sq, OrderingTarget::Symmetric).unwrap(), q);
}
}
#[test]
fn trees_have_no_fill(n in 1_usize..120, seed in any::<u64>()) {
let mut rng = Rng::new(seed);
let e = tree_edges(&mut rng, n);
let label = random_label(&mut rng, n);
let s = graph(n, &e, &label);
let none = 2 * n - 1;
prop_assert!(fill(&s, &Perm::identity(n)) >= none);
prop_assert_eq!(fill(&s, &Amd::default().order(&s, OrderingTarget::Symmetric).unwrap()), none);
prop_assert_eq!(fill(&s, &Colamd::default().order(&s, OrderingTarget::Symmetric).unwrap()), none);
let m = incidence(n, &e, &label);
prop_assert_eq!(fill(&s, &Colamd::default().order(&m, OrderingTarget::Columns).unwrap()), none);
prop_assert_eq!(fill(&s, &Amd::default().order(&m, OrderingTarget::Columns).unwrap()), none);
}
#[test]
fn block_arrows_beat_the_natural_order(blocks in 2_usize..8, size in 2_usize..6,
w in 1_usize..4, seed in any::<u64>()) {
prop_assume!(size + w <= blocks * size);
let mut rng = Rng::new(seed);
let n = w + blocks * size;
let mut e = Vec::new();
for b in 0..blocks {
let base = w + b * size;
for x in 0..size {
for y in x + 1..size {
e.push((base + x, base + y));
}
for c in 0..w {
e.push((c, base + x));
}
}
}
let id: Vec<u32> = (0..n).map(idx32).collect();
let nat = fill(&graph(n, &e, &id), &Perm::identity(n));
let label = random_label(&mut rng, n);
let s = graph(n, &e, &label);
for o in [&Amd::default() as &dyn Ordering, &Colamd::default()] {
let f = fill(&s, &o.order(&s, OrderingTarget::Symmetric).unwrap());
prop_assert!(f <= nat, "{}: {} > natural {}", o.name(), f, nat);
}
}
}
}