Skip to main content

xingo_sparse/
dense.rs

1//! # Chapter 5.16 — Dense kernels for supernodal blocks
2//!
3//! *New with task L1.5 (PLAN.md trilha L; the contract's §5.8, decision
4//! Q4).*
5//!
6//! ## Purpose
7//! A supernodal factorization (L1.6, `SupernodalLdl`) spends its time in a
8//! handful of dense operations on the blocks of `L`: factorize a diagonal
9//! block, solve the block below it against that factor, and subtract the
10//! products into the blocks of the ancestors. [`DenseKernel`] names them
11//! with their BLAS/LAPACK names (`potrf`, `sytrf`, `trsm`, `gemm`, `syrk`)
12//! so a backend can be swapped without touching the sparse code:
13//! [`NaiveDense`] — straightforward loops, generic in `T`, always available,
14//! the oracle of the others — and `FaerDense` (feature `faer`), faer's
15//! optimized `f64` kernels. A scalar type other than `f64` (the
16//! double-double of `xingo-extprec`) uses [`NaiveDense`].
17//!
18//! ## Notation
19//! A [`DenseBlock`] `A` is `rows × cols`, column-major: `a_ij =
20//! data[i + j·rows]`. Square blocks are read and written **in their lower
21//! triangle only** (the strict upper triangle is left untouched), like
22//! LAPACK with `uplo = 'L'`. For [`DenseKernel::sytrf`], `P A Pᵀ = L B Lᵀ`
23//! with `P` a permutation ([`Perm`], `fwd[k]` = the original index placed
24//! `k`-th), `L` unit lower triangular and `B` block diagonal with `1×1`
25//! and `2×2` blocks; `α = (1 + √17)/8 ≈ 0.6404` is Bunch & Kaufman's
26//! constant, which minimizes the bound on element growth.
27//!
28//! ## Algorithm P (`potrf`, Cholesky by columns)
29//! For `j = 0, …, n − 1`: `d ← a_jj − Σ_{k<j} l_jk²`; if `d ≤ 0` (or not
30//! finite) fail with [`DenseError::NotPositiveDefinite`] at `j`;
31//! `l_jj ← √d`; for `i > j`, `l_ij ← (a_ij − Σ_{k<j} l_ik l_jk) / l_jj`.
32//!
33//! ## Algorithm B (`sytrf`, Bunch–Kaufman partial pivoting)
34//! On a working copy `W` of the symmetric matrix, `k ← 0`; while `k < n`:
35//! B1. `λ ← max_{i>k} |w_ik|`, `r` its first row index. If `λ = 0`, the
36//!     column is already eliminated: a `1×1` pivot `w_kk` (possibly zero).
37//! B2. Else if `|w_kk| ≥ α λ`: `1×1` pivot at `k`.
38//! B3. Else `σ ← max_{i≥k, i≠r} |w_ir|`: if `|w_kk| σ ≥ α λ²`, `1×1` at
39//!     `k`; else if `|w_rr| ≥ α σ`, swap `r` into position `k` and take a
40//!     `1×1` pivot; else swap `r` into position `k + 1` and take the `2×2`
41//!     pivot `E = W[k..k+2, k..k+2]`.
42//! B4. A swap exchanges rows and columns of `W`, the rows of the computed
43//!     columns of `L`, and the entries of `P`.
44//! B5. Eliminate: `1×1` — `l_ik ← w_ik / w_kk` and `W ← W − l d lᵀ` on the
45//!     trailing block; `2×2` — `[l_ik, l_i,k+1] ← [w_ik, w_i,k+1] E⁻¹` and
46//!     `W ← W − [w_k w_k+1] E⁻¹ [w_k w_k+1]ᵀ`; advance `k` by 1 or 2.
47//! Output in `A`: `B`'s diagonal on the diagonal, the off-diagonal of each
48//! `2×2` block at `(k + 1, k)` (where `L` has an implicit zero), `L` below;
49//! `pivot_2x2[k] = pivot_2x2[k + 1] = true` for a `2×2` block. The inertia
50//! is that of `B` (Sylvester; Chapter 5.14, step K4).
51//!
52//! ## Algorithms T, G, S (`trsm_lower`, `gemm_sub`, `syrk_sub`)
53//! `trsm_lower(L, B, false)`: `B ← L⁻¹ B` by forward substitution, column
54//! by column of `B`; `trsm_lower(L, B, true)`: `B ← B L⁻ᵀ`, i.e. the rows
55//! of `B` solved against `L` (the "solve the block below the diagonal"
56//! step of a left-looking Cholesky). `L` is non-unit lower triangular (the
57//! output of `potrf`). `gemm_sub(A, B, C)`: `C ← C − A Bᵀ`. `syrk_sub(A,
58//! C)`: `C ← C − A Aᵀ` on the lower triangle of `C`. Sums run over `k` in
59//! increasing order.
60//!
61//! ## Invariants
62//! (D1) After `potrf`, `L Lᵀ = A` (lower triangle) to working precision.
63//! (D2) After `sytrf`, `P A Pᵀ = L B Lᵀ` to working precision, `B` has
64//!      only `1×1` and `2×2` blocks, every `2×2` block has a negative
65//!      determinant or is indefinite-safe (`|e_21| ≥ λ > 0`), and the
66//!      returned inertia sums to `n`. Checked by the tests against the
67//!      reconstruction and against `NaiveDense` vs `FaerDense`.
68//! (D3) Every kernel leaves the strict upper triangle of a square block
69//!      untouched.
70//!
71//! ## References
72//! Bunch & Kaufman, "Some stable methods for calculating inertia and
73//! solving symmetric linear systems", Math. Comp. 31, 1977; Golub & Van
74//! Loan, *Matrix Computations*, 4th ed. 2013, §4.2 and §4.4; Higham,
75//! *Accuracy and Stability of Numerical Algorithms*, 2nd ed. 2002, ch.
76//! 10–11; LAPACK `dpotrf`/`dsytrf`/`dtrsm`/`dgemm`/`dsyrk` (interfaces,
77//! read); faer 0.24.4 `linalg::{cholesky, triangular_solve, matmul}` (the
78//! API, read, not copied).
79
80use crate::kkt::count_inertia;
81use crate::real::{Real, is_exact_zero, le, lt};
82use crate::{DenseError, Inertia, Perm};
83
84/// A column-major dense block: `a_ij = data[i + j·rows]`.
85#[derive(Clone, Debug, PartialEq)]
86pub struct DenseBlock<T: Real = f64> {
87    /// Number of rows.
88    pub rows: usize,
89    /// Number of columns.
90    pub cols: usize,
91    /// The entries, column by column (`rows · cols` of them).
92    pub data: Vec<T>,
93}
94
95impl<T: Real> DenseBlock<T> {
96    /// The `rows × cols` zero block.
97    pub fn zeros(rows: usize, cols: usize) -> Self {
98        Self {
99            rows,
100            cols,
101            data: vec![T::ZERO; rows * cols],
102        }
103    }
104
105    /// Entry `(i, j)`.
106    #[inline]
107    pub fn get(&self, i: usize, j: usize) -> T {
108        self.data[i + j * self.rows]
109    }
110
111    /// Sets entry `(i, j)`.
112    #[inline]
113    pub fn set(&mut self, i: usize, j: usize, v: T) {
114        self.data[i + j * self.rows] = v;
115    }
116
117    fn is_consistent(&self) -> bool {
118        self.data.len() == self.rows * self.cols
119    }
120
121    fn square(&self) -> Result<usize, DenseError> {
122        if self.is_consistent() && self.rows == self.cols {
123            Ok(self.rows)
124        } else {
125            Err(DenseError::Dimension)
126        }
127    }
128}
129
130/// The kernels a supernodal factorization needs (BLAS-3 names kept for
131/// recognizability). Square blocks are read and written in their lower
132/// triangle (invariant (D3)).
133pub trait DenseKernel<T: Real> {
134    /// In-place Cholesky `A = L Lᵀ` of a square block (Algorithm P).
135    fn potrf(&self, a: &mut DenseBlock<T>) -> Result<(), DenseError>;
136
137    /// In-place `P A Pᵀ = L B Lᵀ` with `1×1`/`2×2` pivots (Bunch–Kaufman,
138    /// Algorithm B); returns the inertia and writes the permutation into
139    /// `perm` and the `2×2` marks into `pivot_2x2` (both resized to `n`).
140    fn sytrf(
141        &self,
142        a: &mut DenseBlock<T>,
143        perm: &mut Perm,
144        pivot_2x2: &mut Vec<bool>,
145    ) -> Result<Inertia, DenseError>;
146
147    /// `B ← B L⁻ᵀ` (`transpose = true`) or `B ← L⁻¹ B` (`false`), `L`
148    /// square, non-unit lower triangular.
149    ///
150    /// # Panics
151    /// If the dimensions do not agree.
152    fn trsm_lower(&self, l: &DenseBlock<T>, b: &mut DenseBlock<T>, transpose: bool);
153
154    /// `C ← C − A Bᵀ`.
155    ///
156    /// # Panics
157    /// If the dimensions do not agree.
158    fn gemm_sub(&self, a: &DenseBlock<T>, b: &DenseBlock<T>, c: &mut DenseBlock<T>);
159
160    /// `C ← C − A Aᵀ`, lower triangle of `C`.
161    ///
162    /// # Panics
163    /// If the dimensions do not agree.
164    fn syrk_sub(&self, a: &DenseBlock<T>, c: &mut DenseBlock<T>);
165
166    /// A short name for logs and tables.
167    fn name(&self) -> &'static str;
168}
169
170/// Reference kernels: straightforward loops, always available, the oracle
171/// of the others in tests.
172///
173/// ```
174/// use xingo_sparse::{DenseBlock, DenseKernel, NaiveDense};
175/// let mut a = DenseBlock { rows: 2, cols: 2, data: vec![4.0, 2.0, 0.0, 5.0] };
176/// NaiveDense.potrf(&mut a).unwrap();
177/// assert_eq!((a.get(0, 0), a.get(1, 0), a.get(1, 1)), (2.0, 1.0, 2.0));
178/// ```
179#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
180pub struct NaiveDense;
181
182/// Bunch & Kaufman's `α = (1 + √17)/8` (a constant of Algorithm B, not a
183/// tuning knob: it minimizes the growth bound).
184fn bk_alpha<T: Real>() -> T {
185    (T::ONE + T::from_f64(17.0).sqrt()) / T::from_f64(8.0)
186}
187
188impl<T: Real> DenseKernel<T> for NaiveDense {
189    fn potrf(&self, a: &mut DenseBlock<T>) -> Result<(), DenseError> {
190        let n = a.square()?;
191        for j in 0..n {
192            let d = a.get(j, j) - (0..j).map(|k| a.get(j, k) * a.get(j, k)).sum::<T>();
193            if !d.is_finite() || le(d, T::ZERO, T::ZERO) {
194                return Err(DenseError::NotPositiveDefinite { position: j });
195            }
196            let ljj = d.sqrt();
197            a.set(j, j, ljj);
198            for i in j + 1..n {
199                let s = a.get(i, j) - (0..j).map(|k| a.get(i, k) * a.get(j, k)).sum::<T>();
200                a.set(i, j, s / ljj);
201            }
202        }
203        Ok(())
204    }
205
206    #[allow(clippy::too_many_lines)] // Algorithm B, steps B1–B5 in one place
207    fn sytrf(
208        &self,
209        a: &mut DenseBlock<T>,
210        perm: &mut Perm,
211        pivot_2x2: &mut Vec<bool>,
212    ) -> Result<Inertia, DenseError> {
213        let n = a.square()?;
214        let alpha = bk_alpha::<T>();
215        // The working copy W (full, symmetric) and the factor L.
216        let mut w = vec![T::ZERO; n * n];
217        for j in 0..n {
218            for i in j..n {
219                w[i + j * n] = a.get(i, j);
220                w[j + i * n] = a.get(i, j);
221            }
222        }
223        let mut l = vec![T::ZERO; n * n];
224        let mut fwd: Vec<usize> = (0..n).collect();
225        let mut two = vec![false; n];
226        // `first[k]`: a 2×2 block starts at k.
227        let mut first = vec![false; n];
228        let mut diag = vec![T::ZERO; n];
229        let mut off = vec![T::ZERO; n];
230        let swap = |w: &mut [T], l: &mut [T], fwd: &mut [usize], k: usize, r: usize| {
231            if k == r {
232                return;
233            }
234            for c in 0..n {
235                w.swap(k + c * n, r + c * n);
236            }
237            for c in 0..n {
238                w.swap(c + k * n, c + r * n);
239            }
240            for c in 0..k {
241                l.swap(k + c * n, r + c * n);
242            }
243            fwd.swap(k, r);
244        };
245        let mut k = 0;
246        while k < n {
247            // B1.
248            let mut lambda = T::ZERO;
249            let mut r = k;
250            for i in k + 1..n {
251                if lt(lambda, w[i + k * n].abs()) {
252                    lambda = w[i + k * n].abs();
253                    r = i;
254                }
255            }
256            let akk = w[k + k * n].abs();
257            let size = if is_exact_zero(lambda) || !lt(akk, alpha * lambda) {
258                1 // B1, B2
259            } else {
260                // B3.
261                let mut sigma = T::ZERO;
262                for i in k..n {
263                    if i != r {
264                        sigma = sigma.max(w[i + r * n].abs());
265                    }
266                }
267                if !lt(akk * sigma, alpha * lambda * lambda) {
268                    1
269                } else if !lt(w[r + r * n].abs(), alpha * sigma) {
270                    swap(&mut w, &mut l, &mut fwd, k, r);
271                    1
272                } else {
273                    swap(&mut w, &mut l, &mut fwd, k + 1, r);
274                    2
275                }
276            };
277            // B5.
278            if size == 1 {
279                let d = w[k + k * n];
280                diag[k] = d;
281                l[k + k * n] = T::ONE;
282                if !is_exact_zero(d) {
283                    for i in k + 1..n {
284                        l[i + k * n] = w[i + k * n] / d;
285                    }
286                    for j in k + 1..n {
287                        let ljd = l[j + k * n] * d;
288                        for i in j..n {
289                            let v = w[i + j * n] - l[i + k * n] * ljd;
290                            w[i + j * n] = v;
291                            w[j + i * n] = v;
292                        }
293                    }
294                }
295                k += 1;
296            } else {
297                let (e11, e21, e22) = (w[k + k * n], w[k + 1 + k * n], w[k + 1 + (k + 1) * n]);
298                let det = e11 * e22 - e21 * e21;
299                diag[k] = e11;
300                diag[k + 1] = e22;
301                off[k] = e21;
302                two[k] = true;
303                two[k + 1] = true;
304                first[k] = true;
305                l[k + k * n] = T::ONE;
306                l[k + 1 + (k + 1) * n] = T::ONE;
307                for i in k + 2..n {
308                    let (x, y) = (w[i + k * n], w[i + (k + 1) * n]);
309                    // [x y] E⁻¹, E⁻¹ = [e22 −e21; −e21 e11] / det.
310                    l[i + k * n] = (x * e22 - y * e21) / det;
311                    l[i + (k + 1) * n] = (y * e11 - x * e21) / det;
312                }
313                for j in k + 2..n {
314                    let (xj, yj) = (w[j + k * n], w[j + (k + 1) * n]);
315                    for i in j..n {
316                        let v = w[i + j * n] - (l[i + k * n] * xj + l[i + (k + 1) * n] * yj);
317                        w[i + j * n] = v;
318                        w[j + i * n] = v;
319                    }
320                }
321                k += 2;
322            }
323        }
324        // Output.
325        for j in 0..n {
326            a.set(j, j, diag[j]);
327            for i in j + 1..n {
328                let v = if first[j] && i == j + 1 {
329                    off[j]
330                } else {
331                    l[i + j * n]
332                };
333                a.set(i, j, v);
334            }
335        }
336        *perm = Perm::from_fwd(fwd.into_iter().map(crate::perm::idx32).collect())
337            .map_err(|_| DenseError::Dimension)?;
338        pivot_2x2.clear();
339        pivot_2x2.extend_from_slice(&two);
340        Ok(block_inertia(a, pivot_2x2))
341    }
342
343    fn trsm_lower(&self, l: &DenseBlock<T>, b: &mut DenseBlock<T>, transpose: bool) {
344        let n = l.square().expect("trsm_lower: L must be square");
345        assert!(b.is_consistent(), "trsm_lower: B is inconsistent");
346        if transpose {
347            // X Lᵀ = B: row i of X solves L xᵢ = bᵢ (forward substitution).
348            assert_eq!(b.cols, n, "trsm_lower: B must have n columns");
349            for i in 0..b.rows {
350                for j in 0..n {
351                    let s = b.get(i, j) - (0..j).map(|k| b.get(i, k) * l.get(j, k)).sum::<T>();
352                    b.set(i, j, s / l.get(j, j));
353                }
354            }
355        } else {
356            assert_eq!(b.rows, n, "trsm_lower: B must have n rows");
357            for c in 0..b.cols {
358                for i in 0..n {
359                    let s = b.get(i, c) - (0..i).map(|k| l.get(i, k) * b.get(k, c)).sum::<T>();
360                    b.set(i, c, s / l.get(i, i));
361                }
362            }
363        }
364    }
365
366    fn gemm_sub(&self, a: &DenseBlock<T>, b: &DenseBlock<T>, c: &mut DenseBlock<T>) {
367        assert!(
368            a.is_consistent()
369                && b.is_consistent()
370                && c.is_consistent()
371                && a.cols == b.cols
372                && c.rows == a.rows
373                && c.cols == b.rows,
374            "gemm_sub: dimensions do not agree"
375        );
376        for j in 0..c.cols {
377            for i in 0..c.rows {
378                let s = (0..a.cols).map(|k| a.get(i, k) * b.get(j, k)).sum::<T>();
379                c.set(i, j, c.get(i, j) - s);
380            }
381        }
382    }
383
384    fn syrk_sub(&self, a: &DenseBlock<T>, c: &mut DenseBlock<T>) {
385        let n = c.square().expect("syrk_sub: C must be square");
386        assert!(
387            a.is_consistent() && a.rows == n,
388            "syrk_sub: dimensions do not agree"
389        );
390        for j in 0..n {
391            for i in j..n {
392                let s = (0..a.cols).map(|k| a.get(i, k) * a.get(j, k)).sum::<T>();
393                c.set(i, j, c.get(i, j) - s);
394            }
395        }
396    }
397
398    fn name(&self) -> &'static str {
399        "naive"
400    }
401}
402
403/// The inertia of the block diagonal `B` stored by `sytrf`.
404fn block_inertia<T: Real>(a: &DenseBlock<T>, pivot_2x2: &[bool]) -> Inertia {
405    let n = a.rows;
406    let mut ones = Vec::new();
407    let mut twos = Vec::new();
408    let mut k = 0;
409    while k < n {
410        if pivot_2x2[k] && k + 1 < n {
411            twos.push((a.get(k, k), a.get(k + 1, k), a.get(k + 1, k + 1)));
412            k += 2;
413        } else {
414            ones.push(a.get(k, k));
415            k += 1;
416        }
417    }
418    count_inertia(ones.into_iter(), twos.into_iter())
419}
420
421/// faer's dense kernels (`f64` only in v0.1; extended types use
422/// [`NaiveDense`]). `sytrf` uses faer's Bunch–Kaufman with partial
423/// pivoting, whose pivot choices may differ from [`NaiveDense`]'s in ties:
424/// both satisfy (D2).
425#[cfg(feature = "faer")]
426#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
427pub struct FaerDense;
428
429#[cfg(feature = "faer")]
430mod faer_impl {
431    use faer::dyn_stack::{MemBuffer, MemStack};
432    use faer::linalg::cholesky::lblt::factor::{LbltParams, PivotingStrategy};
433    use faer::linalg::cholesky::llt::factor::LltRegularization;
434    use faer::linalg::matmul::triangular::BlockStructure;
435    use faer::{Accum, ColMut, MatMut, MatRef, Par, Spec};
436
437    use super::{DenseBlock, DenseError, DenseKernel, FaerDense, Inertia, Perm, block_inertia};
438    use crate::real::is_exact_zero;
439
440    fn view(a: &DenseBlock<f64>) -> MatRef<'_, f64> {
441        MatRef::from_column_major_slice(&a.data, a.rows, a.cols)
442    }
443
444    fn view_mut(a: &mut DenseBlock<f64>) -> MatMut<'_, f64> {
445        MatMut::from_column_major_slice_mut(&mut a.data, a.rows, a.cols)
446    }
447
448    impl DenseKernel<f64> for FaerDense {
449        fn potrf(&self, a: &mut DenseBlock<f64>) -> Result<(), DenseError> {
450            let n = a.square()?;
451            let params = Spec::default();
452            let mut buf = MemBuffer::new(
453                faer::linalg::cholesky::llt::factor::cholesky_in_place_scratch::<f64>(
454                    n,
455                    Par::Seq,
456                    params,
457                ),
458            );
459            faer::linalg::cholesky::llt::factor::cholesky_in_place(
460                view_mut(a),
461                LltRegularization::default(),
462                Par::Seq,
463                MemStack::new(&mut buf),
464                params,
465            )
466            .map(|_| ())
467            .map_err(|e| match e {
468                faer::linalg::cholesky::llt::factor::LltError::NonPositivePivot { index } => {
469                    DenseError::NotPositiveDefinite { position: index }
470                }
471            })
472        }
473
474        fn sytrf(
475            &self,
476            a: &mut DenseBlock<f64>,
477            perm: &mut Perm,
478            pivot_2x2: &mut Vec<bool>,
479        ) -> Result<Inertia, DenseError> {
480            let n = a.square()?;
481            let mut params: Spec<LbltParams, f64> = Spec::default();
482            params.config.pivoting = PivotingStrategy::Partial;
483            let mut subdiag = vec![0.0_f64; n];
484            let mut fwd = vec![0_usize; n];
485            let mut inv = vec![0_usize; n];
486            let mut buf = MemBuffer::new(
487                faer::linalg::cholesky::lblt::factor::cholesky_in_place_scratch::<usize, f64>(
488                    n,
489                    Par::Seq,
490                    params,
491                ),
492            );
493            faer::linalg::cholesky::lblt::factor::cholesky_in_place(
494                view_mut(a),
495                ColMut::from_slice_mut(&mut subdiag).as_diagonal_mut(),
496                &mut fwd,
497                &mut inv,
498                Par::Seq,
499                MemStack::new(&mut buf),
500                params,
501            );
502            pivot_2x2.clear();
503            pivot_2x2.resize(n, false);
504            let mut k = 0;
505            while k < n {
506                if k + 1 < n && !is_exact_zero(subdiag[k]) {
507                    a.set(k + 1, k, subdiag[k]);
508                    pivot_2x2[k] = true;
509                    pivot_2x2[k + 1] = true;
510                    k += 2;
511                } else {
512                    k += 1;
513                }
514            }
515            *perm = Perm::from_fwd(fwd.into_iter().map(crate::perm::idx32).collect())
516                .map_err(|_| DenseError::Dimension)?;
517            Ok(block_inertia(a, pivot_2x2))
518        }
519
520        fn trsm_lower(&self, l: &DenseBlock<f64>, b: &mut DenseBlock<f64>, transpose: bool) {
521            let n = l.square().expect("trsm_lower: L must be square");
522            if transpose {
523                assert_eq!(b.cols, n, "trsm_lower: B must have n columns");
524                // X Lᵀ = B  ⇔  L Xᵀ = Bᵀ.
525                faer::linalg::triangular_solve::solve_lower_triangular_in_place(
526                    view(l),
527                    view_mut(b).transpose_mut(),
528                    Par::Seq,
529                );
530            } else {
531                assert_eq!(b.rows, n, "trsm_lower: B must have n rows");
532                faer::linalg::triangular_solve::solve_lower_triangular_in_place(
533                    view(l),
534                    view_mut(b),
535                    Par::Seq,
536                );
537            }
538        }
539
540        fn gemm_sub(&self, a: &DenseBlock<f64>, b: &DenseBlock<f64>, c: &mut DenseBlock<f64>) {
541            assert!(
542                a.cols == b.cols && c.rows == a.rows && c.cols == b.rows,
543                "gemm_sub: dimensions do not agree"
544            );
545            faer::linalg::matmul::matmul(
546                view_mut(c),
547                Accum::Add,
548                view(a),
549                view(b).transpose(),
550                -1.0,
551                Par::Seq,
552            );
553        }
554
555        fn syrk_sub(&self, a: &DenseBlock<f64>, c: &mut DenseBlock<f64>) {
556            let n = c.square().expect("syrk_sub: C must be square");
557            assert_eq!(a.rows, n, "syrk_sub: dimensions do not agree");
558            faer::linalg::matmul::triangular::matmul(
559                view_mut(c),
560                BlockStructure::TriangularLower,
561                Accum::Add,
562                view(a),
563                BlockStructure::Rectangular,
564                view(a).transpose(),
565                BlockStructure::Rectangular,
566                -1.0,
567                Par::Seq,
568            );
569        }
570
571        fn name(&self) -> &'static str {
572            "faer"
573        }
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use crate::real::approx_eq;
581    use proptest::prelude::*;
582
583    /// A deterministic pseudo-random block (xorshift).
584    fn block(rows: usize, cols: usize, seed: u64) -> DenseBlock {
585        let mut s = seed | 1;
586        let data = (0..rows * cols)
587            .map(|_| {
588                s ^= s << 13;
589                s ^= s >> 7;
590                s ^= s << 17;
591                #[allow(clippy::cast_precision_loss)]
592                let u = (s >> 11) as f64 / (1_u64 << 53) as f64;
593                2.0 * u - 1.0
594            })
595            .collect();
596        DenseBlock { rows, cols, data }
597    }
598
599    /// A symmetric block: `M + Mᵀ` (indefinite in general), or
600    /// `M Mᵀ + n I` (positive definite).
601    fn symmetric(n: usize, seed: u64, spd: bool) -> DenseBlock {
602        let m = block(n, n, seed);
603        let mut a = DenseBlock::zeros(n, n);
604        for j in 0..n {
605            for i in 0..n {
606                #[allow(clippy::cast_precision_loss)]
607                let v = if spd {
608                    (0..n).map(|k| m.get(i, k) * m.get(j, k)).sum::<f64>()
609                        + if i == j { n as f64 } else { 0.0 }
610                } else {
611                    m.get(i, j) + m.get(j, i)
612                };
613                a.set(i, j, v);
614            }
615        }
616        a
617    }
618
619    fn close(x: &DenseBlock, y: &DenseBlock, lower_only: bool) -> bool {
620        (0..x.cols).all(|j| {
621            (if lower_only { j } else { 0 }..x.rows)
622                .all(|i| approx_eq(x.get(i, j), y.get(i, j), 1e-10))
623        })
624    }
625
626    /// `‖P A Pᵀ − L B Lᵀ‖_max` from the output of `sytrf`.
627    fn sytrf_error(a0: &DenseBlock, f: &DenseBlock, perm: &Perm, two: &[bool]) -> f64 {
628        let n = a0.rows;
629        let mut l = DenseBlock::zeros(n, n);
630        let mut b = DenseBlock::zeros(n, n);
631        let mut k = 0;
632        while k < n {
633            if two[k] {
634                b.set(k, k, f.get(k, k));
635                b.set(k + 1, k + 1, f.get(k + 1, k + 1));
636                b.set(k + 1, k, f.get(k + 1, k));
637                b.set(k, k + 1, f.get(k + 1, k));
638                l.set(k, k, 1.0);
639                l.set(k + 1, k + 1, 1.0);
640                for i in k + 2..n {
641                    l.set(i, k, f.get(i, k));
642                    l.set(i, k + 1, f.get(i, k + 1));
643                }
644                k += 2;
645            } else {
646                b.set(k, k, f.get(k, k));
647                l.set(k, k, 1.0);
648                for i in k + 1..n {
649                    l.set(i, k, f.get(i, k));
650                }
651                k += 1;
652            }
653        }
654        let mut err = 0.0_f64;
655        for i in 0..n {
656            for j in 0..=i {
657                let mut s = 0.0;
658                for p in 0..n {
659                    for q in 0..n {
660                        s += l.get(i, p) * b.get(p, q) * l.get(j, q);
661                    }
662                }
663                let (pi, pj) = (perm.fwd[i] as usize, perm.fwd[j] as usize);
664                let aij = if pi >= pj {
665                    a0.get(pi, pj)
666                } else {
667                    a0.get(pj, pi)
668                };
669                err = err.max((s - aij).abs());
670            }
671        }
672        err
673    }
674
675    #[test]
676    fn potrf_rejects_indefinite_blocks() {
677        let mut a = DenseBlock {
678            rows: 2,
679            cols: 2,
680            data: vec![1.0, 2.0, 0.0, 1.0],
681        };
682        assert_eq!(
683            NaiveDense.potrf(&mut a.clone()),
684            Err(DenseError::NotPositiveDefinite { position: 1 })
685        );
686        #[cfg(feature = "faer")]
687        assert_eq!(
688            FaerDense.potrf(&mut a),
689            Err(DenseError::NotPositiveDefinite { position: 1 })
690        );
691        let mut r = DenseBlock::<f64>::zeros(2, 3);
692        assert_eq!(NaiveDense.potrf(&mut r), Err(DenseError::Dimension));
693        let mut p = Perm::identity(0);
694        let mut t = Vec::new();
695        assert_eq!(
696            NaiveDense.sytrf(&mut r, &mut p, &mut t),
697            Err(DenseError::Dimension)
698        );
699        a.data.pop();
700        assert_eq!(NaiveDense.potrf(&mut a), Err(DenseError::Dimension));
701    }
702
703    #[test]
704    fn sytrf_finds_2x2_pivots_and_inertia() {
705        // [0 1; 1 0]: no 1×1 pivot is acceptable.
706        let mut a = DenseBlock {
707            rows: 2,
708            cols: 2,
709            data: vec![0.0, 1.0, 0.0, 0.0],
710        };
711        let mut p = Perm::identity(0);
712        let mut two = Vec::new();
713        let s = NaiveDense.sytrf(&mut a, &mut p, &mut two).unwrap();
714        assert_eq!(
715            s,
716            Inertia {
717                positive: 1,
718                negative: 1,
719                zero: 0
720            }
721        );
722        assert_eq!(two, vec![true, true]);
723        // A singular block has a zero pivot.
724        let mut z = DenseBlock {
725            rows: 2,
726            cols: 2,
727            data: vec![1.0, 0.0, 0.0, 0.0],
728        };
729        let s = NaiveDense.sytrf(&mut z, &mut p, &mut two).unwrap();
730        assert_eq!(s.zero, 1);
731    }
732
733    proptest! {
734        #![proptest_config(ProptestConfig::with_cases(48))]
735        /// (D1), (D2) and NaiveDense vs FaerDense: equal to working
736        /// precision (factors of potrf, trsm, gemm, syrk entry by entry;
737        /// sytrf by reconstruction and identical inertia).
738        #[test]
739        fn kernels_agree(n in 1_usize..24, k in 1_usize..9, seed in any::<u64>()) {
740            // potrf.
741            let spd = symmetric(n, seed, true);
742            let mut x = spd.clone();
743            NaiveDense.potrf(&mut x).unwrap();
744            let mut llt = DenseBlock::zeros(n, n);
745            for j in 0..n {
746                for i in j..n {
747                    llt.set(i, j, (0..=j).map(|p| x.get(i, p) * x.get(j, p)).sum());
748                }
749            }
750            prop_assert!(close(&llt, &spd, true));
751            // sytrf on an indefinite block.
752            let ind = symmetric(n, seed ^ 0x9e37, false);
753            let mut f = ind.clone();
754            let mut p = Perm::identity(0);
755            let mut two = Vec::new();
756            let s = NaiveDense.sytrf(&mut f, &mut p, &mut two).unwrap();
757            prop_assert_eq!(s.positive + s.negative + s.zero, n);
758            prop_assert!(sytrf_error(&ind, &f, &p, &two) <= 1e-9);
759            // trsm, gemm, syrk.
760            let b = block(k, n, seed ^ 0x7);
761            let mut t1 = b.clone();
762            NaiveDense.trsm_lower(&x, &mut t1, true);
763            let b2 = block(n, k, seed ^ 0xb);
764            let mut t2 = b2.clone();
765            NaiveDense.trsm_lower(&x, &mut t2, false);
766            let a3 = block(k, n, seed ^ 0xd);
767            let mut c3 = block(k, k, seed ^ 0x11);
768            let c3_0 = c3.clone();
769            NaiveDense.gemm_sub(&a3, &b, &mut c3);
770            for i in 0..k {
771                for j in 0..k {
772                    let v: f64 = c3_0.get(i, j) - (0..n).map(|p| a3.get(i, p) * b.get(j, p)).sum::<f64>();
773                    prop_assert!(approx_eq(v, c3.get(i, j), 1e-12));
774                }
775            }
776            let mut c4 = symmetric(k, seed ^ 0x13, false);
777            let c4_0 = c4.clone();
778            NaiveDense.syrk_sub(&a3, &mut c4);
779            #[cfg(feature = "faer")]
780            {
781                let mut y = spd.clone();
782                FaerDense.potrf(&mut y).unwrap();
783                prop_assert!(close(&x, &y, true));
784                let mut g = ind.clone();
785                let mut q = Perm::identity(0);
786                let mut two_f = Vec::new();
787                let sf = FaerDense.sytrf(&mut g, &mut q, &mut two_f).unwrap();
788                prop_assert_eq!(s, sf);
789                prop_assert!(sytrf_error(&ind, &g, &q, &two_f) <= 1e-9);
790                let mut u1 = b.clone();
791                FaerDense.trsm_lower(&x, &mut u1, true);
792                prop_assert!(close(&t1, &u1, false));
793                let mut u2 = b2.clone();
794                FaerDense.trsm_lower(&x, &mut u2, false);
795                prop_assert!(close(&t2, &u2, false));
796                let mut d3 = c3_0.clone();
797                FaerDense.gemm_sub(&a3, &b, &mut d3);
798                prop_assert!(close(&c3, &d3, false));
799                let mut d4 = c4_0.clone();
800                FaerDense.syrk_sub(&a3, &mut d4);
801                prop_assert!(close(&c4, &d4, true));
802                // (D3): the strict upper triangle is untouched.
803                prop_assert!((0..k).all(|j| (0..j).all(|i| approx_eq(d4.get(i, j), c4_0.get(i, j), 0.0))));
804            }
805            prop_assert!((0..k).all(|j| (0..j).all(|i| approx_eq(c4.get(i, j), c4_0.get(i, j), 0.0))));
806            // trsm really solved: t1 Lᵀ = b.
807            for i in 0..k {
808                for j in 0..n {
809                    let v: f64 = (0..=j).map(|p| t1.get(i, p) * x.get(j, p)).sum();
810                    prop_assert!(approx_eq(v, b.get(i, j), 1e-9));
811                }
812            }
813        }
814    }
815}