treams_core/linalg/
mod.rs

1//! Dense linear solves and general complex eigensystems with analytic derivatives, and
2//! restarted GMRES. treams-rs extension.
3//!
4//! Two libraries share the linear algebra, by one rule:
5//!
6//! - nalgebra's `DMatrix` stores every dense matrix.
7//! - faer factors and multiplies dense complex matrices of any size, through this
8//!   module: LU solves ([`solve`], `Lu`), singular values ([`svdvals`]), eigensystems
9//!   ([`eig`]) and products (`product` and its variants). `view` and `view_mut` lend a
10//!   `DMatrix` to faer without a copy.
11//! - Real symmetric eigenproblems use nalgebra's `SymmetricEigen`: `special::wigner`
12//!   diagonalizes the tridiagonal generator of the small-d matrix, of size 2l + 1.
13//! - Fixed-size matrices of at most 4 x 4 stay in nalgebra: the 3 x 3 inverse of
14//!   `lattice::reciprocal`, the closed-form 2 x 2 inverse of `coeffs::mie`, the 4 x 4
15//!   and 2 x 2 inverses of `coeffs::mie_cyl` and the 4 x 4 inverse of
16//!   `smatrix::interface`.
17#![allow(clippy::indexing_slicing)] // Validated matrix dimensions and eigenvector pivots.
18
19mod gmres;
20mod saved;
21
22use std::sync::OnceLock;
23
24pub use gmres::{Convergence, GmresOptions};
25pub(crate) use gmres::{gmres, gmres_batch, norm};
26
27use faer::{
28    Accum, Conj, Mat, MatMut, MatRef, Spec,
29    diag::Diag,
30    dyn_stack::{MemBuffer, MemStack, StackReq},
31    linalg::{
32        evd,
33        lu::partial_pivoting::{factor, solve as lu_solve},
34        matmul::matmul,
35        svd,
36    },
37    perm::Perm,
38    traits::Conjugate,
39};
40use nalgebra::DMatrix;
41
42use crate::{
43    Complex, Error, Result,
44    numerics::{self, finite, ratio},
45};
46
47/// Column-major faer view of a nalgebra matrix.
48pub(crate) fn view(matrix: &DMatrix<Complex>) -> MatRef<'_, Complex> {
49    MatRef::from_column_major_slice(matrix.as_slice(), matrix.nrows(), matrix.ncols())
50}
51
52/// Mutable column-major faer view of a nalgebra matrix.
53pub(crate) fn view_mut(matrix: &mut DMatrix<Complex>) -> MatMut<'_, Complex> {
54    let (rows, cols) = matrix.shape();
55    MatMut::from_column_major_slice_mut(matrix.as_mut_slice(), rows, cols)
56}
57
58/// `out = left right` on the treams-rs pool ([`crate::threads::product`]). Pass
59/// `.adjoint()` views for conjugate-transposed factors; faer conjugates them inside
60/// the product kernel.
61pub(crate) fn product_into<L, R>(
62    out: MatMut<'_, Complex>,
63    left: MatRef<'_, L>,
64    right: MatRef<'_, R>,
65) where
66    L: Conjugate<Canonical = Complex>,
67    R: Conjugate<Canonical = Complex>,
68{
69    let (m, n, k) = (out.nrows(), out.ncols(), left.ncols());
70    crate::threads::product(m, n, k, |par| {
71        matmul(
72            out,
73            Accum::Replace,
74            left,
75            right,
76            Complex::new(1.0, 0.0),
77            par,
78        );
79    });
80}
81
82/// `left right` in a new matrix; see [`product_into`].
83pub(crate) fn product_views<L, R>(left: MatRef<'_, L>, right: MatRef<'_, R>) -> DMatrix<Complex>
84where
85    L: Conjugate<Canonical = Complex>,
86    R: Conjugate<Canonical = Complex>,
87{
88    let mut result = DMatrix::zeros(left.nrows(), right.ncols());
89    product_into(view_mut(&mut result), left, right);
90    result
91}
92
93/// `left right`.
94pub(crate) fn product(left: &DMatrix<Complex>, right: &DMatrix<Complex>) -> DMatrix<Complex> {
95    product_views(view(left), view(right))
96}
97
98/// `left rightᴴ`.
99pub(crate) fn product_adjoint_right(
100    left: &DMatrix<Complex>,
101    right: &DMatrix<Complex>,
102) -> DMatrix<Complex> {
103    product_views(view(left), view(right).adjoint())
104}
105
106/// `out = left rightᴴ`, reusing the allocation of `out`.
107pub(crate) fn product_adjoint_right_into(
108    out: &mut DMatrix<Complex>,
109    left: &DMatrix<Complex>,
110    right: &DMatrix<Complex>,
111) {
112    product_into(view_mut(out), view(left), view(right).adjoint());
113}
114
115/// Positive diagonal scales for the equivalent operator `R A C`.
116///
117/// A normal solve scales its right-hand side by `row` and its solution by
118/// `column`. A conjugate-transpose solve uses the opposite order. These scales
119/// are numerical coordinates, not additional inputs to the implicit derivative.
120#[derive(Clone, Debug)]
121pub(crate) struct Equilibration {
122    /// Left (equation) scale.
123    pub(crate) row: Vec<f64>,
124    /// Right (unknown) scale.
125    pub(crate) column: Vec<f64>,
126}
127
128/// Equilibrate severely unbalanced finite square operators in place.
129///
130/// Small-argument, high-order multipole operators can have unit diagonal and
131/// off-diagonal entries exceeding `1e20`. Their unscaled LU loses accuracy even
132/// when the equivalent equilibrated system is well conditioned. The scan that
133/// rejects non-finite entries also measures the spread of entry sizes. An operator
134/// whose nonzero entries, or whose diagonal entries, all lie within a factor of 10 of
135/// the largest entry stays unchanged; others get unit row and column maxima, as in
136/// LAPACK xGEEQU, then [`balance`] evens out their sums.
137pub(crate) fn equilibrate(operator: &mut DMatrix<Complex>) -> Result<Option<Equilibration>> {
138    let n = operator.nrows();
139    if n == 0 || !operator.is_square() {
140        return Err(Error::InvalidInput(
141            "require a finite nonempty square operator".into(),
142        ));
143    }
144    // LAPACK xLAQGE uses 0.1 for row/column scale ratios. Waiting until
145    // sqrt(epsilon) loses observable digits in moderately unbalanced multipole
146    // systems even though a change of coordinates makes them well conditioned.
147    let relative_floor = 0.1;
148    let mut minimum = f64::INFINITY;
149    let mut maximum = 0.0_f64;
150    for &z in operator.iter() {
151        if !finite(z) {
152            return Err(Error::InvalidInput(
153                "require a finite nonempty square operator".into(),
154            ));
155        }
156        let magnitude = z.re.abs().max(z.im.abs());
157        if magnitude > 0.0 {
158            minimum = minimum.min(magnitude);
159        }
160        maximum = maximum.max(magnitude);
161    }
162    if maximum > 0.0
163        && (minimum / maximum >= relative_floor
164            || (0..n).all(|i| {
165                let z = operator[(i, i)];
166                z.re.abs().max(z.im.abs()) / maximum >= relative_floor
167            }))
168    {
169        return Ok(None);
170    }
171    // Only potentially unbalanced operators allocate scratch or need another
172    // pass. Isolated tiny entries do not by themselves require equilibration.
173    let mut row = vec![0.0_f64; n];
174    let mut column_min = f64::INFINITY;
175    for values in operator.as_slice().chunks_exact(n) {
176        let mut column_max = 0.0_f64;
177        for (row_max, &z) in row.iter_mut().zip(values) {
178            let magnitude = z.re.abs().max(z.im.abs());
179            *row_max = row_max.max(magnitude);
180            column_max = column_max.max(magnitude);
181        }
182        column_min = column_min.min(column_max);
183    }
184    let row_min = row.iter().copied().fold(f64::INFINITY, f64::min);
185    if row_min == 0.0 || column_min == 0.0 {
186        return Err(Error::Singular);
187    }
188    if row_min / maximum >= relative_floor && column_min / maximum >= relative_floor {
189        return Ok(None);
190    }
191    if column_min < row_min {
192        // Scale the more disparate axis first. Otherwise normalizing large rows
193        // can underflow an entire small column before it gets its own scale.
194        // Transposition swaps the two measured spreads, so this recurses once.
195        operator.transpose_mut();
196        let result = equilibrate(operator);
197        operator.transpose_mut();
198        return result.map(|scales| {
199            scales.map(|scales| Equilibration {
200                row: scales.column,
201                column: scales.row,
202            })
203        });
204    }
205    let inverse = |value: f64| {
206        value
207            .clamp(f64::MIN_POSITIVE, f64::MIN_POSITIVE.recip())
208            .recip()
209    };
210    for value in &mut row {
211        *value = inverse(*value);
212    }
213    let mut column = Vec::with_capacity(n);
214    for values in operator.as_mut_slice().chunks_exact_mut(n) {
215        let mut column_max = 0.0_f64;
216        for (value, &scale) in values.iter_mut().zip(&row) {
217            *value *= scale;
218            column_max = column_max.max(value.re.abs().max(value.im.abs()));
219        }
220        let scale = inverse(column_max);
221        column.push(scale);
222        for value in values {
223            *value *= scale;
224        }
225    }
226    balance(operator, &mut row, &mut column)?;
227    Ok(Some(Equilibration { row, column }))
228}
229
230/// The most sweeps of [`balance`]; dense operators take at most three.
231const BALANCE_SWEEPS: usize = 64;
232
233/// Even out the row and column sums of a max-equilibrated operator in place with
234/// power-of-two factors, folded into `row` and `column`.
235///
236/// Row and column maxima do not determine the scales: in `A = D_r H D_c` the largest
237/// entry of a row depends on `D_c`, so an off-diagonal entry of `H` can end up as
238/// large as its diagonal, partial pivoting may choose it, and a change of units costs
239/// digits. Unit magnitude sums determine the scaled operator whenever it has total
240/// support (Sinkhorn and Knopp), whatever the units.
241///
242/// Each sweep moves every column factor, then every row factor, to a power of two that
243/// minimizes the convex potential `Σ |a_ij| x_i y_j − Σ ln x_i − Σ ln y_j` along it. The
244/// sweeps stop after one that moves no factor by more than two, or after
245/// [`BALANCE_SWEEPS`]. A sum that is not normal, or factors beyond `2^±256`, keep the
246/// max scales.
247fn balance(operator: &mut DMatrix<Complex>, row: &mut [f64], column: &mut [f64]) -> Result<()> {
248    let n = operator.nrows();
249    let magnitude = |z: &Complex| z.re.abs().max(z.im.abs());
250    // Scale `factor` by the power of two that puts its `sum` in `[ln 2, 2 ln 2)`, which
251    // minimizes `sum 2^τ − τ ln 2`; `Some(true)` reports a step beyond a factor of two.
252    let advance = |factor: &mut f64, sum: f64| {
253        let ratio = sum / std::f64::consts::LN_2;
254        // The exponent bits alone: the largest power of two at most `ratio`.
255        let step = f64::from_bits(ratio.to_bits() & 0x7ff0_0000_0000_0000).recip();
256        *factor *= step;
257        ratio.is_normal().then_some(!(0.5..=2.0).contains(&step))
258    };
259    let mut left = numerics::filled(n, 1.0_f64)?;
260    let mut right = numerics::filled(n, 1.0_f64)?;
261    // Row sums without their left factor.
262    let mut partial = numerics::filled(n, 0.0_f64)?;
263    for _ in 0..BALANCE_SWEEPS {
264        let mut coarse = false;
265        partial.fill(0.0);
266        for (values, factor) in operator.as_slice().chunks_exact(n).zip(&mut right) {
267            let sum: f64 = values
268                .iter()
269                .zip(&left)
270                .map(|(z, &x)| x * magnitude(z))
271                .sum();
272            let Some(large) = advance(factor, sum * *factor) else {
273                return Ok(());
274            };
275            coarse |= large;
276            for (total, z) in partial.iter_mut().zip(values) {
277                *total += magnitude(z) * *factor;
278            }
279        }
280        for (factor, &total) in left.iter_mut().zip(&partial) {
281            let Some(large) = advance(factor, total * *factor) else {
282                return Ok(());
283            };
284            coarse |= large;
285        }
286        if !coarse {
287            break;
288        }
289    }
290    let bound = 2.0_f64.powi(256);
291    let fits = |scales: &[f64], factors: &[f64]| {
292        scales.iter().zip(factors).all(|(&scale, &factor)| {
293            (bound.recip()..=bound).contains(&factor) && (scale * factor).is_normal()
294        })
295    };
296    if fits(row, &left) && fits(column, &right) {
297        for (values, &y) in operator.as_mut_slice().chunks_exact_mut(n).zip(&right) {
298            // Both factors lie within 2^±256, so their product scales exactly.
299            for (value, &x) in values.iter_mut().zip(&left) {
300                *value *= x * y;
301            }
302        }
303        for (scales, factors) in [(row, &left), (column, &right)] {
304            for (scale, &factor) in scales.iter_mut().zip(factors) {
305                *scale *= factor;
306            }
307        }
308    }
309    Ok(())
310}
311
312/// `matrix[(i, j)] scales[i] 2^shifts[j]`, rounded once unless subnormal. A shifted
313/// column splits each scale into a mantissa and a power of two, and applies the power
314/// first when it scales up, so no intermediate product overflows or flushes a
315/// subnormal entry.
316fn scale_rows(mut matrix: MatMut<'_, Complex>, scales: &[f64], shifts: &[i32]) {
317    for (j, &shift) in shifts.iter().enumerate() {
318        for (value, &scale) in matrix.as_mut().col_mut(j).iter_mut().zip(scales) {
319            if shift == 0 {
320                *value *= scale;
321            } else {
322                let (mantissa, exponent) = libm::frexp(scale);
323                let power = exponent + shift;
324                let part = |x: f64| {
325                    if power > 0 {
326                        libm::scalbn(x, power - 1) * (2.0 * mantissa)
327                    } else {
328                        libm::scalbn(x * mantissa, power)
329                    }
330                };
331                *value = Complex::new(part(value.re), part(value.im));
332            }
333        }
334    }
335}
336
337/// The faer workers of an LU factorization or solve with `rows` rows and `columns`
338/// right-hand sides, on a pool of `budget` threads.
339///
340/// Solves with fewer than 32 right-hand sides run on one worker. Recursive LU and
341/// triangular solves split the work into panels, one task each. A pool of up to four
342/// threads runs whole: the panels stay wide enough for four workers. Larger pools get one worker per 512 rows up to four, one per 2048 rows
343/// beyond that, and at most one per 16 right-hand sides, because narrower panels cost
344/// more in task overhead than they gain. The constants are tuned up to 8192 rows;
345/// `benches/lu_scheduling.rs` times a factorization and its solves for any worker
346/// count, to check them on other hardware.
347fn lu_threads(rows: usize, columns: usize, budget: usize) -> usize {
348    // Narrow solves: faer's parallel triangular solves and products split the inner
349    // dimension by worker count, which changes the last bits of the solution, and
350    // their O(rows²) work per column gains little from workers.
351    if columns < 32 {
352        return 1;
353    }
354    if budget <= 4 {
355        return budget;
356    }
357    (rows / 512)
358        .min(4)
359        .max(rows / 2048)
360        .min(columns / 16)
361        .clamp(1, budget)
362}
363
364/// The faer workers of an LU call on the treams-rs budget.
365fn lu_workers(rows: usize, columns: usize) -> usize {
366    // Faer takes no parallel branch here (LU and row swaps below 64 rows, products
367    // below its M·N·K threshold, at most 64 right-hand sides in the triangular
368    // solves), so the result is the same without the hand-off to the pool.
369    if rows < 64 && columns <= 64 {
370        return 1;
371    }
372    lu_threads(rows, columns, crate::threads::current_num_threads())
373}
374
375/// A faer workspace of `req`, or [`Error::OutOfMemory`] when the system refuses it or
376/// the requirement overflowed.
377fn scratch(req: StackReq) -> Result<MemBuffer> {
378    MemBuffer::try_new(req).map_err(|_| {
379        // dyn-stack marks an overflowed requirement with alignment 0 and size 0; it
380        // asked for more than usize::MAX bytes.
381        if req.align_bytes() == 0 {
382            Error::out_of_memory(usize::MAX, 2)
383        } else {
384            Error::out_of_memory(req.size_bytes(), 1)
385        }
386    })
387}
388
389/// Packed pivoted LU: overwrite the operator and share its triangular storage.
390#[derive(Clone, Debug)]
391pub(crate) struct Lu {
392    factors: DMatrix<Complex>,
393    permutation: Perm<usize>,
394    equilibration: Option<Equilibration>,
395}
396
397impl Lu {
398    pub(crate) fn new(mut factors: DMatrix<Complex>) -> Result<Self> {
399        let n = factors.nrows();
400        let equilibration = equilibrate(&mut factors)?;
401        let mut forward = numerics::filled(n, 0_usize)?;
402        let mut inverse = numerics::filled(n, 0_usize)?;
403        crate::threads::dense(lu_workers(n, n), |par| -> Result<()> {
404            factor::lu_in_place(
405                view_mut(&mut factors),
406                &mut forward,
407                &mut inverse,
408                par,
409                MemStack::new(&mut scratch(
410                    factor::lu_in_place_scratch::<usize, Complex>(n, n, par, Spec::default()),
411                )?),
412                Spec::default(),
413            );
414            Ok(())
415        })?;
416        if factors.diagonal().iter().any(|&z| z == Complex::default()) {
417            return Err(Error::Singular);
418        }
419        Ok(Self {
420            factors,
421            equilibration,
422            permutation: Perm::new_checked(
423                forward.into_boxed_slice(),
424                inverse.into_boxed_slice(),
425                n,
426            ),
427        })
428    }
429
430    /// Solve `A X = B` in place, with `A` the operator given to [`Lu::new`].
431    pub(crate) fn solve_in_place(&self, rhs: MatMut<'_, Complex>) -> Result<()> {
432        self.solve_with(rhs, false)
433    }
434
435    /// Solve `Aᴴ X = B` in place.
436    pub(crate) fn solve_adjoint_in_place(&self, rhs: MatMut<'_, Complex>) -> Result<()> {
437        self.solve_with(rhs, true)
438    }
439
440    // The factors hold `R A C`. A forward solve scales by the row scales, solves
441    // and scales by the column scales; an adjoint solve uses the reverse order.
442    fn solve_with(&self, mut rhs: MatMut<'_, Complex>, adjoint: bool) -> Result<()> {
443        let (n, columns) = (self.factors.nrows(), rhs.ncols());
444        let scales = self.equilibration.as_ref().map(|scales| {
445            if adjoint {
446                (&scales.column, &scales.row)
447            } else {
448                (&scales.row, &scales.column)
449            }
450        });
451        // Shift a right-hand side by a power of two when a scaled entry could exceed 2^960,
452        // which leaves 2^64 for growth in the triangular solves, or fall below 2^-1020: the
453        // scales then neither overflow a system whose solution is near the largest float
454        // nor flush its small entries. The shift nearest zero keeps the entries normal and
455        // below 2^960; for too wide a range, nonzero and finite; failing that, there is
456        // none. Others keep their bits.
457        let mut shifts = Vec::new();
458        if let Some((before, _)) = scales {
459            shifts = numerics::filled(columns, 0_i32)?;
460            for (j, shift) in shifts.iter_mut().enumerate() {
461                // Real and imaginary parts count apart: a large one must not hide a small one.
462                let entries = || {
463                    (rhs.as_ref().col(j).iter().zip(before))
464                        .flat_map(|(z, &scale)| [(z.re.abs(), scale), (z.im.abs(), scale)])
465                        .filter(|&(magnitude, _)| magnitude > 0.0)
466                };
467                // The extreme scaled parts, zero or infinite where they leave the range.
468                let (smallest, largest) = entries()
469                    .map(|(magnitude, scale)| magnitude * scale)
470                    .fold((f64::INFINITY, 0.0_f64), |(low, high), x| {
471                        (low.min(x), high.max(x))
472                    });
473                if largest > 2.0_f64.powi(960) || smallest < 2.0_f64.powi(-1020) {
474                    // Scaled parts lie in [2^(e - 2), 2^e) for the exponent sums e.
475                    let (low, high) = entries()
476                        .map(|(magnitude, scale)| libm::frexp(magnitude).1 + libm::frexp(scale).1)
477                        .fold((i32::MAX, i32::MIN), |(low, high), e| {
478                            (low.min(e), high.max(e))
479                        });
480                    let (floor, ceiling) = (-1020 - low, 960 - high);
481                    let (lowest, highest) = (-1072 - low, 1023 - high);
482                    *shift = if floor <= ceiling {
483                        0.clamp(floor, ceiling)
484                    } else if lowest <= highest {
485                        0.clamp(lowest, highest)
486                    } else {
487                        0
488                    };
489                }
490            }
491            scale_rows(rhs.as_mut(), before, &shifts);
492        }
493        let (factors, permutation) = (view(&self.factors), self.permutation.as_ref());
494        crate::threads::dense(lu_workers(n, columns), |par| -> Result<()> {
495            if adjoint {
496                lu_solve::solve_transpose_in_place_with_conj(
497                    factors,
498                    factors,
499                    permutation,
500                    Conj::Yes,
501                    rhs.as_mut(),
502                    par,
503                    MemStack::new(&mut scratch(lu_solve::solve_transpose_in_place_scratch::<
504                        usize,
505                        Complex,
506                    >(n, columns, par))?),
507                );
508            } else {
509                lu_solve::solve_in_place(
510                    factors,
511                    factors,
512                    permutation,
513                    rhs.as_mut(),
514                    par,
515                    MemStack::new(&mut scratch(lu_solve::solve_in_place_scratch::<
516                        usize,
517                        Complex,
518                    >(n, columns, par))?),
519                );
520            }
521            Ok(())
522        })?;
523        if let Some((_, after)) = scales {
524            for shift in &mut shifts {
525                *shift = -*shift;
526            }
527            scale_rows(rhs, after, &shifts);
528        }
529        Ok(())
530    }
531}
532
533/// What [`solve`] saves for its derivatives: the LU factors and the solution. Both
534/// derivative directions solve with the same factors.
535#[derive(Clone, Debug)]
536pub struct SolveResidual {
537    lu: Lu,
538    value: DMatrix<Complex>,
539}
540
541/// Solve A X = B with pivoted LU; B is a matrix of right-hand sides.
542pub fn solve(operator: &DMatrix<Complex>, rhs: DMatrix<Complex>) -> Result<SolveResidual> {
543    solve_owned(operator.clone(), rhs)
544}
545
546/// Solve while reusing the owned operator buffer for its LU factors.
547pub fn solve_owned(operator: DMatrix<Complex>, mut rhs: DMatrix<Complex>) -> Result<SolveResidual> {
548    let n = operator.nrows();
549    if n == 0
550        || !operator.is_square()
551        || rhs.nrows() != n
552        || rhs.ncols() == 0
553        || rhs.iter().any(|&z| !finite(z))
554    {
555        return Err(Error::InvalidInput(
556            "require a finite nonempty square operator and matching right-hand sides".into(),
557        ));
558    }
559    let lu = Lu::new(operator)?;
560    lu.solve_in_place(view_mut(&mut rhs))?;
561    if rhs.iter().any(|&z| !finite(z)) {
562        return Err(Error::Singular);
563    }
564    Ok(SolveResidual { lu, value: rhs })
565}
566
567/// Eigen- and singular-value decompositions run on the calling thread. faer's
568/// parallel Hessenberg and bidiagonal reductions split inner products by worker
569/// count, so their last bits, and the order of eigenvalues, would follow the
570/// budget. On four workers they gained at most 1.3× (eigenvalues) and 1.7×
571/// (singular values) at n = 768, and nothing below n = 384.
572const DECOMPOSITION: faer::Par = faer::Par::Seq;
573
574/// Thin singular vectors and values. faer's `Svd` reads faer's global parallelism,
575/// so the decomposition is called with [`DECOMPOSITION`] instead.
576#[derive(Debug)]
577struct ThinSvd {
578    u: Mat<Complex>,
579    s: Diag<Complex>,
580    v: Mat<Complex>,
581}
582
583/// Reserve decomposition vectors before initializing their entries.
584fn faer_zeros(rows: usize, columns: usize) -> Result<Mat<Complex>> {
585    let mut matrix = Mat::new();
586    matrix
587        .try_reserve(rows, columns)
588        .map_err(|_| Error::out_of_memory(rows.saturating_mul(columns), size_of::<Complex>()))?;
589    matrix.resize_with(rows, columns, |_, _| Complex::default());
590    Ok(matrix)
591}
592
593fn thin_svd(a: MatRef<'_, Complex>) -> Result<ThinSvd> {
594    let (m, n) = a.shape();
595    let size = m.min(n);
596    let mut factors = ThinSvd {
597        u: faer_zeros(m, size)?,
598        s: Diag::zeros(size),
599        v: faer_zeros(n, size)?,
600    };
601    let thin = svd::ComputeSvdVectors::Thin;
602    let par = DECOMPOSITION;
603    svd::svd(
604        a,
605        factors.s.as_mut(),
606        Some(factors.u.as_mut()),
607        Some(factors.v.as_mut()),
608        par,
609        MemStack::new(&mut scratch(svd::svd_scratch::<Complex>(
610            m,
611            n,
612            thin,
613            thin,
614            par,
615            Spec::default(),
616        ))?),
617        Spec::default(),
618    )
619    .map_err(|err| Error::NotConverged(format!("singular-value decomposition: {err:?}")))?;
620    Ok(factors)
621}
622
623/// Eigenvalues and right eigenvectors, called with an explicit `Par` as [`thin_svd`].
624fn eigen(a: MatRef<'_, Complex>) -> Result<(Diag<Complex>, Mat<Complex>)> {
625    let n = a.nrows();
626    let mut values = Diag::zeros(n);
627    let mut vectors = faer_zeros(n, n)?;
628    let par = DECOMPOSITION;
629    evd::evd_cplx(
630        a,
631        values.as_mut(),
632        None,
633        Some(vectors.as_mut()),
634        par,
635        MemStack::new(&mut scratch(evd::evd_scratch::<Complex>(
636            n,
637            evd::ComputeEigenvectors::No,
638            evd::ComputeEigenvectors::Yes,
639            par,
640            Spec::default(),
641        ))?),
642        Spec::default(),
643    )
644    .map_err(|err| Error::NotConverged(format!("eigendecomposition: {err:?}")))?;
645    Ok((values, vectors))
646}
647
648/// What [`svdvals`] saves for its derivatives: the thin singular vectors and the singular
649/// values.
650#[derive(Debug)]
651pub struct SvdvalsResidual {
652    u: Mat<Complex>,
653    v: Mat<Complex>,
654    values: Vec<f64>,
655}
656
657/// Singular values of a finite nonempty rectangular complex matrix.
658pub fn svdvals(operator: &DMatrix<Complex>) -> Result<SvdvalsResidual> {
659    if operator.is_empty() || operator.iter().any(|&z| !finite(z)) {
660        return Err(Error::InvalidInput(
661            "SVD requires a finite nonempty matrix".into(),
662        ));
663    }
664    let scale = operator.iter().map(|z| z.norm()).fold(0.0, f64::max);
665    let scale = if scale == 0.0 { 1.0 } else { scale };
666    let decomposition = thin_svd(view(&operator.map(|z| z / scale)))?;
667    let values: Vec<_> = (0..operator.nrows().min(operator.ncols()))
668        .map(|i| decomposition.s.as_ref()[i].re * scale)
669        .collect();
670    if values.iter().any(|x| !x.is_finite()) {
671        return Err(Error::NonFinite("non-finite singular values".into()));
672    }
673    Ok(SvdvalsResidual {
674        u: decomposition.u,
675        v: decomposition.v,
676        values,
677    })
678}
679
680impl SvdvalsResidual {
681    /// The singular values, in descending order, which the pullback reads.
682    #[must_use]
683    pub fn values(&self) -> &[f64] {
684        &self.values
685    }
686
687    /// The shape of the input operator.
688    #[must_use]
689    pub fn shape(&self) -> (usize, usize) {
690        (self.u.nrows(), self.v.nrows())
691    }
692
693    /// Singular-value tangents `Re(uᴴ dA v)` for simple positive singular values.
694    ///
695    /// The ordered individual singular values have no linear derivative at repeated
696    /// or zero values. Unlike the pullback of a smooth spectral sum, a pushforward
697    /// returns each individual derivative and therefore rejects these cases.
698    /// Values within `64 ε` of the largest value's scale are numerically unresolved
699    /// and are treated as zero, including roundoff from rank-deficient matrices.
700    pub fn pushforward(&self, operator: &DMatrix<Complex>) -> Result<Vec<f64>> {
701        if operator.shape() != self.shape() || operator.iter().any(|&z| !finite(z)) {
702            return Err(Error::InvalidInput(
703                "invalid singular-value operator tangent".into(),
704            ));
705        }
706        let tolerance = 64.0 * f64::EPSILON * self.values[0];
707        if self.values.iter().any(|&value| value <= tolerance) {
708            return Err(Error::InvalidInput(
709                "singular values at zero or below numerical resolution have no pushforward".into(),
710            ));
711        }
712        if self
713            .values
714            .windows(2)
715            .any(|pair| pair[0] - pair[1] <= tolerance)
716        {
717            return Err(Error::InvalidInput(
718                "individual repeated singular values have no pushforward".into(),
719            ));
720        }
721        let moved = product_views(view(operator), self.v.as_ref());
722        let u = self.u.as_ref();
723        Ok((0..self.values.len())
724            .map(|j| {
725                (0..u.nrows())
726                    .map(|i| (u[(i, j)].conj() * moved[(i, j)]).re)
727                    .sum()
728            })
729            .collect())
730    }
731
732    /// Equal weights are supported at repeated positive values; numerically zero
733    /// values require numerically zero weights because the singular value itself
734    /// is not smooth there. Both tests use relative tolerance `64 ε`.
735    pub fn pullback(&self, cotangent: &[f64]) -> Result<DMatrix<Complex>> {
736        if cotangent.len() != self.values.len() || cotangent.iter().any(|x| !x.is_finite()) {
737            return Err(Error::Derivative(
738                crate::DerivativeError::InvalidSingularValueCotangent,
739            ));
740        }
741        let tolerance = 64.0 * f64::EPSILON * self.values[0];
742        let weight_tolerance =
743            64.0 * f64::EPSILON * cotangent.iter().map(|x| x.abs()).fold(0.0, f64::max);
744        for i in 0..cotangent.len() {
745            if self.values[i] <= tolerance && cotangent[i].abs() > weight_tolerance {
746                return Err(Error::Derivative(
747                    crate::DerivativeError::UnresolvedSingularValue,
748                ));
749            }
750            if i > 0
751                && self.values[i - 1] > tolerance
752                && (self.values[i - 1] - self.values[i]).abs() <= tolerance
753                && (cotangent[i - 1] - cotangent[i]).abs()
754                    > 64.0 * f64::EPSILON * cotangent[i - 1].abs().max(cotangent[i].abs())
755            {
756                return Err(Error::Derivative(
757                    crate::DerivativeError::UnequalSingularValueWeights,
758                ));
759            }
760        }
761        let u = self.u.as_ref();
762        let weighted =
763            DMatrix::from_fn(u.nrows(), cotangent.len(), |i, j| u[(i, j)] * cotangent[j]);
764        Ok(product_views(view(&weighted), self.v.as_ref().adjoint()))
765    }
766}
767
768/// Cotangents of the inputs of [`solve`] and [`solve_owned`], in the order of their
769/// arguments.
770#[derive(Clone, Debug)]
771pub struct SolveGradient {
772    /// Cotangent of the operator `A`.
773    pub operator: DMatrix<Complex>,
774    /// Cotangent of the right-hand sides `B`.
775    pub rhs: DMatrix<Complex>,
776}
777
778impl SolveResidual {
779    /// The solution `X` of `A X = B`, one column per right-hand side, which the pullback
780    /// reads.
781    #[must_use]
782    pub const fn value(&self) -> &DMatrix<Complex> {
783        &self.value
784    }
785
786    /// The shape of the solution: unknowns and right-hand sides.
787    #[must_use]
788    pub fn shape(&self) -> (usize, usize) {
789        self.value.shape()
790    }
791
792    /// Solve `A dX = dB - dA X`, reusing the forward LU and the tangent RHS buffer.
793    pub fn pushforward(
794        &self,
795        operator: &DMatrix<Complex>,
796        mut rhs: DMatrix<Complex>,
797    ) -> Result<DMatrix<Complex>> {
798        let n = self.value.nrows();
799        if operator.shape() != (n, n)
800            || rhs.shape() != self.shape()
801            || operator.iter().chain(rhs.iter()).any(|&z| !finite(z))
802        {
803            return Err(Error::InvalidInput("invalid linear-solve tangent".into()));
804        }
805        rhs -= product(operator, &self.value);
806        self.lu.solve_in_place(view_mut(&mut rhs))?;
807        if rhs.iter().any(|&z| !finite(z)) {
808            return Err(Error::Singular);
809        }
810        Ok(rhs)
811    }
812
813    pub(crate) fn adjoint_rhs(&self, mut cotangent: DMatrix<Complex>) -> Result<DMatrix<Complex>> {
814        if cotangent.shape() != self.shape() || cotangent.iter().any(|&z| !finite(z)) {
815            return Err(Error::InvalidInput("invalid linear-solve cotangent".into()));
816        }
817        self.lu.solve_adjoint_in_place(view_mut(&mut cotangent))?;
818        Ok(cotangent)
819    }
820
821    /// Return operator and right-hand-side cotangents, reusing the forward LU.
822    pub fn pullback(&self, cotangent: DMatrix<Complex>) -> Result<SolveGradient> {
823        let rhs = self.adjoint_rhs(cotangent)?;
824        Ok(SolveGradient {
825            operator: -product_adjoint_right(&rhs, &self.value),
826            rhs,
827        })
828    }
829}
830
831/// What [`eig`] saves for its derivatives: the right eigensystem, with unit vectors whose
832/// largest component is real and positive.
833#[derive(Debug)]
834pub struct EigResidual {
835    values: Vec<Complex>,
836    vectors: DMatrix<Complex>,
837    pivots: Vec<usize>,
838    scale: f64,
839    /// Factor the eigenvectors only when a derivative needs their inverse;
840    /// every later direction and cotangent shares the same factors.
841    vectors_lu: OnceLock<Result<Lu>>,
842}
843
844/// General complex eigendecomposition. Repeated eigenvalues are allowed in the primal.
845pub fn eig(operator: &DMatrix<Complex>) -> Result<EigResidual> {
846    let n = operator.nrows();
847    if n == 0 || !operator.is_square() || operator.iter().any(|&z| !finite(z)) {
848        return Err(Error::InvalidInput(
849            "eigensystem requires a finite nonempty square matrix".into(),
850        ));
851    }
852    let scale = operator.iter().map(|z| z.norm()).fold(0.0, f64::max);
853    let scale = if scale == 0.0 { 1.0 } else { scale };
854    let (eigenvalues, eigenvectors) = eigen(view(&operator.map(|z| z / scale)))?;
855    let values: Vec<_> = (0..n).map(|i| eigenvalues.as_ref()[i] * scale).collect();
856    let mut vectors = DMatrix::from_fn(n, n, |i, j| eigenvectors[(i, j)]);
857    let mut pivots = Vec::with_capacity(n);
858    for mut column in vectors.column_iter_mut() {
859        let pivot = (0..n)
860            .max_by(|&i, &j| column[i].norm_sqr().total_cmp(&column[j].norm_sqr()))
861            .ok_or(Error::Singular)?;
862        let phase = column[pivot] / column[pivot].norm();
863        let normalization = phase * column.norm();
864        column /= normalization;
865        pivots.push(pivot);
866    }
867    if values.iter().chain(vectors.iter()).any(|&z| !finite(z)) {
868        return Err(Error::NonFinite("non-finite eigensystem".into()));
869    }
870    Ok(EigResidual {
871        values,
872        vectors,
873        pivots,
874        scale,
875        vectors_lu: OnceLock::new(),
876    })
877}
878
879/// Eigenvalues ordered by real part, then imaginary part. The same eigensystem
880/// residual supplies derivatives without imposing an eigenvector phase constraint.
881pub fn eigvals(operator: &DMatrix<Complex>) -> Result<EigResidual> {
882    let mut residual = eig(operator)?;
883    let n = residual.values.len();
884    let mut order: Vec<_> = (0..n).collect();
885    order.sort_by(|&i, &j| {
886        residual.values[i]
887            .re
888            .total_cmp(&residual.values[j].re)
889            .then_with(|| residual.values[i].im.total_cmp(&residual.values[j].im))
890    });
891    residual.values = order.iter().map(|&j| residual.values[j]).collect();
892    residual.vectors = DMatrix::from_fn(n, n, |i, j| residual.vectors[(i, order[j])]);
893    residual.pivots = order.iter().map(|&j| residual.pivots[j]).collect();
894    Ok(residual)
895}
896
897impl EigResidual {
898    fn vectors_lu(&self) -> Result<&Lu> {
899        self.vectors_lu
900            .get_or_init(|| Lu::new(self.vectors.clone()))
901            .as_ref()
902            .map_err(Clone::clone)
903    }
904
905    /// The eigenvalues in the solver's order, which the pullback reads.
906    #[must_use]
907    pub fn values(&self) -> &[Complex] {
908        &self.values
909    }
910
911    /// The right eigenvectors, one column per eigenvalue, which the pullback reads.
912    #[must_use]
913    pub const fn vectors(&self) -> &DMatrix<Complex> {
914        &self.vectors
915    }
916
917    /// Whether the phase pivot is tied with another largest component.
918    fn tied_pivot(&self, column: usize) -> bool {
919        let pivot = self.pivots[column];
920        let size = self.vectors[(pivot, column)].norm();
921        (0..self.values.len()).any(|i| {
922            i != pivot
923                && (size - self.vectors[(i, column)].norm()).abs() <= 64.0 * f64::EPSILON * size
924        })
925    }
926
927    /// Express the perturbation in the eigenbasis; its diagonal is the
928    /// eigenvalue derivative and its off-diagonal entries determine eigenvectors.
929    fn eigenbasis_tangent(&self, operator: &DMatrix<Complex>) -> Result<DMatrix<Complex>> {
930        let n = self.values.len();
931        if operator.shape() != (n, n) || operator.iter().any(|&z| !finite(z)) {
932            return Err(Error::InvalidInput("invalid eigensystem tangent".into()));
933        }
934        for j in 0..n {
935            for i in 0..j {
936                if (self.values[j] - self.values[i]).norm() <= 64.0 * f64::EPSILON * self.scale {
937                    return Err(Error::InvalidInput(
938                        "individual eigenmodes have no pushforward at repeated eigenvalues".into(),
939                    ));
940                }
941            }
942        }
943        let mut moved = product(operator, &self.vectors);
944        self.vectors_lu()?.solve_in_place(view_mut(&mut moved))?;
945        if moved.iter().any(|&z| !finite(z)) {
946            return Err(Error::Singular);
947        }
948        Ok(moved)
949    }
950
951    /// An ordered spectrum switches modes when real parts cross. Only equal
952    /// loss weights across a tied group make that change irrelevant.
953    fn check_value_order(&self, weights: Option<&[Complex]>) -> Result<()> {
954        for j in 0..self.values.len() {
955            for i in 0..j {
956                let tied = (self.values[j].re - self.values[i].re).abs()
957                    <= 64.0 * f64::EPSILON * self.scale;
958                let equal_weights = weights.is_some_and(|g| {
959                    (g[j] - g[i]).norm() <= 64.0 * f64::EPSILON * g[j].norm().max(g[i].norm())
960                });
961                if tied && !equal_weights {
962                    return Err(Error::InvalidInput(
963                        "ordered eigenvalue derivatives require distinct real parts or equal loss weights at ordering ties".into(),
964                    ));
965                }
966            }
967        }
968        Ok(())
969    }
970
971    /// Eigenvalue tangents with no eigenvector phase constraint. Distinct real
972    /// parts keep [`eigvals`]' ordering fixed; tied phase pivots are supported.
973    pub fn pushforward_values(&self, operator: &DMatrix<Complex>) -> Result<Vec<Complex>> {
974        let moved = self.eigenbasis_tangent(operator)?;
975        self.check_value_order(None)?;
976        Ok(moved.diagonal().iter().copied().collect())
977    }
978
979    /// Pullback of [`eigvals`], allowing equal weights at ordering ties.
980    pub fn pullback_values(&self, values: &[Complex]) -> Result<DMatrix<Complex>> {
981        let n = self.values.len();
982        let gradient = self.pullback(values, DMatrix::zeros(n, n))?;
983        self.check_value_order(Some(values))?;
984        Ok(gradient)
985    }
986
987    /// Eigenpair tangents with the same unit norm and real-positive pivot phase as
988    /// the primal. Repeated eigenvalues and tied phase pivots have no pushforward of
989    /// the complete eigenpair output. Use [`Self::pushforward_values`] when only
990    /// eigenvalues are needed.
991    pub fn pushforward(
992        &self,
993        operator: &DMatrix<Complex>,
994    ) -> Result<(Vec<Complex>, DMatrix<Complex>)> {
995        let n = self.values.len();
996        let mut moved = self.eigenbasis_tangent(operator)?;
997        if (0..n).any(|j| self.tied_pivot(j)) {
998            return Err(Error::InvalidInput(
999                "eigenvector pushforward requires a unique largest component; use eigvals for eigenvalues alone".into(),
1000            ));
1001        }
1002        let values: Vec<_> = moved.diagonal().iter().copied().collect();
1003        // E = V⁻¹ dA V; divide off-diagonal entries by λ_j - λ_i to get
1004        // the unnormalized eigenvector changes. Its diagonal already gave dλ.
1005        for j in 0..n {
1006            for i in 0..n {
1007                moved[(i, j)] = if i == j {
1008                    Complex::default()
1009                } else {
1010                    ratio(moved[(i, j)], self.values[j] - self.values[i])
1011                };
1012            }
1013        }
1014        let mut vectors = product(&self.vectors, &moved);
1015        for j in 0..n {
1016            let pivot = self.pivots[j];
1017            let normalization = self.vectors.column(j).dotc(&vectors.column(j)).re;
1018            let phase = vectors[(pivot, j)].im / self.vectors[(pivot, j)].re;
1019            let correction = Complex::new(normalization, phase);
1020            for i in 0..n {
1021                vectors[(i, j)] -= self.vectors[(i, j)] * correction;
1022            }
1023        }
1024        if values.iter().chain(vectors.iter()).any(|&z| !finite(z)) {
1025            return Err(Error::Singular);
1026        }
1027        Ok((values, vectors))
1028    }
1029
1030    /// The operator gradient from the eigenvalue and eigenvector cotangents, with the
1031    /// phase of each vector fixed at its pivot.
1032    ///
1033    /// Repeated eigenvalues support equal eigenvalue weights and zero vector
1034    /// cotangents within each repeated group. Individual modes there have no pullback.
1035    pub fn pullback(
1036        &self,
1037        values: &[Complex],
1038        mut vectors: DMatrix<Complex>,
1039    ) -> Result<DMatrix<Complex>> {
1040        let n = self.values.len();
1041        if values.len() != n
1042            || vectors.shape() != (n, n)
1043            || values.iter().chain(vectors.iter()).any(|&z| !finite(z))
1044        {
1045            return Err(Error::InvalidInput("invalid eigensystem cotangent".into()));
1046        }
1047        let vector_active: Vec<_> = vectors
1048            .column_iter()
1049            .map(|v| v.iter().any(|&z| z != Complex::default()))
1050            .collect();
1051        if !vector_active.iter().any(|&active| active) && values.iter().all(|&g| g == values[0]) {
1052            // Sum of all eigenvalues is trace(A), including defective matrices.
1053            return Ok(DMatrix::identity(n, n) * values[0]);
1054        }
1055        // Project out normalization and pivot-phase changes before pairing the
1056        // cotangent with the off-diagonal eigenvector derivative.
1057        for j in 0..n {
1058            let inner = vectors.column(j).dotc(&self.vectors.column(j));
1059            let pivot = self.pivots[j];
1060            if inner.im.abs() > 64.0 * f64::EPSILON * vectors.column(j).norm() && self.tied_pivot(j)
1061            {
1062                return Err(Error::InvalidInput(
1063                    "phase-dependent eigenvector pullback requires a unique largest component"
1064                        .into(),
1065                ));
1066            }
1067            for i in 0..n {
1068                vectors[(i, j)] -= inner.re * self.vectors[(i, j)];
1069            }
1070            vectors[(pivot, j)] += Complex::i() * inner.im / self.vectors[(pivot, j)].re;
1071        }
1072        let mut g = product_views(view(&self.vectors).adjoint(), view(&vectors));
1073        for j in 0..n {
1074            for i in 0..n {
1075                if i == j {
1076                    g[(i, j)] = values[i];
1077                    continue;
1078                }
1079                let gap = self.values[j] - self.values[i];
1080                if gap.norm() <= 64.0 * f64::EPSILON * self.scale {
1081                    let weight_scale = values[i].norm().max(values[j].norm());
1082                    if vector_active[i]
1083                        || vector_active[j]
1084                        || (values[i] - values[j]).norm() > 64.0 * f64::EPSILON * weight_scale
1085                    {
1086                        return Err(Error::InvalidInput(
1087                            "individual eigenmodes have no pullback at repeated eigenvalues".into(),
1088                        ));
1089                    }
1090                    g[(i, j)] = Complex::default();
1091                } else {
1092                    g[(i, j)] = ratio(g[(i, j)], gap.conj());
1093                }
1094            }
1095        }
1096        let mut result = product_adjoint_right(&g, &self.vectors);
1097        self.vectors_lu()?
1098            .solve_adjoint_in_place(view_mut(&mut result))?;
1099        if result.iter().any(|&z| !finite(z)) {
1100            return Err(Error::Singular);
1101        }
1102        Ok(result)
1103    }
1104}
1105
1106/// LU scheduling and workspaces, equilibration and the solve pullback.
1107#[cfg(test)]
1108mod tests {
1109    use faer::dyn_stack::StackReq;
1110    use nalgebra::DMatrix;
1111    use proptest::{prelude::*, test_runner::TestCaseError};
1112
1113    use super::{SolveGradient, eig, equilibrate, faer_zeros, lu_threads, scratch, solve_owned};
1114    use crate::{
1115        Complex, Error,
1116        test_support::{ALGEBRA_CASES, complex_matrix, prop_assert_close},
1117    };
1118
1119    #[test]
1120    fn eigen_derivatives_share_one_lazy_factorization() {
1121        let operator = DMatrix::from_diagonal(&nalgebra::DVector::from_vec(vec![
1122            Complex::new(2.0, 0.1),
1123            Complex::new(3.0, -0.2),
1124        ]));
1125        let residual = eig(&operator).unwrap();
1126        assert!(residual.vectors_lu.get().is_none());
1127        // A trace cotangent needs no inverse eigenvectors, including at defective
1128        // eigenvalues. It should not allocate a derivative factorization.
1129        let weights = [Complex::from(1.0); 2];
1130        assert_eq!(
1131            residual.pullback(&weights, DMatrix::zeros(2, 2)).unwrap(),
1132            DMatrix::identity(2, 2)
1133        );
1134        assert!(residual.vectors_lu.get().is_none());
1135        let direction = DMatrix::from_element(2, 2, Complex::new(0.2, 0.1));
1136        let tangent = residual.pushforward(&direction).unwrap();
1137        let factor = residual.vectors_lu().unwrap();
1138        let gradient = residual
1139            .pullback(&weights, DMatrix::from_element(2, 2, Complex::from(0.3)))
1140            .unwrap();
1141        assert_eq!(residual.pushforward(&direction).unwrap(), tangent);
1142        assert_eq!(
1143            residual
1144                .pullback(&weights, DMatrix::from_element(2, 2, Complex::from(0.3)))
1145                .unwrap(),
1146            gradient
1147        );
1148        assert!(std::ptr::eq(factor, residual.vectors_lu().unwrap()));
1149    }
1150
1151    proptest! {
1152        #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
1153
1154        #[test]
1155        fn lu_scheduling_respects_worker_and_rhs_budgets(
1156            rows in 1_usize..100_000,
1157            columns in 1_usize..100_000,
1158            budget in 1_usize..128,
1159        ) {
1160            check_lu_scheduling(rows, columns, budget)?;
1161        }
1162    }
1163
1164    /// The worker count stays within the pool, is one below 32 right-hand sides, and
1165    /// otherwise uses a pool of up to four threads whole.
1166    fn check_lu_scheduling(
1167        rows: usize,
1168        columns: usize,
1169        budget: usize,
1170    ) -> Result<(), TestCaseError> {
1171        let threads = lu_threads(rows, columns, budget);
1172        prop_assert!((1..=budget).contains(&threads));
1173        if columns < 32 {
1174            prop_assert_eq!(threads, 1);
1175        } else if budget <= 4 {
1176            prop_assert_eq!(threads, budget);
1177        }
1178        Ok(())
1179    }
1180
1181    /// Power-of-two equation (row) and unknown (column) units of a linear system.
1182    #[derive(Clone, Debug)]
1183    enum Units {
1184        /// Independent row exponents.
1185        Rows(Vec<i32>),
1186        /// Independent column exponents.
1187        Columns(Vec<i32>),
1188        /// Independent row and column exponents.
1189        Both(Vec<i32>, Vec<i32>),
1190        /// Rows alternating `2^∓e` and columns `2^±e/2`, or with the two spreads
1191        /// exchanged, which makes the columns the more disparate axis.
1192        Alternating { exponent: i32, transposed: bool },
1193    }
1194
1195    impl Units {
1196        /// Row and column scales `(d_r, d_c)` of `A = d_r H d_c`.
1197        fn scales(&self, n: usize) -> (Vec<f64>, Vec<f64>) {
1198            let power = |e: i32| 2.0_f64.powi(e);
1199            let unit = vec![1.0; n];
1200            match self {
1201                Self::Rows(e) => (e.iter().map(|&e| power(e)).collect(), unit),
1202                Self::Columns(e) => (unit, e.iter().map(|&e| power(e)).collect()),
1203                Self::Both(r, c) => (
1204                    r.iter().map(|&e| power(e)).collect(),
1205                    c.iter().map(|&e| power(e)).collect(),
1206                ),
1207                &Self::Alternating {
1208                    exponent,
1209                    transposed,
1210                } => {
1211                    let sign = |i: usize| if i.is_multiple_of(2) { -1 } else { 1 };
1212                    let (wide, narrow) = (exponent, exponent / 2);
1213                    let (row, column) = if transposed {
1214                        (narrow, wide)
1215                    } else {
1216                        (wide, narrow)
1217                    };
1218                    (
1219                        (0..n).map(|i| power(sign(i) * row)).collect(),
1220                        (0..n).map(|j| power(-sign(j) * column)).collect(),
1221                    )
1222                }
1223            }
1224        }
1225    }
1226
1227    fn system() -> impl Strategy<Value = (DMatrix<Complex>, Units, DMatrix<Complex>)> {
1228        (1_usize..=40, 1_usize..=3).prop_flat_map(|(n, columns)| {
1229            let exponents = || prop::collection::vec(-400_i32..=400, n);
1230            (
1231                complex_matrix(n, n, 0.5),
1232                Just((0..n).collect::<Vec<_>>()).prop_shuffle(),
1233                prop_oneof![
1234                    exponents().prop_map(Units::Rows),
1235                    exponents().prop_map(Units::Columns),
1236                    (exponents(), exponents()).prop_map(|(r, c)| Units::Both(r, c)),
1237                    (0_i32..=400, any::<bool>()).prop_map(|(exponent, transposed)| {
1238                        Units::Alternating {
1239                            exponent,
1240                            transposed,
1241                        }
1242                    }),
1243                ],
1244                complex_matrix(n, columns, 1.0),
1245            )
1246                .prop_map(move |(random, rows, units, y)| {
1247                    // `random + n I` with permuted rows, so pivoting must reorder them.
1248                    let size = Complex::from(f64::from(u32::try_from(n).unwrap()));
1249                    let mut h = random + DMatrix::identity(n, n) * size;
1250                    h = DMatrix::from_fn(n, n, |i, j| h[(rows[i], j)]);
1251                    (h, units, y)
1252                })
1253        })
1254    }
1255
1256    proptest! {
1257        #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
1258
1259        #[test]
1260        fn solve_and_adjoint_are_invariant_to_equation_and_unknown_units(
1261            (h, units, y) in system(),
1262        ) {
1263            check_units(&h, &units, &y)?;
1264        }
1265    }
1266
1267    /// Alternating units put the row maxima of `2^20 I` plus entries of order one off
1268    /// its diagonal. With unit maxima alone, pivoting chose them and the solve kept only
1269    /// ten digits.
1270    #[test]
1271    fn units_keep_the_pivots_of_a_dominant_diagonal() -> Result<(), TestCaseError> {
1272        let t = |k: usize| f64::from(u32::try_from(k).unwrap());
1273        let h = DMatrix::from_fn(6, 6, |i, j| {
1274            Complex::new(0.5 - 0.125 * t(i), 0.25 + 0.125 * t(j))
1275        }) + DMatrix::identity(6, 6) * Complex::from(2.0_f64.powi(20));
1276        let y = DMatrix::from_fn(6, 1, |i, _| Complex::new(1.0, t(i)));
1277        for transposed in [false, true] {
1278            let units = Units::Alternating {
1279                exponent: 64,
1280                transposed,
1281            };
1282            check_units(&h, &units, &y)?;
1283        }
1284        Ok(())
1285    }
1286
1287    /// For `A = d_r H d_c` with a well-conditioned `H`, the solve of `A X = d_r H Y`
1288    /// gives `d_c X = Y` and the adjoint solve of `Aᴴ Z = d_c Hᴴ Y` gives `d_r Z = Y`,
1289    /// whatever the units: equilibration removes them in both the direct and the
1290    /// transposed branch. The returned scales reproduce the equilibrated operator,
1291    /// and the operator cotangent is `-Z Xᴴ`, so scaling `A` and `B` jointly leaves
1292    /// the loss unchanged.
1293    fn check_units(
1294        h: &DMatrix<Complex>,
1295        units: &Units,
1296        y: &DMatrix<Complex>,
1297    ) -> Result<(), TestCaseError> {
1298        let n = h.nrows();
1299        let (row, column) = units.scales(n);
1300        let operator = DMatrix::from_fn(n, n, |i, j| h[(i, j)] * row[i] * column[j]);
1301        let hy = h * y;
1302        let rhs = DMatrix::from_fn(n, y.ncols(), |i, j| hy[(i, j)] * row[i]);
1303        let tolerance = 1e-13 * y.norm();
1304        let residual = solve_owned(operator.clone(), rhs.clone()).unwrap();
1305        let unknowns = DMatrix::from_fn(n, y.ncols(), |i, j| residual.value[(i, j)] * column[i]);
1306        prop_assert_close!(&unknowns, y, tolerance);
1307        let hz = h.adjoint() * y;
1308        let cotangent = DMatrix::from_fn(n, y.ncols(), |i, j| hz[(i, j)] * column[i]);
1309        let adjoint = residual.adjoint_rhs(cotangent.clone()).unwrap();
1310        let equations = DMatrix::from_fn(n, y.ncols(), |i, j| adjoint[(i, j)] * row[i]);
1311        prop_assert_close!(&equations, y, tolerance);
1312
1313        let SolveGradient {
1314            operator: ga,
1315            rhs: gb,
1316        } = residual.pullback(cotangent).unwrap();
1317        prop_assert_eq!(&gb, &adjoint);
1318        for i in 0..n {
1319            for j in 0..n {
1320                let terms = (0..y.ncols()).map(|p| (adjoint[(i, p)], residual.value[(j, p)]));
1321                let expected: Complex = terms.clone().map(|(z, x)| -z * x.conj()).sum();
1322                let scale: f64 = terms.map(|(z, x)| z.norm() * x.norm()).sum();
1323                prop_assert_close!(ga[(i, j)], expected, 1e-14 * scale, "entry ({}, {})", i, j);
1324            }
1325        }
1326        // Scaling A and B jointly leaves X unchanged: Re⟨ga, A⟩ + Re⟨gb, B⟩ = 0.
1327        let pairs = ga.iter().zip(&operator).chain(gb.iter().zip(&rhs));
1328        let magnitude: f64 = pairs.clone().map(|(g, x)| g.norm() * x.norm()).sum();
1329        let joint: f64 = pairs.map(|(g, x)| (g.conj() * x).re).sum();
1330        prop_assert_close!(joint, 0.0, 1e-13 * magnitude);
1331
1332        // Equilibration in the direct or transposed branch.
1333        let Units::Alternating {
1334            exponent,
1335            transposed,
1336        } = *units
1337        else {
1338            return Ok(());
1339        };
1340        // Past 2^32 the spreads, not the random entries of H, decide the branch.
1341        if n < 2 || exponent < 32 {
1342            return Ok(());
1343        }
1344        let magnitude = |z: Complex| z.re.abs().max(z.im.abs());
1345        let row_min = operator
1346            .row_iter()
1347            .map(|r| r.iter().copied().map(magnitude).fold(0.0, f64::max))
1348            .fold(f64::INFINITY, f64::min);
1349        let column_min = operator
1350            .column_iter()
1351            .map(|c| c.iter().copied().map(magnitude).fold(0.0, f64::max))
1352            .fold(f64::INFINITY, f64::min);
1353        prop_assert_eq!(column_min < row_min, transposed);
1354        let mut equilibrated = operator.clone();
1355        let scales = equilibrate(&mut equilibrated).unwrap().unwrap();
1356        for i in 0..n {
1357            for j in 0..n {
1358                let expected = operator[(i, j)] * scales.row[i] * scales.column[j];
1359                let actual = equilibrated[(i, j)];
1360                let tolerance = 4.0 * f64::EPSILON * expected.norm() + f64::MIN_POSITIVE;
1361                prop_assert_close!(actual, expected, tolerance, "entry ({}, {})", i, j);
1362            }
1363        }
1364        let mut transpose = operator.transpose();
1365        let swapped = equilibrate(&mut transpose).unwrap().unwrap();
1366        prop_assert_eq!(&scales.row, &swapped.column);
1367        prop_assert_eq!(&scales.column, &swapped.row);
1368        Ok(())
1369    }
1370
1371    #[test]
1372    fn scaled_solves_keep_solutions_near_the_float_limits() {
1373        let p = |e: i32| 2.0_f64.powi(e);
1374        let (h, t, u) = (p(1023), p(-100), p(-1022));
1375        let systems = [
1376            // Balancing doubles row 1; the second block needs its small equations.
1377            (
1378                4,
1379                vec![
1380                    1.0, 1.0, 0.0, 0.0, 0.0, t, 0.0, 0.0, 0.0, 0.0, 1.0, u, 0.0, 0.0, 1.0, -u,
1381                ],
1382                vec![0.0, h, 0.0, 1.0],
1383            ),
1384            // The max scales double row 0.
1385            (2, vec![0.5, 0.5, 0.0, t], vec![1.5 * h, 1.5 * h]),
1386            // The right-hand side spans 2^2000 until the row scales apply.
1387            (2, vec![1.0, 0.0, 0.0, p(-1000)], vec![p(1000), 1.0]),
1388            // A subnormal right-hand side entry whose row scale makes it normal.
1389            (2, vec![t, 0.0, 0.0, 1.0], vec![p(-974), h]),
1390        ];
1391        let check = |a: DMatrix<Complex>, x: DMatrix<Complex>| {
1392            let b = &a * &x;
1393            assert_eq!(solve_owned(a.clone(), b.clone()).unwrap().value, x);
1394            let transposed = solve_owned(a.adjoint(), DMatrix::zeros(x.nrows(), 1)).unwrap();
1395            assert_eq!(transposed.adjoint_rhs(b).unwrap(), x);
1396        };
1397        for (n, a, x) in systems {
1398            let a = DMatrix::from_row_slice(n, n, &a).map(Complex::from);
1399            check(a, DMatrix::from_vec(n, 1, x).map(Complex::from));
1400        }
1401        // A tiny imaginary part beside a large real one.
1402        let a = DMatrix::from_row_slice(2, 2, &[1.0, u, 1.0, -u]).map(Complex::from);
1403        check(
1404            a,
1405            DMatrix::from_vec(2, 1, vec![Complex::from(h), Complex::i()]),
1406        );
1407    }
1408
1409    #[test]
1410    fn refused_and_overflowing_workspaces_return_out_of_memory() {
1411        let more = format!("cannot allocate more than {} bytes", usize::MAX);
1412        let half = StackReq::new::<Complex>(1 << 59);
1413        for overflow in [StackReq::new::<Complex>(usize::MAX), half.and(half)] {
1414            assert!(
1415                matches!(scratch(overflow), Err(Error::OutOfMemory(message)) if message == more),
1416                "{overflow:?}"
1417            );
1418        }
1419        // 2^52 bytes, beyond the user address space of every supported system.
1420        assert!(matches!(
1421            scratch(StackReq::new::<Complex>(1 << 48)),
1422            Err(Error::OutOfMemory(message)) if message == "cannot allocate 4503599627370496 bytes"
1423        ));
1424        assert!(scratch(StackReq::new::<Complex>(4)).is_ok());
1425        assert!(matches!(
1426            faer_zeros(1 << 24, 1 << 24),
1427            Err(Error::OutOfMemory(_))
1428        ));
1429        assert!(matches!(
1430            faer_zeros(usize::MAX, 2),
1431            Err(Error::OutOfMemory(_))
1432        ));
1433        let matrix = faer_zeros(3, 2).unwrap();
1434        assert_eq!(matrix.shape(), (3, 2));
1435        assert_eq!(matrix[(2, 1)], Complex::default());
1436    }
1437
1438    #[test]
1439    fn ordinary_operator_is_not_modified() {
1440        let mut operator = DMatrix::from_row_slice(
1441            2,
1442            2,
1443            &[
1444                Complex::new(2.0, 1.0),
1445                Complex::new(0.5, -0.25),
1446                Complex::new(-0.25, 0.5),
1447                Complex::new(3.0, -1.0),
1448            ],
1449        );
1450        let original = operator.clone();
1451        assert!(equilibrate(&mut operator).unwrap().is_none());
1452        assert_eq!(operator, original);
1453        operator[(0, 1)] = Complex::from(2.0_f64.powi(-100));
1454        assert!(equilibrate(&mut operator).unwrap().is_none());
1455        operator *= Complex::from(2.0_f64.powi(-600));
1456        assert!(equilibrate(&mut operator).unwrap().is_none());
1457    }
1458
1459    #[test]
1460    fn extreme_column_units_do_not_underflow_during_row_scaling() {
1461        let high = 2.0_f64.powi(600);
1462        let low = high.recip();
1463        let operator = DMatrix::from_row_slice(
1464            2,
1465            2,
1466            &[
1467                Complex::from(high),
1468                Complex::from(low),
1469                Complex::from(high),
1470                Complex::from(2.0 * low),
1471            ],
1472        );
1473        let rhs = DMatrix::from_column_slice(2, 1, &[Complex::from(1.0), Complex::from(2.0)]);
1474        let residual = solve_owned(operator, rhs).unwrap();
1475        assert_eq!(residual.value[(0, 0)], Complex::default());
1476        assert_eq!(residual.value[(1, 0)], Complex::from(high));
1477        // A^H [1, -1] = [0, -low], with both exact adjoint coordinates finite.
1478        let cotangent =
1479            DMatrix::from_column_slice(2, 1, &[Complex::default(), Complex::from(-low)]);
1480        let adjoint = residual.adjoint_rhs(cotangent).unwrap();
1481        assert_eq!(adjoint[(0, 0)], Complex::from(1.0));
1482        assert_eq!(adjoint[(1, 0)], Complex::from(-1.0));
1483    }
1484}