xingo_sparse/perm.rs
1//! # Chapter 5.9 — Permutations and sparsity patterns
2//!
3//! *New with task L1.3 (PLAN.md trilha L).*
4//!
5//! ## Purpose
6//! A fill-reducing ordering (Chapter 5.10) looks only at *where* the
7//! nonzeros of a matrix are, never at their values, and answers with a
8//! permutation. This chapter fixes both objects: [`Pattern`], the
9//! structure of a compressed matrix without its values, and [`Perm`], a
10//! permutation stored together with its inverse. It also measures what an
11//! ordering is for — the number of nonzeros of the Cholesky factor of the
12//! permuted pattern (Algorithm E) — which is the yardstick of every test
13//! and of the acceptance tables of the orderings.
14//!
15//! ## Notation
16//! A [`Pattern`] `S` with `n_rows = m` and `n = n_cols` columns stores, as
17//! a [`CscMatrix`] does, the row indices of column `j` in
18//! `row_idx[col_ptr[j]..col_ptr[j + 1]]`. A [`Perm`] `π` of `0..n` is the
19//! pair of arrays
20//!
21//! * `fwd[k] = i` — the *old* index `i` that comes `k`-th in the new order
22//! (the `k`-th pivot of an elimination, the `k`-th column of `A Q`);
23//! * `inv[i] = k` — the new position of the old index `i`.
24//!
25//! As matrices: `P` with `(P x)_k = x_{fwd[k]}`, so `P A Pᵀ` has entry
26//! `(k, l) = a_{fwd[k], fwd[l]}`.
27//!
28//! For a square pattern `S` let `G(S)` be the undirected graph on `0..n`
29//! with an edge `{i, j}` (`i ≠ j`) whenever `s_ij` or `s_ji` is stored: the
30//! graph of `S + Sᵀ` without its diagonal. `L` is the Cholesky factor of a
31//! symmetric positive definite matrix with the pattern of `P (S + Sᵀ) Pᵀ`
32//! (and a full diagonal), `nnz(L)` its number of entries *including* the
33//! diagonal — the *fill* of the ordering in the tables (no numerical
34//! cancellation is assumed: the structural count).
35//!
36//! ## Algorithm S (Symmetrization `S + Sᵀ`)
37//! S1. Compute `Sᵀ` by Algorithm T of Chapter 5.8 (indices only).
38//! S2. For each column `j`, merge the sorted lists of column `j` of `S` and
39//! of `Sᵀ`, keeping each index once. The result has sorted columns.
40//!
41//! ## Algorithm E (Elimination tree and Cholesky counts, by row subtrees)
42//! Given the symmetric graph `G` and the permutation `π`; let `C = P G Pᵀ`.
43//! Row `k` of `L` is the set of `l ≤ k` reached from the entries `c_kl`,
44//! `l < k`, by walking up the elimination tree of the leading `k × k`
45//! block (the *row subtree*, Liu 1986).
46//!
47//! E1. Set `parent[k] ← ∅`, `mark[k] ← ∅` for all `k`, `count ← n` (the
48//! diagonal).
49//! E2. For `k = 0, 1, …, n − 1`: set `mark[k] ← k`; for every neighbour `i`
50//! of `fwd[k]` with `l = inv[i] < k`, walk `x ← l, parent[x], …` while
51//! `mark[x] ≠ k`: set `mark[x] ← k`, `count ← count + 1` (the entry
52//! `l_kx`), and if `parent[x] = ∅` set `parent[x] ← k` and stop.
53//! E3. Return `count`.
54//!
55//! The walk of E2 from `l` stops at the first node already in row `k`'s
56//! subtree, so every node of the row subtree is visited once: the cost is
57//! `O(nnz(L) + nnz(S))`, no `log` and no sort. Setting `parent` the first
58//! time a root is reached is Liu's elimination-tree algorithm without path
59//! compression.
60//!
61//! ## Algorithm P (Postorder of the elimination tree, heaviest child last)
62//! *New with task L1.5b.* Given `G` and `π`, a permutation `π′` with the
63//! same filled graph whose subtrees are contiguous and whose heaviest
64//! child comes right before its parent.
65//!
66//! P1. Run Algorithm E, keeping `parent[k]` and the column counts `c_k`
67//! (entries of column `k` of `L`, diagonal included).
68//! P2. Order the children of every node by increasing `(c, k)`: the child
69//! with the largest column count — the largest front — last.
70//! P3. Visit the roots in increasing order, each subtree depth first with
71//! the children in the order of P2, and number every node after its
72//! children: `post[t]` is the `t`-th node. Return `fwd′[t] =
73//! fwd[post[t]]`.
74//!
75//! Every topological order of the elimination tree — a postorder in
76//! particular — eliminates with the same filled graph (Liu 1990, §3), so
77//! `nnz(L)` does not change; the postorder makes each subtree a run of
78//! consecutive columns, so every chain of only children (a fundamental
79//! supernode) is contiguous; and placing the heaviest child last puts the
80//! largest front next to its parent. That matters to a supernodal
81//! backend: faer 0.24 (read, not copied) merges a child supernode into its
82//! parent in its relaxed amalgamation *only* when the child immediately
83//! precedes the parent, and SuiteSparse's AMD postorders its assembly tree
84//! with the largest front last for the same reason. With the children in
85//! index order instead, faer stored 2.78 M entries of `L` for the
86//! augmented system of maros-r7 (the same 1.21 M structural ones) and
87//! factorized 11× slower (task L1.5b, D4). Cost `O(nnz(L) + n log n)`.
88//!
89//! ## Algorithm G (The pattern of `SᵀS`)
90//! For COLAMD (Chapter 5.10) the relevant symmetric graph is that of `SᵀS`,
91//! whose Cholesky factor bounds the `U` of an LU with partial pivoting and
92//! is the `R` of a QR factorization (George & Ng 1987). Column `j` of `SᵀS`
93//! is the union, over the rows `i` of column `j` of `S`, of the columns of
94//! row `i` (rows read from `Sᵀ`): mark-and-collect, then sort each column.
95//! Cost `O(Σ_i r_i²)` with `r_i` the row counts — which is why COLAMD itself
96//! never forms it.
97//!
98//! ## Invariants
99//! (P1) (= the contract's (S3)) A [`Perm`] of length `n` has `fwd` and `inv`
100//! of length `n`, entries `< n`, and `inv[fwd[k]] = k` for every `k`
101//! (so both are bijections). [`Perm::check_invariants`] checks it and
102//! every constructor asserts it in debug builds.
103//! (P2) A [`Pattern`] satisfies (C1), (C2) and (C4) of Chapter 5.8 (there
104//! are no values, so (C3) is void): pointers non-decreasing from 0 to
105//! `nnz`, row indices strictly increasing inside a column and smaller
106//! than `n_rows`. Checked by [`Pattern::check_invariants`].
107//! (P3) The outputs of Algorithms S and G are symmetric patterns that
108//! satisfy (P2).
109//! (P4) Algorithm P returns a permutation with the `nnz(L)` of its input
110//! (checked in the tests against Algorithm E), in which every node
111//! comes after all of its elimination-tree descendants.
112//!
113//! ## References
114//! Liu, "A compact row storage scheme for Cholesky factors using
115//! elimination trees", ACM TOMS 12(2), 1986, and "The role of elimination
116//! trees in sparse factorization", SIAM J. Matrix Anal. Appl. 11(1), 1990;
117//! George & Ng, "Symbolic factorization for sparse Gaussian elimination
118//! with partial pivoting", SIAM J. Sci. Stat. Comput. 8(6), 1987; Davis,
119//! *Direct Methods for Sparse Linear Systems* (2006), §4.1 and §4.4 (read,
120//! not copied); the contract of the crate (`Pattern`, `Perm`, (S3)).
121
122use crate::index::dim_to_u32;
123use crate::real::Real;
124use crate::{CscMatrix, SparseError};
125
126/// A permutation of `0..n` with its inverse (invariant (P1), the
127/// contract's (S3)).
128///
129/// `fwd[k]` is the old index placed at new position `k`; `inv[i]` is the
130/// new position of the old index `i`. The fields are public, as in the
131/// contract; a value built by hand should be checked with
132/// [`Perm::check_invariants`].
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct Perm {
135 /// `fwd[k]`: the old index at new position `k`.
136 pub fwd: Vec<u32>,
137 /// `inv[i]`: the new position of the old index `i`.
138 pub inv: Vec<u32>,
139}
140
141impl Perm {
142 /// The identity permutation of `0..n`.
143 ///
144 /// # Panics
145 /// If `n` exceeds `u32::MAX`.
146 pub fn identity(n: usize) -> Self {
147 assert!(dim_to_u32("permutation", n).is_ok(), "n exceeds u32::MAX");
148 let fwd: Vec<u32> = (0..n).map(idx32).collect();
149 Self {
150 inv: fwd.clone(),
151 fwd,
152 }
153 }
154
155 /// The permutation whose forward array is `fwd`, with its inverse;
156 /// an error unless `fwd` is a bijection of `0..fwd.len()`.
157 pub fn from_fwd(fwd: Vec<u32>) -> Result<Self, SparseError> {
158 let n = fwd.len();
159 dim_to_u32("permutation", n)?;
160 let mut inv = vec![u32::MAX; n];
161 for (k, &i) in fwd.iter().enumerate() {
162 let i = i as usize;
163 if i >= n {
164 return Err(SparseError::IndexOutOfRange {
165 what: "permutation entry",
166 index: i,
167 bound: n,
168 });
169 }
170 if inv[i] != u32::MAX {
171 return Err(SparseError::Malformed(format!(
172 "(P1): index {i} appears twice in the permutation"
173 )));
174 }
175 inv[i] = idx32(k);
176 }
177 let p = Self { fwd, inv };
178 debug_assert_eq!(p.check_invariants(), Ok(()));
179 Ok(p)
180 }
181
182 /// The length `n`.
183 #[inline]
184 pub fn len(&self) -> usize {
185 self.fwd.len()
186 }
187
188 /// `true` iff `n = 0`.
189 #[inline]
190 pub fn is_empty(&self) -> bool {
191 self.fwd.is_empty()
192 }
193
194 /// The inverse permutation (`fwd` and `inv` exchanged).
195 #[must_use]
196 pub fn inverse(&self) -> Self {
197 Self {
198 fwd: self.inv.clone(),
199 inv: self.fwd.clone(),
200 }
201 }
202
203 /// `true` iff this is the identity.
204 pub fn is_identity(&self) -> bool {
205 self.fwd.iter().enumerate().all(|(k, &i)| i as usize == k)
206 }
207
208 /// Checks invariant (P1).
209 pub fn check_invariants(&self) -> Result<(), SparseError> {
210 let n = self.fwd.len();
211 if self.inv.len() != n {
212 return Err(SparseError::Dimension {
213 what: "inverse permutation",
214 expected: n,
215 found: self.inv.len(),
216 });
217 }
218 for (k, &i) in self.fwd.iter().enumerate() {
219 let i = i as usize;
220 if i >= n {
221 return Err(SparseError::IndexOutOfRange {
222 what: "permutation entry",
223 index: i,
224 bound: n,
225 });
226 }
227 if self.inv[i] as usize != k {
228 return Err(SparseError::Malformed(format!(
229 "(P1): inv[fwd[{k}]] = {} ≠ {k}",
230 self.inv[i]
231 )));
232 }
233 }
234 Ok(())
235 }
236}
237
238/// The structure of a compressed sparse column matrix without its values
239/// (invariant (P2)): what the orderings of Chapter 5.10 consume.
240#[derive(Clone, Debug, PartialEq, Eq)]
241pub struct Pattern {
242 /// Number of rows `m`.
243 pub n_rows: usize,
244 /// Column pointers, length `n + 1`.
245 pub col_ptr: Vec<usize>,
246 /// Row indices, strictly increasing inside each column.
247 pub row_idx: Vec<u32>,
248}
249
250impl Pattern {
251 /// The pattern with the given arrays, validated ((P2)).
252 pub fn from_raw_parts(
253 n_rows: usize,
254 col_ptr: Vec<usize>,
255 row_idx: Vec<u32>,
256 ) -> Result<Self, SparseError> {
257 let p = Self {
258 n_rows,
259 col_ptr,
260 row_idx,
261 };
262 p.check_invariants()?;
263 Ok(p)
264 }
265
266 /// The empty `m × n` pattern.
267 ///
268 /// # Panics
269 /// If `m` or `n` exceeds `u32::MAX`.
270 pub fn empty(n_rows: usize, n_cols: usize) -> Self {
271 assert!(dim_to_u32("rows", n_rows).is_ok() && dim_to_u32("columns", n_cols).is_ok());
272 Self {
273 n_rows,
274 col_ptr: vec![0; n_cols + 1],
275 row_idx: Vec::new(),
276 }
277 }
278
279 /// Number of rows `m`.
280 #[inline]
281 pub fn n_rows(&self) -> usize {
282 self.n_rows
283 }
284
285 /// Number of columns `n`.
286 #[inline]
287 pub fn n_cols(&self) -> usize {
288 self.col_ptr.len().saturating_sub(1)
289 }
290
291 /// Number of stored entries.
292 #[inline]
293 pub fn nnz(&self) -> usize {
294 self.row_idx.len()
295 }
296
297 /// The row indices of column `j`.
298 #[inline]
299 pub fn col(&self, j: usize) -> &[u32] {
300 &self.row_idx[self.col_ptr[j]..self.col_ptr[j + 1]]
301 }
302
303 /// `Sᵀ` (Algorithm T of Chapter 5.8, indices only).
304 #[must_use]
305 pub fn transpose(&self) -> Self {
306 // T1–T2. Counts and prefix sums.
307 let mut ptr = vec![0_usize; self.n_rows + 1];
308 for &i in &self.row_idx {
309 ptr[i as usize + 1] += 1;
310 }
311 for i in 0..self.n_rows {
312 ptr[i + 1] += ptr[i];
313 }
314 // T3. Scatter by increasing column (sorted output).
315 let mut next = ptr[..self.n_rows].to_vec();
316 let mut idx = vec![0_u32; self.nnz()];
317 for j in 0..self.n_cols() {
318 for &i in self.col(j) {
319 idx[next[i as usize]] = idx32(j);
320 next[i as usize] += 1;
321 }
322 }
323 let t = Self {
324 n_rows: self.n_cols(),
325 col_ptr: ptr,
326 row_idx: idx,
327 };
328 debug_assert_eq!(t.check_invariants(), Ok(()));
329 t
330 }
331
332 /// `S + Sᵀ` (Algorithm S); the diagonal entries of `S` are kept, none
333 /// is added. An error unless `S` is square.
334 pub fn symmetric(&self) -> Result<Self, SparseError> {
335 let n = self.n_cols();
336 if self.n_rows != n {
337 return Err(SparseError::Dimension {
338 what: "pattern (must be square to symmetrize)",
339 expected: n,
340 found: self.n_rows,
341 });
342 }
343 let t = self.transpose();
344 let mut col_ptr = Vec::with_capacity(n + 1);
345 let mut row_idx = Vec::with_capacity(2 * self.nnz());
346 col_ptr.push(0);
347 for j in 0..n {
348 // S2. Merge two sorted lists.
349 let (a, b) = (self.col(j), t.col(j));
350 let (mut p, mut q) = (0, 0);
351 while p < a.len() || q < b.len() {
352 let x = a.get(p).copied().unwrap_or(u32::MAX);
353 let y = b.get(q).copied().unwrap_or(u32::MAX);
354 let next = x.min(y);
355 p += usize::from(x == next);
356 q += usize::from(y == next);
357 row_idx.push(next);
358 }
359 col_ptr.push(row_idx.len());
360 }
361 let s = Self {
362 n_rows: n,
363 col_ptr,
364 row_idx,
365 };
366 debug_assert_eq!(s.check_invariants(), Ok(()));
367 Ok(s)
368 }
369
370 /// The pattern of `SᵀS` (Algorithm G), diagonal included: an `n × n`
371 /// symmetric pattern.
372 #[must_use]
373 pub fn ata(&self) -> Self {
374 let n = self.n_cols();
375 let t = self.transpose();
376 let mut mark = vec![usize::MAX; n];
377 let mut col_ptr = Vec::with_capacity(n + 1);
378 let mut row_idx = Vec::new();
379 col_ptr.push(0);
380 for j in 0..n {
381 let start = row_idx.len();
382 for &i in self.col(j) {
383 for &k in t.col(i as usize) {
384 if mark[k as usize] != j {
385 mark[k as usize] = j;
386 row_idx.push(k);
387 }
388 }
389 }
390 row_idx[start..].sort_unstable();
391 col_ptr.push(row_idx.len());
392 }
393 let s = Self {
394 n_rows: n,
395 col_ptr,
396 row_idx,
397 };
398 debug_assert_eq!(s.check_invariants(), Ok(()));
399 s
400 }
401
402 /// `nnz(L)`, diagonal included, of the Cholesky factor of
403 /// `P (S + Sᵀ) Pᵀ` for the permutation `perm` (Algorithm E): the fill
404 /// an ordering yields. An error unless `S` is square with `perm` of the
405 /// same order.
406 pub fn cholesky_nnz(&self, perm: &Perm) -> Result<usize, SparseError> {
407 let n = self.n_cols();
408 if self.n_rows != n || perm.len() != n {
409 return Err(SparseError::Dimension {
410 what: "cholesky_nnz (square pattern and permutation of its order)",
411 expected: n,
412 found: if self.n_rows == n {
413 perm.len()
414 } else {
415 self.n_rows
416 },
417 });
418 }
419 perm.check_invariants()?;
420 let (_, counts) = etree_counts(&self.symmetric()?, perm);
421 // E3.
422 Ok(counts.iter().sum())
423 }
424
425 /// Algorithm P on the symmetric pattern `self` (both triangles, as
426 /// [`Pattern::symmetric`] returns): `perm` re-postordered by its
427 /// elimination tree, heaviest child last (invariant (P4)).
428 #[cfg_attr(not(feature = "faer"), allow(dead_code))] // used by the KKT backends
429 pub(crate) fn etree_postorder(&self, perm: &Perm) -> Perm {
430 let n = perm.len();
431 debug_assert_eq!(self.n_cols(), n);
432 // P1.
433 let (parent, counts) = etree_counts(self, perm);
434 // P2. Children grouped by parent, by increasing (count, index).
435 let none = usize::MAX;
436 let mut kids: Vec<(usize, usize, usize)> = (0..n)
437 .filter(|&k| parent[k] != none)
438 .map(|k| (parent[k], counts[k], k))
439 .collect();
440 kids.sort_unstable();
441 let mut start = vec![0_usize; n + 1];
442 for &(p, _, _) in &kids {
443 start[p + 1] += 1;
444 }
445 for k in 0..n {
446 start[k + 1] += start[k];
447 }
448 // P3.
449 let mut fwd = Vec::with_capacity(n);
450 let mut stack: Vec<(usize, usize)> = Vec::new();
451 for root in (0..n).filter(|&k| parent[k] == none) {
452 stack.push((root, start[root]));
453 while let Some(top) = stack.last_mut() {
454 let (x, next) = *top;
455 if next < start[x + 1] {
456 top.1 += 1;
457 let c = kids[next].2;
458 stack.push((c, start[c]));
459 } else {
460 stack.pop();
461 fwd.push(perm.fwd[x]);
462 }
463 }
464 }
465 let post = Perm::from_fwd(fwd).expect("(P4): a postorder is a permutation");
466 debug_assert!(
467 (0..n).all(|k| parent[k] == none
468 || post.inv[perm.fwd[k] as usize] < post.inv[perm.fwd[parent[k]] as usize]),
469 "(P4): a node before its parent"
470 );
471 post
472 }
473
474 /// Checks invariant (P2).
475 pub fn check_invariants(&self) -> Result<(), SparseError> {
476 dim_to_u32("rows", self.n_rows)?;
477 dim_to_u32("columns", self.n_cols())?;
478 if self.col_ptr.is_empty() {
479 return Err(SparseError::Malformed("(P2): empty pointer array".into()));
480 }
481 if self.col_ptr[0] != 0 || self.col_ptr[self.n_cols()] != self.row_idx.len() {
482 return Err(SparseError::Malformed(format!(
483 "(P2): pointers must run from 0 to nnz = {}",
484 self.row_idx.len()
485 )));
486 }
487 for j in 0..self.n_cols() {
488 if self.col_ptr[j] > self.col_ptr[j + 1] {
489 return Err(SparseError::Malformed(format!(
490 "(P2): pointer of column {j} decreases"
491 )));
492 }
493 let col = self.col(j);
494 for (p, &i) in col.iter().enumerate() {
495 if i as usize >= self.n_rows {
496 return Err(SparseError::IndexOutOfRange {
497 what: "pattern row index",
498 index: i as usize,
499 bound: self.n_rows,
500 });
501 }
502 if p > 0 && col[p - 1] >= i {
503 return Err(SparseError::Malformed(format!(
504 "(P2): rows of column {j} not strictly increasing"
505 )));
506 }
507 }
508 }
509 Ok(())
510 }
511}
512
513impl<T: Real> CscMatrix<T> {
514 /// The structure without values, consumed by the orderings (Chapter
515 /// 5.9).
516 pub fn pattern(&self) -> Pattern {
517 Pattern {
518 n_rows: self.n_rows,
519 col_ptr: self.col_ptr.clone(),
520 row_idx: self.row_idx.clone(),
521 }
522 }
523
524 /// The pattern of `A + Aᵀ` (Algorithm S) for the symmetric orderings;
525 /// an error unless `A` is square.
526 pub fn symmetric_pattern(&self) -> Result<Pattern, SparseError> {
527 self.pattern().symmetric()
528 }
529}
530
531/// Converts an index below `u32::MAX` to `u32`.
532#[inline]
533/// Algorithm E (steps E1–E2) on the symmetric graph `g` under `perm`: the
534/// elimination tree (`parent[k]`, `usize::MAX` for a root; positions in
535/// the new order) and the column counts of `L` (diagonal included).
536fn etree_counts(g: &Pattern, perm: &Perm) -> (Vec<usize>, Vec<usize>) {
537 let n = perm.len();
538 // E1.
539 let none = usize::MAX;
540 let mut parent = vec![none; n];
541 let mut mark = vec![none; n];
542 let mut counts = vec![1_usize; n];
543 // E2.
544 for k in 0..n {
545 mark[k] = k;
546 for &i in g.col(perm.fwd[k] as usize) {
547 let mut x = perm.inv[i as usize] as usize;
548 if x >= k {
549 continue;
550 }
551 while mark[x] != k {
552 mark[x] = k;
553 counts[x] += 1;
554 if parent[x] == none {
555 parent[x] = k;
556 break;
557 }
558 x = parent[x];
559 }
560 }
561 }
562 (parent, counts)
563}
564
565pub(crate) fn idx32(i: usize) -> u32 {
566 u32::try_from(i).expect("index exceeds u32::MAX")
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572 use proptest::prelude::*;
573
574 /// A random `m × n` pattern from a list of `(i, j)` (duplicates
575 /// collapse).
576 fn pattern_of(m: usize, n: usize, entries: &[(usize, usize)]) -> Pattern {
577 let mut cols: Vec<Vec<u32>> = vec![Vec::new(); n];
578 for &(i, j) in entries {
579 cols[j % n].push(idx32(i % m));
580 }
581 let mut col_ptr = vec![0];
582 let mut row_idx = Vec::new();
583 for mut c in cols {
584 c.sort_unstable();
585 c.dedup();
586 row_idx.extend(c);
587 col_ptr.push(row_idx.len());
588 }
589 Pattern::from_raw_parts(m, col_ptr, row_idx).unwrap()
590 }
591
592 /// Brute force: `nnz(L)` by eliminating the dense graph of `P G Pᵀ`.
593 fn cholesky_nnz_dense(s: &Pattern, perm: &Perm) -> usize {
594 let n = s.n_cols();
595 let mut adj = vec![vec![false; n]; n];
596 for j in 0..n {
597 for &i in s.col(j) {
598 let (a, b) = (perm.inv[i as usize] as usize, perm.inv[j] as usize);
599 if a != b {
600 adj[a][b] = true;
601 adj[b][a] = true;
602 }
603 }
604 }
605 let mut count = n;
606 for k in 0..n {
607 let later: Vec<usize> = (k + 1..n).filter(|&l| adj[k][l]).collect();
608 count += later.len();
609 for &a in &later {
610 for &b in &later {
611 if a != b {
612 adj[a][b] = true;
613 }
614 }
615 }
616 }
617 count
618 }
619
620 #[test]
621 fn perm_constructors_and_errors() {
622 let p = Perm::identity(4);
623 assert!(p.is_identity() && p.check_invariants().is_ok() && p.len() == 4);
624 let q = Perm::from_fwd(vec![2, 0, 3, 1]).unwrap();
625 assert_eq!(q.inv, vec![1, 3, 0, 2]);
626 assert_eq!(q.inverse().inverse(), q);
627 assert!(!q.is_identity() && !q.is_empty());
628 assert!(Perm::from_fwd(vec![0, 0]).is_err());
629 assert!(Perm::from_fwd(vec![0, 2]).is_err());
630 let bad = Perm {
631 fwd: vec![0, 1],
632 inv: vec![1, 0],
633 };
634 assert!(bad.check_invariants().is_err());
635 assert!(Perm::identity(0).is_empty());
636 }
637
638 #[test]
639 fn pattern_validation_and_access() {
640 assert!(Pattern::from_raw_parts(2, vec![0, 2], vec![1, 0]).is_err());
641 assert!(Pattern::from_raw_parts(2, vec![0, 1], vec![2]).is_err());
642 assert!(Pattern::from_raw_parts(2, vec![0, 2, 1], vec![0, 1]).is_err());
643 let p = Pattern::from_raw_parts(3, vec![0, 2, 2, 3], vec![0, 2, 1]).unwrap();
644 assert_eq!((p.n_rows(), p.n_cols(), p.nnz()), (3, 3, 3));
645 assert_eq!(p.col(0), &[0, 2]);
646 assert_eq!(p.col(1), []);
647 assert!(Pattern::empty(2, 3).check_invariants().is_ok());
648 let a = CscMatrix::<f64>::from_raw_parts(
649 3,
650 vec![0, 2, 2, 3],
651 vec![0, 2, 1],
652 vec![1.0, 2.0, 3.0],
653 )
654 .unwrap();
655 assert_eq!(a.pattern(), p);
656 let s = a.symmetric_pattern().unwrap();
657 assert_eq!(s.col(0), &[0, 2]);
658 assert_eq!(s.col(1), &[2]);
659 assert_eq!(s.col(2), &[0, 1]);
660 assert!(Pattern::empty(2, 3).symmetric().is_err());
661 assert!(p.cholesky_nnz(&Perm::identity(2)).is_err());
662 }
663
664 proptest! {
665 /// (P2)/(P3): transposition is an involution, `S + Sᵀ` is symmetric
666 /// and contains `S`, `SᵀS` is symmetric with its diagonal.
667 #[test]
668 fn pattern_algebra(m in 1_usize..12, n in 1_usize..12,
669 e in prop::collection::vec((0_usize..12, 0_usize..12), 0..60)) {
670 let s = pattern_of(m, n, &e);
671 prop_assert_eq!(s.transpose().transpose(), s.clone());
672 let g = s.ata();
673 prop_assert_eq!(g.transpose(), g.clone());
674 for j in 0..n {
675 if !s.col(j).is_empty() {
676 prop_assert!(g.col(j).contains(&idx32(j)));
677 }
678 }
679 let q = pattern_of(n, n, &e);
680 let sym = q.symmetric().unwrap();
681 prop_assert_eq!(sym.transpose(), sym.clone());
682 for j in 0..n {
683 for &i in q.col(j) {
684 prop_assert!(sym.col(j).contains(&i));
685 }
686 }
687 }
688
689 /// Algorithm E against the dense elimination, under random
690 /// permutations.
691 #[test]
692 fn cholesky_count_matches_dense_elimination(
693 n in 1_usize..25,
694 e in prop::collection::vec((0_usize..25, 0_usize..25), 0..80),
695 keys in prop::collection::vec(any::<u32>(), 25),
696 ) {
697 let s = pattern_of(n, n, &e);
698 let mut fwd: Vec<u32> = (0..n).map(idx32).collect();
699 fwd.sort_by_key(|&i| keys[i as usize]);
700 let perm = Perm::from_fwd(fwd).unwrap();
701 prop_assert_eq!(s.cholesky_nnz(&perm).unwrap(), cholesky_nnz_dense(&s, &perm));
702 }
703
704 /// (P4): Algorithm P keeps `nnz(L)` and returns a postorder of the
705 /// elimination tree with the heaviest child of every node right
706 /// before it.
707 #[test]
708 fn etree_postorder_keeps_the_fill(
709 n in 1_usize..25,
710 e in prop::collection::vec((0_usize..25, 0_usize..25), 0..80),
711 keys in prop::collection::vec(any::<u32>(), 25),
712 ) {
713 let s = pattern_of(n, n, &e).symmetric().unwrap();
714 let mut fwd: Vec<u32> = (0..n).map(idx32).collect();
715 fwd.sort_by_key(|&i| keys[i as usize]);
716 let perm = Perm::from_fwd(fwd).unwrap();
717 let post = s.etree_postorder(&perm);
718 prop_assert_eq!(post.check_invariants(), Ok(()));
719 prop_assert_eq!(s.cholesky_nnz(&post).unwrap(), s.cholesky_nnz(&perm).unwrap());
720 let (parent, counts) = etree_counts(&s, &post);
721 // Postorder: a node follows its children, and every subtree is
722 // a contiguous run ending at its root.
723 let mut size = vec![1_usize; n];
724 for k in 0..n {
725 if parent[k] != usize::MAX {
726 prop_assert!(parent[k] > k);
727 size[parent[k]] += size[k];
728 }
729 }
730 for (k, &p) in parent.iter().enumerate() {
731 if p != usize::MAX {
732 // Contiguity: the subtree of p is p − size(p) + 1 ..= p.
733 prop_assert!(k + size[p] > p);
734 }
735 }
736 // Heaviest child last: the child right before a parent with
737 // children has the largest count among its siblings.
738 for p in 0..n {
739 let kids: Vec<usize> = (0..n).filter(|&k| parent[k] == p).collect();
740 if let Some(&last) = kids.iter().max() {
741 prop_assert_eq!(last + 1, p);
742 let heaviest = kids.iter().map(|&k| counts[k]).max().unwrap();
743 prop_assert_eq!(counts[last], heaviest);
744 }
745 }
746 }
747 }
748}