Skip to main content

xingo_sparse/
kkt.rs

1//! # Chapter 5.14 — KKT systems: analyze once, factorize many
2//!
3//! *New with task L1.5 (PLAN.md trilha L; the contract's §5.7).*
4//!
5//! ## Purpose
6//! At every iteration a primal-dual interior-point method solves a linear
7//! system whose matrix has a *fixed pattern* and *changing values*: the
8//! normal equations `A D Aᵀ` or the augmented (KKT) system
9//! `[−D, Aᵀ; A, 0]`, where the diagonal `D > 0` spreads over many orders
10//! of magnitude as the iterates approach the boundary (Wright 1997, ch.
11//! 11). The interface below is the shape of CHOLMOD (Chen, Davis, Hager &
12//! Rajamanickam 2008): a *symbolic* phase that depends on the pattern only
13//! (ordering, elimination tree, structure of the factor) is done once, a
14//! *numeric* phase is repeated for every new set of values, and a *solve*
15//! phase uses the factor. On top of it sit two things that every backend
16//! shares and that therefore live here, not in a backend:
17//!
18//! * the **regularization policy** (Altman & Gondzio 1999): which static
19//!   shifts `ρ`, `δ` enter which diagonal block, which sign each pivot must
20//!   have, which pivots a backend may bump, and the inertia check (S6);
21//! * **iterative refinement** against the matrix the caller really wants
22//!   solved ([`KktSolver::solve_refined`], a provided method), which keeps
23//!   its best iterate (S7).
24//!
25//! ## Notation
26//! `K ∈ R^{N×N}` symmetric, given by its **lower triangle** in a
27//! [`CscMatrix`] (entries `i ≥ j`; CHOLMOD's `stype < 0`). A matrix that
28//! stores both triangles is accepted when its pattern is symmetric: the
29//! entries above the diagonal are then ignored. Two forms ([`KktForm`]):
30//!
31//! * `NormalEquations`: `K = A D Aᵀ` (`N = m`), positive (semi)definite;
32//! * `Augmented { n_primal: n, n_dual: m }`: `K = [−D, Aᵀ; A, 0]`
33//!   (`N = n + m`), the `n` primal unknowns first.
34//!
35//! A [`Regularization`] `(ρ, δ, τ, β, scale)` = (`primal`, `dual`,
36//! `pivot_tol`, `bump`, `scale`) defines the matrix the backend
37//! factorizes. With `S = diag(s)` the symmetric equilibration of `K`
38//! (step K2′ below; powers of two) and `Σ = diag(−ρ I_n, δ I_m)` for the
39//! augmented system (`Σ = δ I_m` for the normal equations),
40//! ```text
41//!   K_reg = K + S⁻¹ Σ S⁻¹    (ShiftScale::Equilibrated, the default),
42//!   K_reg = K + Σ            (ShiftScale::Absolute),
43//! ```
44//! so the default shifts are taken *in the equilibrated matrix*
45//! `K̃ = S K S`, whose rows have their largest entry near 1: `δ = 10⁻⁸` is
46//! then relative to the magnitude of each row, whatever the spread of
47//! `D`. The *pivot signs* are `σ_k = −1` for the primal rows, `+1` for the
48//! dual rows (all `+1` for the normal equations). The *inertia* of a symmetric
49//! matrix is the triple ([`Inertia`]) of the numbers of positive, negative
50//! and zero eigenvalues; by Sylvester's law it equals the count of
51//! positive, negative and zero pivots of any `P K Pᵀ = L Δ Lᵀ` with `Δ`
52//! diagonal, and, for `Δ` block diagonal with `1×1` and `2×2` blocks,
53//! the sum of the inertias of the blocks. The *relative residual* of `x`
54//! is `res(x) = ‖K x − b‖_∞ / (1 + ‖b‖_∞)`.
55//!
56//! ## Why the regularized augmented system needs no pivoting
57//! With `D > 0`, `ρ > 0` and `δ > 0`, `K_reg` is *quasi-definite*: its
58//! `(1,1)` block is negative definite and its `(2,2)` block positive
59//! definite. Vanderbei (1995) proved that every symmetric permutation of a
60//! quasi-definite matrix has an `LDLᵀ` factorization with diagonal `Δ`,
61//! the first block's pivots negative and the second's positive; Gill,
62//! Saunders & Shinnerl (1996) bound its stability by the size of the
63//! regularization. So the ordering can be chosen for sparsity alone, at
64//! analysis time — the property that makes "analyze once" possible for the
65//! augmented system — and the inertia is known in advance: `(m, n, 0)`.
66//! Without regularization (`ρ = 0` or `δ = 0`) this guarantee is lost, and
67//! a backend needs `2×2` pivots (Bunch & Kaufman 1977) or dynamic
68//! regularization to factorize.
69//!
70//! *On the sign convention.* The contract's (S6) writes the expected
71//! inertia of the augmented system as `(n, m, 0)` for the matrix
72//! `[−D − ρI, Aᵀ; A, δI]`; for that matrix the `n` primal pivots are
73//! *negative* and the `m` dual pivots *positive*, so with the field order
74//! `(positive, negative, zero)` of [`Inertia`] the triple is
75//! `{ positive: m, negative: n, zero: 0 }` — which is what this chapter
76//! checks (`n` primal and `m` dual eigenvalues, each block with its own
77//! sign; listed for ratification in L1.5).
78//!
79//! ## Algorithm K (the regularization policy shared by every backend)
80//! K1. [Validate.] `K` satisfies (S1); it is square, of order `N` equal to
81//!     the form's (`n + m` for the augmented system); its lower triangle
82//!     lies inside the pattern given to the analysis (missing entries are
83//!     zeros: an IPM value that became exactly zero leaves the CSC
84//!     pattern, (C3) of Chapter 5.8).
85//! K1′. [Finite input.] Every stored value of the lower triangle of `K`
86//!     is finite ([`KktError::Structure`] otherwise): a NaN or an infinity
87//!     in `K` would otherwise surface as a NaN solution.
88//! K2. [Assemble.] The lower triangle of `K` on the analyzed pattern, which
89//!     has a full diagonal (a dual diagonal entry absent from `K` is `0`).
90//! K2′. [Symmetric equilibration (Ruiz 2001).] Scale `K` to
91//!     `K̃ = S K S`, `S = diag(s)` with powers of two: start from
92//!     `s = 1`; in each pass compute, for every row `i` of the *scaled*
93//!     matrix (both triangles), `r_i = max_j |k̃_ij|` and multiply `s_i` by
94//!     `⌊1/√r_i⌉₂` (rows without entries keep `s_i`); stop after a pass
95//!     that changes no `s_i` — exact, the factors being powers of two — or
96//!     after [`EQUILIBRATION_PASSES`] passes. Every row of `K̃` then has
97//!     its largest entry in `[2^{-1}, 2^{1}]` roughly, whatever the spread
98//!     of `D`. The scaling is a congruence, so the inertia of `K̃` is that
99//!     of `K` (Sylvester). Being by powers of two it is exact, and an
100//!     `LDLᵀ` without pivoting *commutes* with it: the factor of `K̃` is
101//!     `(S L S⁻¹)(S Δ S)(S L S⁻¹)ᵀ` bit for bit, barring overflow and
102//!     underflow. So K2′ by itself changes exactly two things: the
103//!     thresholds `τ`, `β` of K3 become *relative* to the magnitude of
104//!     each row, and the intermediate values stay in range.
105//! K2″. [Static regularization.] Add the shifts on the diagonal of `K̃`:
106//!     `Σ` itself with [`ShiftScale::Equilibrated`] (`K̃_reg = K̃ + Σ`),
107//!     `S Σ S` with [`ShiftScale::Absolute`] (`K̃_reg = S (K + Σ) S`): `−ρ`
108//!     on the primal diagonal and `+δ` on the dual one (normal equations:
109//!     `+δ` everywhere; `ρ` does not apply, the caller folds it into
110//!     `D`). The backend factorizes `K̃_reg` and solves with `S F̃⁻¹ S`;
111//!     `S⁻¹ K̃_reg S⁻¹` is the `K_reg` of the notation above.
112//! K3. [Dynamic regularization.] If `τ = pivot_tol` is `Some`, the backend
113//!     replaces every pivot `d_k` with `σ_k d_k ≤ τ` (too small, or of the
114//!     wrong sign) by `σ_k β` while it eliminates — the dynamic part of
115//!     Altman & Gondzio's (1999) primal-dual regularization, in the form
116//!     faer's `LdltRegularization` applies — and counts the bumped pivots. Bumping
117//!     needs the sign `σ_k`, which only the form knows — the policy is
118//!     here; the elimination loop that applies it is the backend's.
119//! K3′. [Finite factor (S10).] Every stored value of the factor is
120//!     finite; otherwise the call fails with [`KktError::NotFinite`] at
121//!     the first column (original numbering) holding a NaN or an infinity
122//!     — an overflow of the elimination is reported, never handed to the
123//!     solves (where it would become a NaN solution).
124//! K4. [Inertia (S6).] From the pivots of the factor, count the inertia.
125//!     If it is not `(N, 0, 0)` (normal) or `(m, n, 0)` (augmented),
126//!     return [`KktError::WrongInertia`]. With `τ = Some`, every pivot has
127//!     the sign `σ_k` by K3, so the check cannot fail: the factor is that
128//!     of a perturbation of `K_reg` with the right inertia, and refinement
129//!     (Algorithm F) measures how far it is from `K`.
130//!
131//! ## Choosing the regularization
132//! Three facts, measured on the 558 Netlib systems of L1.5/L1.5b (93 LPs ×
133//! normal and augmented × the start, middle and end regimes of `D`),
134//! decide the default.
135//!
136//! 1. *A shift must be above the rounding level of its row.* A shift
137//!    `δ < ε_T · max|k_ij|` changes nothing in floating point. At the end
138//!    of an interior-point method `D` spreads to `10^{±10}` and
139//!    `max|K| ≈ 10^{10}`, so the absolute `ρ = δ = 10⁻⁸` of L1.5 failed on
140//!    32 of the 558 systems (zero pivots after overflow, or NaN
141//!    solutions). One shift relative to the whole matrix
142//!    ([`Regularization::relative_to`]`(k, 10⁻⁸)`, `δ ≈ 10²` there)
143//!    factorizes all 558, but perturbs the rows of small magnitude so much
144//!    that refinement against the unregularized `K` stalls (418 of 558 at
145//!    or below `10⁻¹⁰` after 50 classical steps). Shifts taken in the
146//!    equilibrated matrix (step K2″, [`ShiftScale::Equilibrated`]) are
147//!    relative *row by row*.
148//! 2. *The product `ρδ` must be well above the unit roundoff.* Eliminating
149//!    a primal pivot of size `~ρ` adds terms of size `~1/ρ` to the dual
150//!    block, whose pivots must stay above `δ`: with `ρ = δ = 10⁻⁸` in the
151//!    equilibrated matrix (`ρδ = 10⁻¹⁶ < ε`) nine augmented systems of the
152//!    end regime still got pivots of the wrong sign; with `ρδ ≥ 10⁻¹⁴` none
153//!    did (the stability analysis of quasi-definite factorizations of Gill,
154//!    Saunders & Shinnerl 1996 bounds the growth by the shifts).
155//! 3. *The normal equations see only `δ`*, and every unit of `δ` slows the
156//!    refinement on their small eigenvalues: `δ = 10⁻⁶` left 46 systems
157//!    above `10⁻¹⁰` after ten steps, `δ = 10⁻⁸` none (with Algorithm F's
158//!    GMRES).
159//!
160//! Hence the default `ρ = 10⁻⁶`, `δ = 10⁻⁸`, equilibrated: all 558 systems
161//! with the right inertia, no bump, and a residual against the
162//! unregularized `K` at or below `10⁻¹⁰` after the ten steps of
163//! [`Refinement::default`] (L1.5b, D1–D3; the bumping of K3 is the safety
164//! net, not the mechanism). An interior-point method that wants other
165//! values passes them; [`ShiftScale::Absolute`] keeps shifts in the
166//! caller's units.
167//!
168//! ## Algorithm F (Refinement by preconditioned GMRES, with the best iterate; (S7))
169//! Given a factor `F ≈ K_reg`, the matrix `K` to be solved (the
170//! *unregularized* one, or `K_reg` itself — whichever the caller passes),
171//! `b` and a [`Refinement`] `(t_max, target)` = (`max_iters`,
172//! `target_residual`), where `t_max` bounds the solves with `F` after the
173//! first one:
174//! F1. `x ← F⁻¹ b`; `r ← b − K x`; `ρ ← res(x)`; `x* ← x`, `ρ* ← ρ`;
175//!     `t ← 0`.
176//! F2. [Done?] If `ρ* ≤ target`, `ρ` is not finite, or `t = t_max`, go to
177//!     F6.
178//! F3. [Arnoldi cycle, right preconditioned (Saad 2003, §9.3.2).] `x ← x*`,
179//!     `r ← b − K x` (the residual of the best iterate), `β ← ‖r‖₂`,
180//!     `v₁ ← r/β`, `g ← β e₁`, `m ← t_max − t`. For `j = 1, 2, …, m`:
181//!     `z_j ← F⁻¹ v_j`, `t ← t + 1`; `w ← K z_j`; modified Gram–Schmidt
182//!     `h_ij ← ⟨w, v_i⟩`, `w ← w − h_ij v_i` for `i ≤ j`;
183//!     `h_{j+1,j} ← ‖w‖₂`; apply the previous Givens rotations to column
184//!     `j` of `H`, compute the rotation that annihilates `h_{j+1,j}` and
185//!     apply it to `g`. Leave the loop with `k = j` when `h_{j+1,j} = 0`
186//!     (the Krylov space is invariant: the solution is in it), when
187//!     `|g_{j+1}| ≤ target·(1 + ‖b‖_∞)` (in exact arithmetic `|g_{j+1}|`
188//!     is the 2-norm of the residual, which bounds its ∞-norm), or when
189//!     `j = m`.
190//! F4. [Update.] Solve the `k × k` upper triangular system `R y = g_{1:k}`
191//!     (the rotated `H`); `x ← x* + Σ_j y_j z_j`; `r ← b − K x`;
192//!     `ρ ← res(x)`. If `ρ < ρ*`, set `x* ← x`, `ρ* ← ρ`.
193//! F5. Go to F2 (a new cycle restarts from `x*`).
194//! F6. Return `x*` with `iters = t`, `residual = ρ*` and
195//!     `converged = (ρ* ≤ target)`.
196//!
197//! *Why GMRES and not the classical iteration.* The classical refinement
198//! `x ← x + F⁻¹ r` of L1.5 contracts the error by the factor
199//! `‖I − F⁻¹ K‖ = ‖F⁻¹ (K_reg − K)‖` per step; on the eigenvectors of `K`
200//! whose eigenvalues are not much larger than the shifts — the nearly
201//! rank-deficient `A D Aᵀ` of the middle of an interior-point method, or
202//! an augmented system with `D` spread to `10^{±4}` — that factor is close
203//! to 1, and ten steps left 15 of the 558 Netlib systems of L1.5b above
204//! `10⁻¹⁰` against the unregularized `K`. Right-preconditioned GMRES
205//! minimizes `‖b − K x‖₂` over the same affine Krylov space
206//! `x₀ + span{F⁻¹r₀, (F⁻¹K) F⁻¹r₀, …}` that holds the classical
207//! iterates, so in exact arithmetic it is never worse, and the few
208//! eigenvalues of `F⁻¹ K` far from 1 — one per small eigenvalue of `K`,
209//! the regularization being a small perturbation elsewhere — cost one
210//! step each. This is GMRES preconditioned by a perturbed (statically
211//! pivoted, here statically regularized) `LDLᵀ`, analyzed by Arioli,
212//! Duff, Gratton & Pralet (2007), and GMRES-based iterative refinement
213//! (Carson & Higham 2017). One cycle with all the budget is the normal
214//! case; F5 restarts only when the estimate `|g_{k+1}|` and the computed
215//! residual disagree (rounding) or the budget allows another cycle after
216//! a breakdown.
217//!
218//! *Memory.* A cycle of `m` steps keeps `m + 1` Arnoldi vectors `v_j` and
219//! `m` preconditioned vectors `z_j` (so F4 needs no further solve), plus
220//! `H` and the rotations: `(2m + 3) N + O(m²)` scalars, held by a
221//! [`RefineWorkspace`] that [`KktSolver::solve_refined_with`] reuses
222//! across calls — after the first call with a given order and budget,
223//! refinement allocates nothing (Chapter 5.15 for the solves themselves).
224//!
225//! The residual is computed with the stored lower triangle of `K`
226//! (`y_i += k_ij x_j` and, for `i ≠ j`, `y_j += k_ij x_i`), in the working
227//! precision of `T` — fixed-precision refinement, which brings the
228//! componentwise backward error of a backward-stable factorization down to
229//! the order of the unit roundoff (Skeel 1980; Higham 2002, ch. 12); with
230//! a regularized factor it also removes the bias of the shifts (Altman &
231//! Gondzio 1999, §6). When `K` is singular but `b` is consistent the
232//! iteration still converges in the residual.
233//!
234//! ## Invariants
235//! (K1) The pattern given to [`KktSolver::analyze`] is square and either
236//!      lower triangular or structurally symmetric
237//!      ([`KktError::NotSymmetric`] otherwise).
238//! (S6) (the contract's) [`KktSolver::factorize`] returns the inertia of
239//!      the factor it computed; it is `(N, 0, 0)` for the normal equations
240//!      and `(m, n, 0)` = `{positive: m, negative: n, zero: 0}` for the
241//!      augmented system, or the call fails with
242//!      [`KktError::WrongInertia`]. Asserted on every successful
243//!      factorization in debug builds.
244//! (S7) (the contract's) [`KktSolver::solve_refined`] never returns a
245//!      residual larger than that of its first solve: it keeps the best
246//!      iterate (step F4). Asserted in debug builds.
247//! (S10) A factor returned by [`KktSolver::factorize`] holds only finite
248//!      values (step K3′); [`KktSolver::solve_refined`] reports a
249//!      non-finite solution explicitly: its `residual` is NaN or infinite
250//!      exactly when the returned `x` has a non-finite entry, and then
251//!      `converged = false` (the input `K`, `b` being finite).
252//!
253//! ## References
254//! Chen, Davis, Hager & Rajamanickam, "Algorithm 887: CHOLMOD, supernodal
255//! sparse Cholesky factorization and update/downdate", ACM TOMS 35(3),
256//! 2008 (the analyze/factorize/solve shape, the lower-triangle input, the
257//! supernodal/simplicial selection by flops per `nnz(L)` — read, not
258//! copied); Altman & Gondzio, "Regularized symmetric indefinite systems in
259//! interior point methods for linear and quadratic optimization", Optim.
260//! Methods Softw. 11–12, 1999; Vanderbei, "Symmetric quasidefinite
261//! matrices", SIAM J. Optim. 5(1), 1995; Gill, Saunders & Shinnerl, "On
262//! the stability of Cholesky factorization for symmetric quasidefinite
263//! systems", SIAM J. Matrix Anal. Appl. 17(1), 1996; Wright,
264//! *Primal-Dual Interior-Point Methods*, SIAM 1997, ch. 11; Bunch &
265//! Kaufman, "Some stable methods for calculating inertia and solving
266//! symmetric linear systems", Math. Comp. 31, 1977; Skeel, "Iterative
267//! refinement implies numerical stability for Gaussian elimination", Math.
268//! Comp. 35, 1980; Higham, *Accuracy and Stability of Numerical
269//! Algorithms*, 2nd ed. 2002, ch. 11–12; Ruiz, "A scaling algorithm to
270//! equilibrate both rows and columns norms in matrices", RAL-TR-2001-034,
271//! 2001 (symmetric variant, rounded to powers of two here); Stellato et al., "OSQP: an
272//! operator splitting solver for quadratic programs", Math. Prog. Comp.
273//! 12, 2020 (quasi-definite `LDLᵀ` without pivoting in an optimization
274//! solver); Saad, *Iterative Methods for Sparse Linear Systems*, 2nd ed.,
275//! SIAM 2003, §6.5 and §9.3 (GMRES, right preconditioning, Givens
276//! rotations); Arioli, Duff, Gratton & Pralet, "A note on GMRES
277//! preconditioned by a perturbed `LDLᵀ` decomposition with static
278//! pivoting", SIAM J. Sci. Comput. 29(5), 2007; Carson & Higham, "A new
279//! analysis of iterative refinement and its application to accurate
280//! solution of ill-conditioned sparse linear systems", SIAM J. Sci.
281//! Comput. 39(6), 2017; Saunders & Tomlin, "Solving regularized linear
282//! programs using barrier methods and KKT systems", SOL 96-4, 1996 (the
283//! product `ρδ` above the rounding level for a stable quasi-definite
284//! factorization).
285
286use crate::real::{Real, is_exact_zero, is_nan, le, lt};
287use crate::{CscMatrix, KktError, Ordering, Pattern, SparseError};
288
289/// Inertia `(positive, negative, zero)` of a symmetric factorization: the
290/// numbers of positive, negative and zero pivots (eigenvalues, by
291/// Sylvester's law).
292#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
293pub struct Inertia {
294    /// Positive pivots.
295    pub positive: usize,
296    /// Negative pivots.
297    pub negative: usize,
298    /// Zero pivots.
299    pub zero: usize,
300}
301
302/// Where the static shifts `ρ`, `δ` of a [`Regularization`] are applied
303/// (step K2″ of Algorithm K).
304#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
305pub enum ShiftScale {
306    /// On the diagonal of the equilibrated matrix `K̃ = S K S` (step K2′),
307    /// whose rows have their largest entry near 1: the shifts are
308    /// relative to the magnitude of each row, `K_reg = K + S⁻¹ Σ S⁻¹`.
309    /// The default.
310    #[default]
311    Equilibrated,
312    /// On the diagonal of `K` itself, in the caller's units:
313    /// `K_reg = K + Σ`.
314    Absolute,
315}
316
317/// Regularization of a quasi-definite KKT matrix (Altman & Gondzio 1999;
318/// Algorithm K).
319#[derive(Clone, Copy, Debug, PartialEq)]
320pub struct Regularization<T: Real = f64> {
321    /// `ρ ≥ 0`, added to the primal block (which becomes `−D − ρI`, in
322    /// the units [`Regularization::scale`] says); ignored for the normal
323    /// equations.
324    pub primal: T,
325    /// `δ ≥ 0`, added to the dual block (`δI`), or to the whole diagonal
326    /// of the normal equations (`A D Aᵀ + δI`), in the units
327    /// [`Regularization::scale`] says.
328    pub dual: T,
329    /// Below this magnitude (or with the wrong sign) a pivot of the
330    /// equilibrated matrix is bumped to `±bump` with the sign the inertia
331    /// expects (dynamic regularization, step K3); `None` disables it.
332    pub pivot_tol: Option<T>,
333    /// The magnitude `β > 0` of a bumped pivot (of the equilibrated
334    /// matrix).
335    pub bump: T,
336    /// Whether `ρ`, `δ` are taken in the equilibrated matrix (the default)
337    /// or in `K` itself (step K2″).
338    pub scale: ShiftScale,
339}
340
341impl<T: Real> Default for Regularization<T> {
342    /// `ρ = 10⁻⁶`, `δ = 10⁻⁸` *in the equilibrated matrix*
343    /// ([`ShiftScale::Equilibrated`]: relative to each row), and pivots of
344    /// the wrong sign or of magnitude at most `τ = 10⁻¹³` bumped to
345    /// `±β = ±2·10⁻⁷`. `ρδ = 10⁻¹⁴`, about 50 units of roundoff, keeps the
346    /// regularized augmented matrix numerically quasi-definite; `δ`
347    /// alone, small, is what the normal equations see (Chapter 5.14,
348    /// "Choosing the regularization", measured in L1.5b). `τ`, `β` are of
349    /// the order of the defaults of current interior-point codes
350    /// (Clarabel's documented dynamic regularization; not tuned here).
351    fn default() -> Self {
352        Self {
353            primal: T::from_f64(1e-6),
354            dual: T::from_f64(1e-8),
355            pivot_tol: Some(T::from_f64(1e-13)),
356            bump: T::from_f64(2e-7),
357            scale: ShiftScale::Equilibrated,
358        }
359    }
360}
361
362impl<T: Real> Regularization<T> {
363    /// No regularization at all: `ρ = δ = 0`, no bumping (the factorization
364    /// then fails on a zero pivot, or with the wrong inertia).
365    #[must_use]
366    pub fn none() -> Self {
367        Self {
368            primal: T::ZERO,
369            dual: T::ZERO,
370            pivot_tol: None,
371            bump: T::ZERO,
372            scale: ShiftScale::Equilibrated,
373        }
374    }
375
376    /// Absolute static shifts relative to the largest entry of `k`:
377    /// `ρ = δ = factor · max|k_ij|` with [`ShiftScale::Absolute`], and the
378    /// dynamic part of [`Regularization::default`] (the L1.5 variant).
379    ///
380    /// One shift for the whole matrix: at the end of an interior-point
381    /// method, where `max|k_ij| ≈ 10^{10}`, `factor = 10⁻⁸` gives
382    /// `δ ≈ 10²`, a large perturbation of the rows of small magnitude —
383    /// refinement against the unregularized `K` then converges slowly
384    /// (Chapter 5.14, "Choosing the regularization"). The default
385    /// ([`ShiftScale::Equilibrated`]) is relative row by row instead.
386    #[must_use]
387    pub fn relative_to(k: &CscMatrix<T>, factor: T) -> Self {
388        let big = inf_norm(&k.val);
389        Self {
390            primal: factor * big,
391            dual: factor * big,
392            scale: ShiftScale::Absolute,
393            ..Self::default()
394        }
395    }
396
397    /// `ρ > 0` and `δ > 0`: the regularized augmented matrix is
398    /// quasi-definite (it factorizes without pivoting, in any order).
399    pub fn is_quasi_definite(&self) -> bool {
400        lt(T::ZERO, self.primal) && lt(T::ZERO, self.dual)
401    }
402}
403
404/// Iterative refinement parameters (Algorithm F).
405#[derive(Clone, Copy, Debug, PartialEq)]
406pub struct Refinement<T: Real = f64> {
407    /// Refinement steps after the first solve.
408    pub max_iters: usize,
409    /// Stop when `‖K x − b‖_∞ / (1 + ‖b‖_∞)` is at or below this.
410    pub target_residual: T,
411}
412
413impl<T: Real> Default for Refinement<T> {
414    /// Ten steps, target `10⁻¹²`.
415    fn default() -> Self {
416        Self {
417            max_iters: 10,
418            target_residual: T::from_f64(1e-12),
419        }
420    }
421}
422
423/// What refinement achieved (S7).
424#[derive(Clone, Copy, Debug, PartialEq)]
425pub struct RefineReport<T: Real = f64> {
426    /// Refinement steps taken (solves after the first).
427    pub iters: usize,
428    /// `‖K x − b‖_∞ / (1 + ‖b‖_∞)` of the returned `x`.
429    pub residual: T,
430    /// `residual ≤ target_residual`.
431    pub converged: bool,
432}
433
434/// Which system the interior-point method hands over.
435#[derive(Clone, Copy, Debug, PartialEq, Eq)]
436pub enum KktForm {
437    /// `A D Aᵀ + δI`, positive definite.
438    NormalEquations,
439    /// `[−D − ρI, Aᵀ; A, δI]`, quasi-definite; the `n_primal` primal rows
440    /// come first.
441    Augmented {
442        /// `n`, the order of the `(1,1)` block.
443        n_primal: usize,
444        /// `m`, the order of the `(2,2)` block.
445        n_dual: usize,
446    },
447}
448
449impl KktForm {
450    /// The inertia (S6) requires of a factor of order `n`.
451    pub fn expected_inertia(self, n: usize) -> Inertia {
452        match self {
453            Self::NormalEquations => Inertia {
454                positive: n,
455                negative: 0,
456                zero: 0,
457            },
458            Self::Augmented { n_primal, n_dual } => Inertia {
459                positive: n_dual,
460                negative: n_primal,
461                zero: 0,
462            },
463        }
464    }
465}
466
467/// Analyze/factorize/solve of symmetric systems with a fixed pattern (the
468/// CHOLMOD shape, read, not copied). Regularization (its policy,
469/// Algorithm K) and refinement (Algorithm F) are shared by every backend.
470///
471/// ```
472/// # #[cfg(feature = "faer")] {
473/// use xingo_sparse::{
474///     Amd, Col, CscMatrix, FaerLdl, KktForm, KktSolver, Refinement, Regularization, Row,
475/// };
476/// // K = [−D, Aᵀ; A, 0] with D = diag(1, 2), A = [1 1]: lower triangle.
477/// let k = CscMatrix::from_triplets(3, 3, &[
478///     (Row(0), Col(0), -1.0), (Row(2), Col(0), 1.0),
479///     (Row(1), Col(1), -2.0), (Row(2), Col(1), 1.0),
480/// ])
481/// .unwrap();
482/// let form = KktForm::Augmented { n_primal: 2, n_dual: 1 };
483/// let solver = FaerLdl::default();
484/// let sym = solver.analyze(&k.pattern(), form, &Amd::default()).unwrap();
485/// let (num, inertia) = solver.factorize(&sym, &k, Regularization::default()).unwrap();
486/// assert_eq!(inertia, form.expected_inertia(3)); // (S6): 1 positive, 2 negative
487/// let b = [1.0, 0.0, 3.0];
488/// let mut x = [0.0; 3];
489/// let report = solver.solve_refined(&num, &k, &b, &mut x, Refinement::default());
490/// assert!(report.converged && report.residual <= 1e-12);
491/// # }
492/// ```
493pub trait KktSolver<T: Real> {
494    /// The result of the symbolic phase.
495    type Symbolic;
496    /// The result of the numeric phase.
497    type Numeric;
498
499    /// Symbolic phase on the lower triangle of `pattern` (plus a full
500    /// diagonal) with the given ordering; done once per pattern.
501    fn analyze(
502        &self,
503        pattern: &Pattern,
504        form: KktForm,
505        ordering: &dyn Ordering,
506    ) -> Result<Self::Symbolic, KktError>;
507
508    /// Numeric phase on the lower triangle of `k` (inside the analyzed
509    /// pattern) with regularization (Algorithm K); returns the factor and
510    /// its inertia (S6).
511    fn factorize(
512        &self,
513        symbolic: &Self::Symbolic,
514        k: &CscMatrix<T>,
515        reg: Regularization<T>,
516    ) -> Result<(Self::Numeric, Inertia), KktError>;
517
518    /// `x ← F⁻¹ x` in place, dense, with `F` the regularized factor.
519    ///
520    /// # Panics
521    /// If `x.len()` is not the order of the factor.
522    fn solve(&self, numeric: &Self::Numeric, x: &mut [T]);
523
524    /// Solve followed by iterative refinement against `k` (Algorithm F);
525    /// keeps the best iterate (S7). `k` is the matrix to be solved — the
526    /// unregularized one the factor approximates, or any other with the
527    /// same order; only its lower triangle is read. `x` is overwritten.
528    /// Allocates its work vectors; [`KktSolver::solve_refined_with`]
529    /// reuses them.
530    ///
531    /// # Panics
532    /// If `b`, `x` and `k` do not have the order of the factor.
533    fn solve_refined(
534        &self,
535        numeric: &Self::Numeric,
536        k: &CscMatrix<T>,
537        b: &[T],
538        x: &mut [T],
539        refinement: Refinement<T>,
540    ) -> RefineReport<T> {
541        let mut work = RefineWorkspace::new();
542        self.solve_refined_with(numeric, k, b, x, refinement, &mut work)
543    }
544
545    /// [`KktSolver::solve_refined`] with caller-owned work vectors: once
546    /// `work` has served a call with the same order and budget, the
547    /// refinement allocates nothing.
548    ///
549    /// # Panics
550    /// If `b`, `x` and `k` do not have the order of the factor.
551    #[allow(clippy::too_many_lines)] // one step of Algorithm F per block, as in the chapter
552    fn solve_refined_with(
553        &self,
554        numeric: &Self::Numeric,
555        k: &CscMatrix<T>,
556        b: &[T],
557        x: &mut [T],
558        refinement: Refinement<T>,
559        work: &mut RefineWorkspace<T>,
560    ) -> RefineReport<T> {
561        let n = b.len();
562        assert_eq!(x.len(), n, "solve_refined: x has the wrong length");
563        assert!(
564            k.n_rows == n && k.n_cols() == n,
565            "solve_refined: k has the wrong order"
566        );
567        let budget = refinement.max_iters;
568        work.reserve(n, budget);
569        let scale = T::ONE + inf_norm(b);
570        let tol = refinement.target_residual * scale;
571        // F1.
572        x.copy_from_slice(b);
573        self.solve(numeric, x);
574        let RefineWorkspace {
575            r,
576            cur,
577            v,
578            z,
579            h,
580            g,
581            cs,
582            sn,
583            y,
584        } = work;
585        residual(k, x, b, r);
586        let first = inf_norm(r) / scale;
587        let mut best = first;
588        let mut t = 0;
589        // `r` holds b − K x* at the top of every cycle.
590        loop {
591            // F2.
592            if !best.is_finite() || le(best, refinement.target_residual, T::ZERO) || t >= budget {
593                break;
594            }
595            // F3.
596            let m = budget - t;
597            let beta = norm2(r);
598            if !beta.is_finite() || is_exact_zero(beta) {
599                break;
600            }
601            for (vi, &ri) in v[..n].iter_mut().zip(r.iter()) {
602                *vi = ri / beta;
603            }
604            g.fill(T::ZERO);
605            g[0] = beta;
606            let mut steps = 0;
607            for j in 0..m {
608                let (zj, vj) = (&mut z[j * n..(j + 1) * n], &v[j * n..(j + 1) * n]);
609                zj.copy_from_slice(vj);
610                self.solve(numeric, zj);
611                t += 1;
612                steps = j + 1;
613                let (done, rest) = v.split_at_mut((j + 1) * n);
614                let w = &mut rest[..n];
615                sym_lower_matvec(k, &z[j * n..(j + 1) * n], w);
616                // Column j of H, (m + 1) rows, column-major.
617                let col = &mut h[j * (m + 1)..(j + 1) * (m + 1)];
618                for i in 0..=j {
619                    let vi = &done[i * n..(i + 1) * n];
620                    let hij = dot(w, vi);
621                    col[i] = hij;
622                    for (wk, &vk) in w.iter_mut().zip(vi) {
623                        *wk -= hij * vk;
624                    }
625                }
626                let hnext = norm2(w);
627                col[j + 1] = hnext;
628                if !hnext.is_finite() {
629                    steps = j;
630                    break;
631                }
632                // Previous rotations on column j.
633                for i in 0..j {
634                    let (a, c) = (col[i], col[i + 1]);
635                    col[i] = cs[i] * a + sn[i] * c;
636                    col[i + 1] = cs[i] * c - sn[i] * a;
637                }
638                // New rotation annihilating h_{j+1,j}.
639                let (a, c) = (col[j], col[j + 1]);
640                let rr = norm2(&[a, c]);
641                if is_exact_zero(rr) {
642                    // R singular in this column: drop the step.
643                    steps = j;
644                    break;
645                }
646                cs[j] = a / rr;
647                sn[j] = c / rr;
648                col[j] = rr;
649                col[j + 1] = T::ZERO;
650                g[j + 1] = -sn[j] * g[j];
651                g[j] *= cs[j];
652                if is_exact_zero(hnext) || le(g[j + 1].abs(), tol, T::ZERO) {
653                    break;
654                }
655                for wk in w.iter_mut() {
656                    *wk = *wk / hnext;
657                }
658            }
659            if steps == 0 {
660                break;
661            }
662            // F4: back substitution R y = g.
663            for i in (0..steps).rev() {
664                let mut acc = g[i];
665                for l in i + 1..steps {
666                    acc -= h[l * (m + 1) + i] * y[l];
667                }
668                y[i] = acc / h[i * (m + 1) + i];
669            }
670            cur.copy_from_slice(x);
671            for (l, &yl) in y[..steps].iter().enumerate() {
672                for (c, &zl) in cur.iter_mut().zip(&z[l * n..(l + 1) * n]) {
673                    *c += yl * zl;
674                }
675            }
676            residual(k, cur, b, r);
677            let res = inf_norm(r) / scale;
678            if res.is_finite() && lt(res, best) {
679                // `r` is the residual of the new x*.
680                x.copy_from_slice(cur);
681                best = res;
682            } else {
683                // F5 would restart from the same x*, whose cycle is the
684                // one just done (deterministic): stop.
685                break;
686            }
687        }
688        // F6.
689        debug_assert!(
690            !best.is_finite() || !first.is_finite() || le(best, first, T::ZERO),
691            "(S7): the returned residual exceeds the first one"
692        );
693        RefineReport {
694            iters: t,
695            residual: best,
696            converged: best.is_finite() && le(best, refinement.target_residual, T::ZERO),
697        }
698    }
699
700    /// Nonzeros of the factor (for the IPM log).
701    fn nnz_factor(&self, numeric: &Self::Numeric) -> usize;
702
703    /// A short name for logs and tables.
704    fn name(&self) -> &'static str;
705}
706
707/// `‖v‖_∞` (`0` for an empty vector; NaN if an entry is NaN — `max`
708/// alone would hide it).
709pub(crate) fn inf_norm<T: Real>(v: &[T]) -> T {
710    let mut m = T::ZERO;
711    for &x in v {
712        if is_nan(x) {
713            return x;
714        }
715        m = m.max(x.abs());
716    }
717    m
718}
719
720/// `‖v‖₂`, scaled against overflow (NaN propagates).
721fn norm2<T: Real>(v: &[T]) -> T {
722    let big = inf_norm(v);
723    if !big.is_finite() || is_exact_zero(big) {
724        return big;
725    }
726    let mut s = T::ZERO;
727    for &x in v {
728        let q = x / big;
729        s += q * q;
730    }
731    big * s.sqrt()
732}
733
734/// `⟨u, v⟩`.
735fn dot<T: Real>(u: &[T], v: &[T]) -> T {
736    let mut s = T::ZERO;
737    for (&a, &b) in u.iter().zip(v) {
738        s += a * b;
739    }
740    s
741}
742
743/// Work vectors of Algorithm F, reusable across calls of
744/// [`KktSolver::solve_refined_with`]: `(2m + 3) N + O(m²)` scalars for a
745/// budget of `m = max_iters` solves and order `N`. Grows on demand, never
746/// shrinks.
747#[derive(Clone, Debug, Default)]
748pub struct RefineWorkspace<T: Real = f64> {
749    r: Vec<T>,
750    cur: Vec<T>,
751    v: Vec<T>,
752    z: Vec<T>,
753    h: Vec<T>,
754    g: Vec<T>,
755    cs: Vec<T>,
756    sn: Vec<T>,
757    y: Vec<T>,
758}
759
760impl<T: Real> RefineWorkspace<T> {
761    /// An empty workspace (it grows at the first call).
762    #[must_use]
763    pub fn new() -> Self {
764        Self {
765            r: Vec::new(),
766            cur: Vec::new(),
767            v: Vec::new(),
768            z: Vec::new(),
769            h: Vec::new(),
770            g: Vec::new(),
771            cs: Vec::new(),
772            sn: Vec::new(),
773            y: Vec::new(),
774        }
775    }
776
777    /// Sizes the vectors for order `n` and budget `m` (allocates only when
778    /// they grow).
779    fn reserve(&mut self, n: usize, m: usize) {
780        fn fit<T: Real>(v: &mut Vec<T>, len: usize) {
781            if v.len() != len {
782                v.resize(len, T::ZERO);
783            }
784        }
785        fit(&mut self.r, n);
786        fit(&mut self.cur, n);
787        fit(&mut self.v, (m + 1) * n);
788        fit(&mut self.z, m * n);
789        fit(&mut self.h, (m + 1) * m);
790        fit(&mut self.g, m + 1);
791        fit(&mut self.cs, m);
792        fit(&mut self.sn, m);
793        fit(&mut self.y, m);
794    }
795}
796
797/// `y ← K x` with `K` given by the lower triangle of `k` (entries above
798/// the diagonal ignored).
799pub(crate) fn sym_lower_matvec<T: Real>(k: &CscMatrix<T>, x: &[T], y: &mut [T]) {
800    y.fill(T::ZERO);
801    for (j, &xj) in x.iter().enumerate() {
802        for p in k.col_ptr[j]..k.col_ptr[j + 1] {
803            let i = k.row_idx[p] as usize;
804            if i < j {
805                continue;
806            }
807            let v = k.val[p];
808            y[i] += v * xj;
809            if i != j {
810                y[j] += v * x[i];
811            }
812        }
813    }
814}
815
816/// `r ← b − K x` (lower triangle of `k`).
817fn residual<T: Real>(k: &CscMatrix<T>, x: &[T], b: &[T], r: &mut [T]) {
818    sym_lower_matvec(k, x, r);
819    for (ri, &bi) in r.iter_mut().zip(b) {
820        *ri = bi - *ri;
821    }
822}
823
824/// The lower triangle of a symmetric pattern plus a full diagonal, as the
825/// symbolic phase of every backend consumes it (step K1 for the pattern,
826/// invariant (K1)). The order `N` is checked against the form.
827#[cfg_attr(not(feature = "faer"), allow(dead_code))] // used by every backend
828pub(crate) fn lower_with_diagonal(pattern: &Pattern, form: KktForm) -> Result<Pattern, KktError> {
829    pattern.check_invariants()?;
830    let n = pattern.n_cols();
831    if pattern.n_rows != n {
832        return Err(KktError::NotSymmetric);
833    }
834    if let KktForm::Augmented { n_primal, n_dual } = form
835        && n_primal + n_dual != n
836    {
837        return Err(KktError::Structure(SparseError::Dimension {
838            what: "KKT order (n_primal + n_dual)",
839            expected: n_primal + n_dual,
840            found: n,
841        }));
842    }
843    let has_upper = (0..n).any(|j| pattern.col(j).first().is_some_and(|&i| (i as usize) < j));
844    if has_upper && pattern.transpose() != *pattern {
845        return Err(KktError::NotSymmetric);
846    }
847    let mut col_ptr = Vec::with_capacity(n + 1);
848    let mut row_idx = Vec::with_capacity(pattern.nnz() / 2 + n);
849    col_ptr.push(0);
850    for j in 0..n {
851        let rows = pattern.col(j);
852        let start = rows.partition_point(|&i| (i as usize) < j);
853        let below = &rows[start..];
854        if below.first() != Some(&crate::perm::idx32(j)) {
855            row_idx.push(crate::perm::idx32(j));
856        }
857        row_idx.extend_from_slice(below);
858        col_ptr.push(row_idx.len());
859    }
860    let l = Pattern {
861        n_rows: n,
862        col_ptr,
863        row_idx,
864    };
865    debug_assert_eq!(l.check_invariants(), Ok(()));
866    Ok(l)
867}
868
869/// The regularization is usable: `ρ, δ, β ≥ 0` finite, and with dynamic
870/// regularization `τ > 0` and `β > 0`.
871#[cfg_attr(not(feature = "faer"), allow(dead_code))] // used by every backend
872pub(crate) fn check_regularization<T: Real>(reg: &Regularization<T>) -> Result<(), KktError> {
873    for (what, x) in [
874        ("primal regularization", reg.primal),
875        ("dual regularization", reg.dual),
876        ("bump", reg.bump),
877    ] {
878        if !x.is_finite() || lt(x, T::ZERO) {
879            return Err(KktError::Structure(SparseError::InvalidValue {
880                what,
881                index: 0,
882                value: x.to_f64(),
883            }));
884        }
885    }
886    if let Some(tau) = reg.pivot_tol
887        && (!tau.is_finite() || !lt(T::ZERO, tau) || !lt(T::ZERO, reg.bump))
888    {
889        return Err(KktError::Structure(SparseError::InvalidValue {
890            what: "pivot_tol and bump (both must be positive)",
891            index: 0,
892            value: tau.to_f64(),
893        }));
894    }
895    Ok(())
896}
897
898/// The pivot signs `σ_k` of step K3: `−1` on the primal rows, `+1` on the
899/// dual rows (all `+1` for the normal equations).
900#[cfg_attr(not(feature = "faer"), allow(dead_code))] // used by every backend
901pub(crate) fn pivot_signs(form: KktForm, n: usize) -> Vec<i8> {
902    match form {
903        KktForm::NormalEquations => vec![1; n],
904        KktForm::Augmented { n_primal, .. } => {
905            (0..n).map(|k| if k < n_primal { -1 } else { 1 }).collect()
906        }
907    }
908}
909
910/// Steps K1′–K2″: scatters the lower triangle of `k` into the values of
911/// the analyzed lower pattern `l` (which has a full diagonal), equilibrates
912/// it (K2′) and adds the static shifts (K2″); returns the values of
913/// `K̃_reg` and the scaling `s`. Fails (K1, K1′) if `k` does not satisfy
914/// (S1), has the wrong order, has a lower entry outside `l`, or a
915/// non-finite lower entry.
916#[cfg_attr(not(feature = "faer"), allow(dead_code))] // used by every backend
917pub(crate) fn assemble_regularized<T: Real>(
918    l: &Pattern,
919    form: KktForm,
920    k: &CscMatrix<T>,
921    reg: &Regularization<T>,
922) -> Result<(Vec<T>, Vec<T>), KktError> {
923    let mut val = assemble(l, k)?;
924    let s = equilibrate_symmetric(l, &mut val);
925    for (j, &sj) in s.iter().enumerate() {
926        let shift = match form {
927            KktForm::NormalEquations => reg.dual,
928            KktForm::Augmented { n_primal, .. } => {
929                if j < n_primal {
930                    -reg.primal
931                } else {
932                    reg.dual
933                }
934            }
935        };
936        if !is_exact_zero(shift) {
937            // The diagonal is the first entry of column j of l.
938            let p = l.col_ptr[j];
939            val[p] += match reg.scale {
940                ShiftScale::Equilibrated => shift,
941                ShiftScale::Absolute => shift * sj * sj,
942            };
943        }
944    }
945    Ok((val, s))
946}
947
948/// Steps K1′–K2: the lower triangle of `k` on the analyzed pattern `l`.
949fn assemble<T: Real>(l: &Pattern, k: &CscMatrix<T>) -> Result<Vec<T>, KktError> {
950    k.check_invariants()?;
951    let n = l.n_cols();
952    if k.n_rows != n || k.n_cols() != n {
953        return Err(KktError::Structure(SparseError::Dimension {
954            what: "KKT matrix order (as analyzed)",
955            expected: n,
956            found: k.n_rows.max(k.n_cols()),
957        }));
958    }
959    let mut val = vec![T::ZERO; l.nnz()];
960    for j in 0..n {
961        let lrows = l.col(j);
962        let base = l.col_ptr[j];
963        let mut q = 0;
964        for p in k.col_ptr[j]..k.col_ptr[j + 1] {
965            let i = k.row_idx[p];
966            if (i as usize) < j {
967                continue;
968            }
969            // Both lists are increasing: a merge.
970            while q < lrows.len() && lrows[q] < i {
971                q += 1;
972            }
973            if q == lrows.len() || lrows[q] != i {
974                return Err(KktError::Structure(SparseError::Malformed(format!(
975                    "KKT entry ({i}, {j}) is outside the analyzed pattern"
976                ))));
977            }
978            // K1′.
979            if !k.val[p].is_finite() {
980                return Err(KktError::Structure(SparseError::InvalidValue {
981                    what: "KKT matrix entry (not finite)",
982                    index: p,
983                    value: k.val[p].to_f64(),
984                }));
985            }
986            val[base + q] = k.val[p];
987        }
988        // The diagonal is the first entry of column j of l.
989        debug_assert_eq!(lrows[0] as usize, j);
990    }
991    Ok(val)
992}
993
994/// Step K4 over a list of `1×1` pivots and `2×2` blocks `(a, b, c)` =
995/// `[a b; b c]`: the inertia by Sylvester's law.
996pub(crate) fn count_inertia<T: Real>(
997    ones: impl Iterator<Item = T>,
998    twos: impl Iterator<Item = (T, T, T)>,
999) -> Inertia {
1000    let mut s = Inertia::default();
1001    let mut one = |d: T| {
1002        if lt(T::ZERO, d) {
1003            s.positive += 1;
1004        } else if lt(d, T::ZERO) {
1005            s.negative += 1;
1006        } else {
1007            s.zero += 1;
1008        }
1009    };
1010    for d in ones {
1011        one(d);
1012    }
1013    for (a, b, c) in twos {
1014        // det < 0: one eigenvalue of each sign; det > 0: both of the sign
1015        // of the trace; det = 0: one zero, the other of the sign of the
1016        // trace.
1017        let det = a * c - b * b;
1018        if lt(det, T::ZERO) {
1019            one(T::ONE);
1020            one(-T::ONE);
1021        } else if lt(T::ZERO, det) {
1022            one(a + c);
1023            one(a + c);
1024        } else {
1025            one(T::ZERO);
1026            one(a + c);
1027        }
1028    }
1029    s
1030}
1031
1032/// Most passes of the symmetric equilibration (step K2′). A cap, not a
1033/// threshold: with power-of-two factors the iteration stops by itself
1034/// when no factor changes, which happens after a few passes in practice
1035/// (Ruiz 2001 reports convergence of the infinity-norm iteration to
1036/// within a factor 2 in a handful of steps).
1037pub const EQUILIBRATION_PASSES: usize = 20;
1038
1039/// Step K2′: symmetric equilibration of the lower pattern `l` with values
1040/// `val` (full diagonal present), in place; returns `s` (powers of two).
1041#[cfg_attr(not(feature = "faer"), allow(dead_code))] // used by every backend
1042pub(crate) fn equilibrate_symmetric<T: Real>(l: &Pattern, val: &mut [T]) -> Vec<T> {
1043    let n = l.n_cols();
1044    let mut s = vec![T::ONE; n];
1045    let mut rmax = vec![T::ZERO; n];
1046    let mut step = vec![T::ONE; n];
1047    for _ in 0..EQUILIBRATION_PASSES {
1048        rmax.fill(T::ZERO);
1049        for j in 0..n {
1050            let range = l.col_ptr[j]..l.col_ptr[j + 1];
1051            for (&i, &x) in l.row_idx[range.clone()].iter().zip(&val[range]) {
1052                let i = i as usize;
1053                let v = x.abs();
1054                rmax[i] = rmax[i].max(v);
1055                rmax[j] = rmax[j].max(v);
1056            }
1057        }
1058        let mut changed = false;
1059        for i in 0..n {
1060            step[i] = if rmax[i].is_finite() && lt(T::ZERO, rmax[i]) {
1061                pow2_round(rmax[i].sqrt().recip())
1062            } else {
1063                T::ONE
1064            };
1065            changed |= !is_exact_zero(step[i] - T::ONE);
1066        }
1067        if !changed {
1068            break;
1069        }
1070        for j in 0..n {
1071            let range = l.col_ptr[j]..l.col_ptr[j + 1];
1072            for (&i, x) in l.row_idx[range.clone()].iter().zip(&mut val[range]) {
1073                *x = *x * step[i as usize] * step[j];
1074            }
1075        }
1076        for (si, &t) in s.iter_mut().zip(&step) {
1077            *si *= t;
1078        }
1079    }
1080    s
1081}
1082
1083/// `⌊x⌉₂ = 2^{round(log₂ x)}` for a finite `x > 0`, computed in `f64`
1084/// (a power of two is exact in every `Real`).
1085fn pow2_round<T: Real>(x: T) -> T {
1086    T::from_f64(x.to_f64().log2().round().exp2())
1087}
1088
1089/// Step K4: the (S6) check.
1090#[cfg_attr(not(feature = "faer"), allow(dead_code))] // used by every backend
1091pub(crate) fn check_inertia(found: Inertia, expected: Inertia) -> Result<Inertia, KktError> {
1092    if found == expected {
1093        Ok(found)
1094    } else {
1095        Err(KktError::WrongInertia { found, expected })
1096    }
1097}
1098
1099#[cfg(test)]
1100// Exact equality is the property under test here (the assembly and the
1101// scaling are products and sums of powers of two, bit for bit); clippy 1.90
1102// flags it, 1.99 does not.
1103#[allow(clippy::float_cmp)]
1104mod tests {
1105    use super::*;
1106    use crate::{Col, Row};
1107
1108    #[test]
1109    fn lower_with_diagonal_accepts_lower_and_symmetric_patterns() {
1110        let lower = CscMatrix::from_triplets(3, 3, &[(Row(2), Col(0), 1.0), (Row(1), Col(1), 2.0)])
1111            .unwrap();
1112        let l = lower_with_diagonal(&lower.pattern(), KktForm::NormalEquations).unwrap();
1113        assert_eq!(l.col_ptr, vec![0, 2, 3, 4]);
1114        assert_eq!(l.row_idx, vec![0, 2, 1, 2]);
1115        let full = CscMatrix::from_triplets(3, 3, &[(Row(2), Col(0), 1.0), (Row(0), Col(2), 1.0)])
1116            .unwrap();
1117        assert_eq!(
1118            lower_with_diagonal(&full.pattern(), KktForm::NormalEquations).unwrap(),
1119            l.clone()
1120        );
1121        let skew = CscMatrix::from_triplets(3, 3, &[(Row(2), Col(0), 1.0), (Row(0), Col(1), 1.0)])
1122            .unwrap();
1123        assert_eq!(
1124            lower_with_diagonal(&skew.pattern(), KktForm::NormalEquations),
1125            Err(KktError::NotSymmetric)
1126        );
1127        let rect = CscMatrix::<f64>::zeros(2, 3);
1128        assert_eq!(
1129            lower_with_diagonal(&rect.pattern(), KktForm::NormalEquations),
1130            Err(KktError::NotSymmetric)
1131        );
1132        assert!(matches!(
1133            lower_with_diagonal(
1134                &lower.pattern(),
1135                KktForm::Augmented {
1136                    n_primal: 1,
1137                    n_dual: 1
1138                }
1139            ),
1140            Err(KktError::Structure(_))
1141        ));
1142    }
1143
1144    #[test]
1145    fn inertia_counts_follow_sylvester() {
1146        let s = count_inertia(
1147            [1.0, -2.0, 0.0].into_iter(),
1148            [(0.0, 1.0, 0.0), (2.0, 1.0, 3.0), (-1.0, 0.5, -1.0)].into_iter(),
1149        );
1150        assert_eq!(
1151            s,
1152            Inertia {
1153                positive: 4,
1154                negative: 4,
1155                zero: 1
1156            }
1157        );
1158        let expected = KktForm::Augmented {
1159            n_primal: 2,
1160            n_dual: 3,
1161        }
1162        .expected_inertia(5);
1163        assert_eq!(expected.positive, 3);
1164        assert_eq!(expected.negative, 2);
1165        assert!(check_inertia(s, expected).is_err());
1166    }
1167
1168    #[test]
1169    fn assembly_adds_the_shifts_and_rejects_foreign_entries() {
1170        let k = CscMatrix::from_triplets(
1171            3,
1172            3,
1173            &[
1174                (Row(0), Col(0), -1.0),
1175                (Row(2), Col(0), 1.0),
1176                (Row(1), Col(1), -2.0),
1177            ],
1178        )
1179        .unwrap();
1180        let form = KktForm::Augmented {
1181            n_primal: 2,
1182            n_dual: 1,
1183        };
1184        let l = lower_with_diagonal(&k.pattern(), form).unwrap();
1185        let mut reg = Regularization {
1186            primal: 0.5,
1187            dual: 0.25,
1188            pivot_tol: None,
1189            bump: 0.0,
1190            scale: ShiftScale::Absolute,
1191        };
1192        // S⁻¹ K̃_reg S⁻¹ = K + Σ exactly (powers of two), whatever S.
1193        let unscale = |(v, s): (Vec<f64>, Vec<f64>)| -> Vec<f64> {
1194            let mut u = Vec::new();
1195            for j in 0..l.n_cols() {
1196                for p in l.col_ptr[j]..l.col_ptr[j + 1] {
1197                    u.push(v[p] / (s[l.row_idx[p] as usize] * s[j]));
1198                }
1199            }
1200            u
1201        };
1202        let v = assemble_regularized(&l, form, &k, &reg).unwrap();
1203        assert!(v.1.iter().all(|&x| x > 0.0 && x.log2().fract() == 0.0));
1204        assert_eq!(unscale(v), vec![-1.5, 1.0, -2.5, 0.25]);
1205        // Equilibrated: K + S⁻¹ Σ S⁻¹.
1206        reg.scale = ShiftScale::Equilibrated;
1207        let (v, s) = assemble_regularized(&l, form, &k, &reg).unwrap();
1208        let u = unscale((v, s.clone()));
1209        assert_eq!(u[0], -1.0 - 0.5 / (s[0] * s[0]));
1210        assert_eq!(u[1], 1.0);
1211        assert_eq!(u[2], -2.0 - 0.5 / (s[1] * s[1]));
1212        assert_eq!(u[3], 0.25 / (s[2] * s[2]));
1213        // A non-finite entry is refused (K1′).
1214        let mut nan = k.clone();
1215        nan.val[1] = f64::NAN;
1216        assert!(matches!(
1217            assemble_regularized(&l, form, &nan, &reg),
1218            Err(KktError::Structure(_))
1219        ));
1220        let other = CscMatrix::from_triplets(3, 3, &[(Row(2), Col(1), 1.0)]).unwrap();
1221        assert!(matches!(
1222            assemble_regularized(&l, form, &other, &reg),
1223            Err(KktError::Structure(_))
1224        ));
1225    }
1226
1227    #[test]
1228    fn symmetric_matvec_reads_the_lower_triangle_only() {
1229        let k = CscMatrix::from_triplets(
1230            2,
1231            2,
1232            &[
1233                (Row(0), Col(0), 2.0),
1234                (Row(1), Col(0), 3.0),
1235                (Row(0), Col(1), 99.0),
1236                (Row(1), Col(1), 4.0),
1237            ],
1238        )
1239        .unwrap();
1240        let mut y = [0.0; 2];
1241        sym_lower_matvec(&k, &[1.0, 1.0], &mut y);
1242        assert_eq!(y, [5.0, 7.0]);
1243    }
1244}