blob: 73d3a126d22aa5f6e31271d396bc2a6f78ead654 [file] [view]
# Eigen StructuredMatrices Module (`contrib/Eigen/StructuredMatrices`)
Fast operators and direct solvers for matrices determined by far fewer parameters than they have entries: circulant,
Toeplitz, Hankel, block-circulant, Kronecker products and sums, diagonal-plus-low-rank, Vandermonde and Cauchy
matrices, and the diagonal-plus-rank-one symmetric eigenproblem. Each operator stores only its generators or factors,
applies itself without forming the matrix, and solves, factors or inverts itself through closed forms or algorithms
that exploit the structure, for example in $`O(n \log n)`$ for circulant systems where a dense LU would take
$`O(n^3)`$.
```cpp
#include <contrib/Eigen/StructuredMatrices> // or the legacy <unsupported/Eigen/StructuredMatrices>
```
The module needs the `FFT` module (included by the umbrella header) and nothing outside Eigen. The class reference is
the Doxygen block of each class; this file gives the overview, the mathematics, and the costs.
## Contents
| Class | Matrix | Stored as | Product | Direct solve | Also |
|---|---|---|---|---|---|
| [`Circulant`](#circulant) | $`C_{ij} = c_{(i-j) \bmod n}`$ | first column | $`O(n \log n)`$ | `solve()` (pseudo-inverse), $`O(n \log n)`$ | eigen, SVD, rank, inverse, det |
| [`Toeplitz`](#toeplitz-and-lookaheadlevinson) | $`T_{ij} = t_{i-j}`$ | first column, first row | $`O(p \log p)`$ | `LookAheadLevinson`, $`O(n^2)`$ | solver `conditionEstimate()` |
| [`Hankel`](#hankel) | $`H_{ij} = h_{i+j}`$ | $`h`$, length $`m+n-1`$ | $`O(p \log p)`$ | `solve()`, $`O(n^2)`$ | `toToeplitz()` |
| [`Bccb`](#bccb) | 2-D circular convolution | $`n_2 \times n_1`$ array | $`O(N \log N)`$ | `solve()` (pseudo-inverse), $`O(N \log N)`$ | eigen, SVD, rank, inverse, det |
| [`KroneckerOperator`](#kroneckeroperator) | $`A \otimes B`$ | $`A`$, $`B`$ | $`O(N (n_1 + n_2))`$ | `solve()`, `leastSquaresSolve()` | eigen, SVD, rank, inverse, det |
| [`KroneckerSum`](#kroneckersum-and-bartelsstewart) | $`A \oplus B`$ | $`A`$, $`B`$ | $`O(N (n_1 + n_2))`$ | `BartelsStewart`, $`O(N \sum_k n_k)`$ | eigen |
| [`DiagonalPlusLowRank`](#diagonalpluslowrank) | $`D + U V^H`$ | $`d`$, $`U`$, $`V`$ | $`O(nk)`$ | `solve()` (Woodbury), $`O(nk^2 + k^3)`$ | inverse, det |
| [`Vandermonde`](#vandermonde-and-bjorckpereyra) | $`V_{ij} = x_i^{\,j}`$ | nodes | $`O(mn)`$ | `BjorckPereyra`, $`O(n^2)`$ | det |
| [`Cauchy`](#cauchy-and-cauchylu) | $`C_{ij} = 1/(x_i - y_j)`$ | nodes | $`O(mn)`$ | `CauchyLU`, $`O(n^2)`$ | det |
| [`DPR1EigenSolver`](#dpr1eigensolver) | $`D + \rho z z^T`$ | — | — | — | eigen, $`O(n^2)`$ |
Sizes: $`n`$ for a square operator, $`m \times n`$ for a rectangular one, $`p`$ the 5-smooth embedding length
$`\ge m+n-1`$, $`N = n_1 n_2`$ for the two-level operators, $`k`$ the rank of a low-rank correction. Product costs are
per right-hand side; the Kronecker entries are for dense square factors.
## Common conventions
### Operators
Every class in the table but `DPR1EigenSolver` is an operator: it derives from `EigenBase`, stores its own copy of its
generators, and provides `rows()`, `cols()` and `coeff(i, j)`. It is built by its constructor or by a `make*` factory
that deduces the compile-time sizes from the arguments (`makeCirculant`, `makeToeplitz`, `makeHankel`, `makeBccb`,
`makeKroneckerOperator`, `makeKroneckerSum`, `makeDiagonalPlusLowRank`, `makeVandermonde`, `makeCauchy`).
- **Products.** `A * x` returns an Eigen `Product` expression for a vector or a multi-column `x`. Assigning it behaves
like any dense product: a temporary resolves aliasing between the destination and `x`, and `.noalias()` skips it.
A real operator applied to a complex right-hand side yields a complex result.
- **Materialization.** Assigning an operator to a dense matrix forms it explicitly (`MatrixXd M = A;`, and also
`M += A;`, `M -= A;`). `KroneckerOperator` and `KroneckerSum` also materialize into a `SparseMatrix`
(`SparseMatrix<double> S; S = K;`).
- **Transposition.** `transpose()`, `conjugate()` and `adjoint()` return an operator of the same kind for every class
except `Vandermonde`, whose transpose has no fast product. The FFT-based operators transform their cached symbol
(index reversal, conjugation, or a phase factor) instead of recomputing an FFT.
- **Iterative solvers.** Since `A * x` is an ordinary product expression, the operators run matrix-free in
`ConjugateGradient`, `BiCGSTAB`, `GMRES`, `MINRES`, and, through `adjoint()`, the least-squares solvers `LSMR` and
`LeastSquaresConjugateGradient`. They expose no coefficient storage, so the solver must be instantiated with
`IdentityPreconditioner`:
```cpp
Toeplitz<double> T(col, row); // rectangular, m > n
LSMR<Toeplitz<double>, IdentityPreconditioner> lsmr(T);
VectorXd x = lsmr.solve(b); // O(p log p) per iteration
```
### Solvers
`LookAheadLevinson`, `BjorckPereyra`, `CauchyLU` and `BartelsStewart` derive from `SolverBase` and follow the usual
decomposition style: `compute(op)` or the constructor, a lazy `solve(b)` with any number of right-hand sides,
`info()`, and `transpose().solve(b)` and `adjoint().solve(b)`, which reuse the same factorization at the same cost.
`Circulant`, `Bccb`, `Hankel`, `KroneckerOperator`, `KroneckerSum` and `DiagonalPlusLowRank` also have a `solve(b)`
member that returns a plain matrix. `Circulant` and `Bccb` solve with their cached symbol; the others redo their setup
on every call (a Levinson factorization, factor LUs, Schur forms, or the capacitance LU), so for repeated solves use
`LookAheadLevinson` on `toToeplitz()` or `BartelsStewart` directly.
### Numerics
- **Determinants** are accumulated in the balanced form $`\mu \cdot 2^e`$, renormalizing every factor and the running
product to $`\max(|\Re\mu|, |\Im\mu|) \in [\tfrac12, 1)`$: the mantissa/exponent split of LINPACK's `xGEDI` [4],
in base 2 rather than 10 so that the rescaling is exact [12]. A partial product therefore cannot overflow or
underflow when the determinant itself is representable, whatever the ordering of the factors; zeros and non-finite
factors propagate exactly.
- **Numerical rank** follows `SVDBase`: a singular value $`\sigma`$ counts as zero exactly when
$`\sigma < \max(c\,\varepsilon\,\sigma_{\max},\ \text{smallest normal})`$ with $`c = \min(\text{rows}, \text{cols})`$.
The pseudo-inverse solves of `Circulant` and `Bccb` invert exactly the symbol entries that `rank()` counts.
- **FFTs** go through the `FFT` module's default backend, kissfft unless `EIGEN_FFTW_DEFAULT`, `EIGEN_MKL_DEFAULT`,
`EIGEN_POCKETFFT_DEFAULT` or `EIGEN_DUCCFFT_DEFAULT` selects another. kissfft is fast only for 2-, 3- and 5-smooth
lengths, so products pad the transform to the next length $`2^a 3^b 5^c`$; spectral quantities (eigenvalues,
solves, rank, determinant) use the exact length. Products use the FFT only when a dimension exceeds 32 (for `Bccb`,
when $`N`$ does), and never for a single-row or single-column `Hankel`. Each thread reuses one FFT engine and its
plan tables (a fresh engine per call under `EIGEN_AVOID_THREAD_LOCAL`).
## Circulant
`Circulant<Scalar, Size = Dynamic>` is the $`n \times n`$ matrix generated by its first column $`c`$:
```math
C_{ij} = c_{(i-j) \bmod n}, \qquad
C = F^H \operatorname{diag}(\hat c)\, F, \qquad
\hat c_k = \sum_{j=0}^{n-1} c_j\, \omega^{jk}, \quad \omega = e^{-2\pi i/n},
```
with $`F_{jk} = \omega^{jk}/\sqrt{n}`$ the unitary DFT matrix. The eigenvalues are the DFT $`\hat c`$ of the generator,
the operator's *symbol*, computed once at construction; the eigenvectors are the columns $`f_k`$ of $`F^H`$, with
entries $`e^{2\pi i jk/n}/\sqrt{n}`$, the same for every circulant matrix. Everything follows from this factorization:
| Member | Result | Cost |
|---|---|---|
| `C * x` | $`\mathcal{F}^{-1}(\hat c \odot \mathcal{F} x)`$ | $`O(n \log n)`$ |
| `solve(b)` | $`C^+ b = \mathcal{F}^{-1}(\hat c^{+} \odot \mathcal{F} b)`$, $`\hat c_k^{+} = 1/\hat c_k`$ for the entries `rank()` counts, else 0 | $`O(n \log n)`$ |
| `eigenvalues()`, `symbol()` | $`\hat c`$, in DFT order | precomputed for $`n > 32`$ |
| `eigenvectors()` | $`[f_0, \dots, f_{n-1}]`$, dense | $`O(n^2)`$ |
| `singularValues()` | $`\lvert \hat c_k \rvert`$, decreasing | $`O(n \log n)`$ |
| `matrixU()`, `matrixV()` | columns $`e^{i \arg \hat c_k} f_k`$ and $`f_k`$, in the order of `singularValues()`, dense | $`O(n^2)`$ |
| `rank()`, `determinant()` | count of retained $`\hat c_k`$; $`\prod_k \hat c_k`$ | $`O(n \log n)`$ |
| `inverse()` | the `Circulant` with column $`\mathcal{F}^{-1}(1/\hat c)`$ | $`O(n \log n)`$ |
| `transpose()`, `conjugate()`, `adjoint()` | `Circulant`, symbol $`\hat c_{-k}`$, $`\overline{\hat c_{-k}}`$, $`\overline{\hat c_k}`$ (indices mod $`n`$) | $`O(n)`$ |
For a real generator the symbol is conjugate-symmetric, $`\hat c_{n-k} = \overline{\hat c_k}`$, the two modes of each
pair carry identical singular values, and `determinant()` returns the real part of the product. `inverse()` requires
a nonsingular operator; `solve()` handles singular ones.
```cpp
VectorXd c = VectorXd::Random(n);
Circulant<double> C(c); // or makeCirculant(c)
VectorXd y = C * x;
VectorXd z = C.solve(y); // minimum-norm least-squares solution
VectorXcd lambda = C.eigenvalues();
```
## Toeplitz and LookAheadLevinson
`Toeplitz<Scalar, Rows = Dynamic, Cols = Dynamic>` is the $`m \times n`$ matrix with constant diagonals, generated by
its first column $`c`$ and first row $`r`$ ($`r_0`$ is ignored; the diagonal comes from $`c`$):
```math
T_{ij} = \begin{cases} c_{i-j}, & i \ge j, \\ r_{j-i}, & i < j. \end{cases}
```
The product embeds $`T`$ in the circulant matrix of 5-smooth size $`p \ge m+n-1`$ with first column
$`[c_0, \dots, c_{m-1}, 0, \dots, 0, r_{n-1}, \dots, r_1]^T`$; $`Tx`$ is the leading $`m`$ entries of that circulant
applied to $`[x; 0]`$, at $`O(p \log p)`$ with the embedding symbol computed once at construction. `transpose()`
swaps the generators; it and `conjugate()` and `adjoint()` reuse the embedding symbol. `column()` and `row()` return the
generators.
`LookAheadLevinson<Scalar>` solves square systems $`Tx = b`$ in $`O(n^2)`$ by the look-ahead Levinson algorithm of
Chan and Hansen [1, 2]. The classical Levinson recursion breaks down at a singular or ill-conditioned leading principal
submatrix, which general (indefinite, nonsymmetric) Toeplitz matrices often have; the look-ahead variant steps over
up to $`p_{\max} - 1`$ consecutive ill-conditioned leading submatrices with a block step, and remains weakly stable
when there are no longer runs.
| Member | Meaning |
|---|---|
| `setMaxBlockSize(p)` | $`p_{\max}`$, default 4; call before `compute()` |
| `info()` | `Success`, or `NumericalIssue` when `conditionEstimate()` reaches $`1/\varepsilon`$; a run of ill-conditioned leading submatrices longer than the look-ahead is reported only through that estimate |
| `conditionEstimate()` | estimate of $`\kappa_2(T)`$, the "algorithm condition number" of [1] |
| `transpose().solve(b)`, `adjoint().solve(b)` | reuse the factorization through the persymmetry $`T^T = E T E`$ ($`E`$ the exchange matrix) |
```cpp
Toeplitz<double> T(col, row); // square
LookAheadLevinson<double> levinson(T);
VectorXd x = levinson.solve(b); // O(n^2)
if (levinson.info() != Success) { /* condition estimate reached 1/eps */ }
```
## Hankel
`Hankel<Scalar, Rows = Dynamic, Cols = Dynamic>` is the $`m \times n`$ matrix with constant anti-diagonals,
$`H_{ij} = h_{i+j}`$. It is built from its first column and last row (the shared corner entry comes from the column)
and stores the single sequence $`h`$ of length $`m+n-1`$ (`generator()`, `column()`, `lastRow()`). A real square
Hankel matrix is symmetric.
A Hankel matrix is a column-reversed Toeplitz matrix, $`H = TE`$ with $`T_{ij} = h_{i+n-1-j}`$ (`toToeplitz()`), so
its product is a convolution evaluated in $`O(p \log p)`$ like the Toeplitz one. Single-row and single-column
operators skip the FFT. `transpose()` keeps $`h`$ and swaps the dimensions; its symbol is the cached one multiplied by
the phases $`e^{2\pi i f (m-n)/p}`$, with no new FFT.
`solve(b)` solves a square system in $`O(n^2)`$ as $`T (Ex) = b`$ with `LookAheadLevinson`. It factorizes on every
call and `eigen_assert`s that the recursion did not break down; to reuse the factorization or check `info()` instead,
run `LookAheadLevinson` on `toToeplitz()` and reverse the rows of its solution.
## Bccb
`Bccb<Scalar, BlockSize = Dynamic, NumBlocks = Dynamic>` is a block circulant matrix with circulant blocks, the
matrix of a two-dimensional circular convolution. It is $`N \times N`$, $`N = n_1 n_2`$: an $`n_1 \times n_1`$ block
circulant whose blocks are $`n_2 \times n_2`$ circulants. It is generated by the $`n_2 \times n_1`$ array $`G`$ whose
column $`k`$ is the first column of block $`k`$; with $`i = i_1 n_2 + i_2`$ and $`j = j_1 n_2 + j_2`$,
```math
C_{ij} = G_{(i_2 - j_2) \bmod n_2,\ (i_1 - j_1) \bmod n_1}, \qquad
C \operatorname{vec}(X) = \operatorname{vec}(G \circledast X),
```
where $`\operatorname{vec}`$ stacks columns and $`\circledast`$ is 2-D circular convolution. BCCB matrices are
diagonalized by the 2-D DFT $`F_{n_1} \otimes F_{n_2}`$ [5, 6]: the symbol $`\hat G`$, the 2-D DFT of $`G`$, holds
the eigenvalues, and eigenvalue $`f_1 n_2 + f_2`$ is $`\hat G_{f_2 f_1}`$. The API mirrors `Circulant` (`solve`,
`eigenvalues`, `eigenvectors`, `singularValues`, `matrixU`, `matrixV`, `rank`, `inverse`, `determinant`, and the
transposition family) at $`O(N \log N)`$, with $`O(N^2)`$ for the dense vector matrices. `blockSize()` and
`numBlocks()` return $`n_2`$ and $`n_1`$.
BCCB matrices model blurring with periodic boundary conditions in image deblurring, and are the standard
preconditioners for two-level Toeplitz (BTTB) systems [6].
## KroneckerOperator
`KroneckerOperator<Lhs, Rhs>` is the Kronecker product $`A \otimes B`$, $`A`$ of size $`m_1 \times n_1`$ and $`B`$ of
size $`m_2 \times n_2`$, as an operator that is never formed. With $`\operatorname{vec}`$ stacking the columns of an
$`n_2 \times n_1`$ matrix $`X`$ (whatever the storage order) and $`\operatorname{mat}`$ its inverse,
```math
(A \otimes B) \operatorname{vec}(X) = \operatorname{vec}(B X A^T),
```
at $`O(m_2 n_1 (n_2 + m_1))`$ per right-hand side instead of $`O(m_1 m_2 n_1 n_2)`$; right-hand sides are processed in
cache-sized batches. Everything else factors through $`A`$ and $`B`$:
| Member | Identity | Notes |
|---|---|---|
| `solve(b)` | $`X = B^{-1} \operatorname{mat}(b) A^{-T}`$ | one LU per dense factor, `SparseLU` per sparse factor; factors square and invertible |
| `leastSquaresSolve(b)` | $`(A \otimes B)^+ = A^+ \otimes B^+`$ | one `CompleteOrthogonalDecomposition` per factor, rank decided per factor; rectangular or rank-deficient factors |
| `rank()` | number of $`\sigma_i(A)\,\sigma_j(B) \ge \min(\text{rows}, \text{cols})\,\varepsilon\,\sigma_{\max}(A)\,\sigma_{\max}(B)`$ | two SVDs; can be below $`\operatorname{rank}(A)\operatorname{rank}(B)`$ |
| `determinant()` | $`\det(A)^{n_2} \det(B)^{n_1}`$ | balanced accumulation of the factor LU pivots |
| `inverse()` | $`A^{-1} \otimes B^{-1}`$ | a `KroneckerOperator` |
| `eigenvalues()`, `eigenvectors()` | $`\lambda_i(A)\,\mu_j(B)`$ at index $`i n_2 + j`$; $`V_A \otimes V_B`$ | unsorted; the vectors are a `KroneckerOperator` |
| `singularValues()`, `matrixU()`, `matrixV()` | $`\sigma_i(A)\,\sigma_j(B)`$; $`U_A \otimes U_B`$; $`V_A \otimes V_B`$ | thin SVD, unsorted; the vectors are `KroneckerOperator`s |
| `transpose()`, `conjugate()`, `adjoint()` | $`A^T \otimes B^T`$, $`\bar A \otimes \bar B`$, $`A^H \otimes B^H`$ | |
The eigenvalues and singular values stay in Kronecker order because sorting them would destroy the Kronecker
structure of the vector matrices.
Each factor can be any of:
| Factor passed | Stored as | Its side of a product | Its solve |
|---|---|---|---|
| dense expression | `Matrix` | GEMM | `PartialPivLU` |
| `DiagonalMatrix` or `.asDiagonal()` expression | its diagonal, $`O(n)`$ | scaling | entrywise division |
| `SparseMatrix` | compressed `SparseMatrix` | sparse-dense product, $`O(\operatorname{nnz})`$ per column | `SparseLU`, factorized once |
| `MatrixXd::Identity(m, n)` | dimensions only | skipped when square; a rectangular one keeps the leading $`\min(m, n)`$ rows or columns | identity |
| `KroneckerOperator` | as is (nested) | its own vec identity | its own factor solves |
| `KroneckerSum` | as is (nested) | $`B X + X A^T`$ | `BartelsStewart` |
`makeKroneckerOperator(a, b, c, ...)` nests to the right, so products of three or more factors stay implicit. The
decomposition family (`eigenvalues` through `matrixV`, `rank`, `leastSquaresSolve`) works on dense copies of
diagonal, identity and sparse factors; `rank` and `leastSquaresSolve` also materialize a nested factor. A sparse
factor whose `SparseLU` fails solves and inverts to NaN.
```cpp
SparseMatrix<double> A = ...; // n x n
auto K = makeKroneckerOperator(MatrixXd::Identity(p, p), A, MatrixXd::Identity(q, q)); // I_p (x) A (x) I_q
VectorXd y = K * x; // O(p q nnz(A)), nothing of size p n q formed
```
Unlike `kroneckerProduct()` from the `KroneckerProduct` module, an expression meant to be evaluated into a matrix,
`KroneckerOperator` is meant to be applied and solved with as it is.
## KroneckerSum and BartelsStewart
`KroneckerSum<Lhs, Rhs>` is the Kronecker sum of square factors, $`A`$ of size $`n_1`$ and $`B`$ of size $`n_2`$,
```math
A \oplus B = A \otimes I_{n_2} + I_{n_1} \otimes B, \qquad
(A \oplus B) \operatorname{vec}(X) = \operatorname{vec}(B X + X A^T),
```
the operator of separable discretizations on tensor-product grids: with $`D_x, D_y`$ the 1-D second-difference
matrices and $`x`$ the fast index of the unknown vector, the 2-D Laplacian is $`D_y \oplus D_x`$ and the 3-D one
$`D_z \oplus D_y \oplus D_x`$. The factors may be
of any kind `KroneckerOperator` accepts, including a `KroneckerSum` (`makeKroneckerSum(a, b, c, ...)` nests to the
right). The product costs one product with each factor, $`O(N (n_1 + n_2))`$ for dense factors and
$`O(n_1 \operatorname{nnz}(B) + n_2 \operatorname{nnz}(A))`$ for sparse ones, with no identity ever formed.
`eigenvalues()` returns the sums $`\lambda_i(A) + \mu_j(B)`$ at index $`i n_2 + j`$ and `eigenvectors()` the
`KroneckerOperator` $`V_A \otimes V_B`$.
`BartelsStewart<KroneckerSumType>` solves $`(A_1 \oplus \cdots \oplus A_d)\, x = b`$ for any number of factors. With
Schur forms $`A_k = Q_k T_k Q_k^H`$,
```math
A_1 \oplus \cdots \oplus A_d = Q\, (T_1 \oplus \cdots \oplus T_d)\, Q^H, \qquad Q = Q_1 \otimes \cdots \otimes Q_d,
```
so a solve applies $`Q^H`$, back-substitutes the (quasi-)upper-triangular middle factor, and applies $`Q`$; for two
factors this is the Bartels-Stewart algorithm [3] for the Sylvester equation $`B X + X A^T = \operatorname{mat}(b)`$.
The Schur forms depend on the factors:
- every factor exactly Hermitian (`isHermitian()`): eigendecompositions, so the middle factor is diagonal and the
solve divides by the eigenvalue sums, the fast diagonalization method [7], in real arithmetic for real factors;
- real factors, not all symmetric, at most four of them: real Schur forms, $`Q_k`$ orthogonal and $`T_k`$
quasi-upper-triangular with a $`2 \times 2`$ block per complex conjugate eigenvalue pair; the coupled block rows
are solved jointly, ending in dense solves at most $`2^d`$ wide, as LAPACK's `xTRSYL` does for two factors;
- otherwise (complex factors, or more than four real ones): complex Schur forms, $`T_k`$ upper triangular.
Setup costs one $`O(n_k^3)`$ decomposition per factor (sparse factors are densified for it) and each solve
$`O(N \sum_k n_k)`$ per right-hand side, plus up to $`O(4^d N)`$ for the coupled solves of the real path.
`KroneckerSum::solve(b)` builds a `BartelsStewart` for that one call.
The system is singular exactly when some sum $`\lambda_{i_1}(A_1) + \cdots + \lambda_{i_d}(A_d)`$ vanishes, which, as
with `PartialPivLU`, nothing detects. The residual satisfies
$`\lVert b - Mx \rVert = O\big((\textstyle\sum_k n_k)\,\varepsilon\,(\sum_k \lVert A_k \rVert)\,\lVert x \rVert\big)`$,
relative to $`\sum_k \lVert A_k \rVert`$ rather than $`\lVert M \rVert`$, which matters when a shift is split across
the factors as in $`(A + cI) \oplus (B - cI)`$. `info()` reports `InvalidInput` for a non-finite factor and
`NoConvergence` when a Schur or eigenvalue iteration fails; the solve then returns NaN.
```cpp
// Implicit Euler for u' = (Dy (+) Dx) u: (I - tau L) u_new = u, with I (x) I = I absorbed into one factor.
SparseMatrix<double> Iy(ny, ny);
Iy.setIdentity();
auto M = makeKroneckerSum(Iy - tau * Dy, -tau * Dx);
BartelsStewart<decltype(M)> step(M); // Schur forms, once
for (int k = 0; k < steps; ++k) u = step.solve(u);
```
## DiagonalPlusLowRank
`DiagonalPlusLowRank<Scalar, Size = Dynamic, Rank = Dynamic>` is $`A = D + U V^H`$ with $`D = \operatorname{diag}(d)`$
and $`U, V`$ of size $`n \times k`$ (`diagonal()`, `factorU()`, `factorV()`, `correctionRank()`; $`k = 0`$ is
allowed). Products cost $`O(nk)`$. The Woodbury identity [8] and the matrix determinant lemma [9] reduce everything
else to the $`k \times k`$ capacitance matrix $`S = I_k + V^H D^{-1} U`$ (`capacitance()`):
```math
A^{-1} = D^{-1} - D^{-1} U S^{-1} V^H D^{-1}, \qquad \det A = \det(D) \det(S).
```
`solve(b)` costs $`O(nk^2 + k^3)`$ to set up and $`O(nk)`$ per right-hand side; `determinant()` is $`O(nk^2 + k^3)`$;
`inverse()` returns the `DiagonalPlusLowRank` $`D^{-1} + \tilde U \tilde V^H`$ with $`\tilde U = -D^{-1} U S^{-1}`$ and
$`\tilde V = D^{-H} V`$; `transpose()` is $`D + \bar V \bar U^H`$ and `adjoint()` is $`\bar D + V U^H`$.
`solve` and `inverse` need $`D`$ and $`S`$ nonsingular and `determinant` needs $`D`$ nonsingular, all with finite
reciprocals $`1/d_i`$; this is not checked beyond NaN/Inf propagation. The splitting routes the solution through
$`D^{-1} b`$, so accuracy degrades when $`\max_i |1/d_i|`$ greatly exceeds $`\lVert A^{-1} \rVert`$, even for a
well-conditioned $`A`$ [10].
## Vandermonde and BjorckPereyra
`Vandermonde<Scalar, Rows = Dynamic, Cols = Dynamic>` is the $`m \times n`$ matrix $`V_{ij} = x_i^{\,j}`$ of the
nodes $`x`$ (`nodes()`); `Vandermonde(x)` is square and `Vandermonde(x, n)` has $`n`$ columns. `V * a` evaluates the
polynomial with ascending coefficients $`a`$ at every node by Horner's rule,
```math
(Va)_i = \sum_{j=0}^{n-1} a_j x_i^{\,j}: \qquad p_{n-1} = a_{n-1}, \quad p_j = a_j + x_i\, p_{j+1},
```
at $`O(mn)`$, the cost of a dense product but with $`O(m)`$ storage. `determinant()` uses
$`\det V = \prod_{i<j} (x_j - x_i)`$ in $`O(n^2)`$. The transpose has no fast product, so `Vandermonde` is not closed
under transposition; least-squares problems are best solved by a dense QR of the materialized matrix. Integer scalar
types are rejected.
`BjorckPereyra<Scalar>` solves square systems in $`O(n^2)`$ operations and $`O(n)`$ storage [11, §4.6]: $`Va = f`$ is
polynomial interpolation, solved by divided differences in the Newton basis followed by the change to the monomial
basis, and the dual (moment) system $`V^T w = b`$ runs the transposed recurrences through `transpose().solve(b)`.
There is no factorization: `compute()` stores the nodes and `info()` reports `NumericalIssue` for an exactly repeated
node or `InvalidInput` for a non-finite one. Genuinely complex nodes are put in Leja order [13] to limit growth in
the Newton representation; real nodes keep their input order.
Real-node Vandermonde matrices are exponentially ill-conditioned: $`\kappa_2(V)`$ grows at least like $`2^n`$ for every
real node set [14]. Björck-Pereyra solves are nevertheless often far more accurate than that suggests: for monotone
nodes and a sign-alternating right-hand side the forward error obeys a small relative bound independent of
$`\kappa(V)`$ [15, ch. 22]. Nodes on the unit circle are the well-conditioned case; for the $`n`$-th roots of unity
$`V/\sqrt{n}`$ is unitary.
## Cauchy and CauchyLU
`Cauchy<Scalar, Rows = Dynamic, Cols = Dynamic>` is $`C_{ij} = 1/(x_i - y_j)`$ for row nodes $`x`$ and column nodes
$`y`$ (`rowNodes()`, `colNodes()`), stored as the $`m + n`$ nodes. Every $`x_i`$ must differ from every $`y_j`$; this
is not checked. Products are evaluated directly at $`O(mn)`$ with $`O(m)`$ extra storage. The class is closed under
transposition, $`C(x, y)^T = C(-y, -x)`$ and $`C(x, y)^H = C(-\bar y, -\bar x)`$, and the determinant of a square
Cauchy matrix has the closed form
```math
\det C = \frac{\prod_{i<j} (x_j - x_i)(y_i - y_j)}{\prod_{i,j} (x_i - y_j)},
```
evaluated in $`O(n^2)`$. The Hilbert matrix is the Cauchy matrix with $`x_i = i + 1`$, $`y_j = -j`$.
`CauchyLU<Scalar>` factors $`PC = LU`$ with partial pivoting in $`O(n^2)`$ by the Gohberg-Kailath-Olshevsky
algorithm [16]. A Cauchy matrix satisfies the rank-one displacement equation
$`D_x C - C D_y = \mathbf{1}\mathbf{1}^T`$, which survives row permutations, so every Schur complement is again
Cauchy-like, $`S_{ij} = a_i b_j / (x_i - y_j)`$, and elimination step $`k`$ only updates the generators:
```math
a_i \leftarrow a_i \frac{x_i - x_k}{x_i - y_k}, \qquad b_j \leftarrow b_j \frac{y_j - y_k}{y_j - x_k}.
```
Each column of the Schur complement is generated from $`O(n)`$ data, so the pivot search costs nothing extra: this is
genuine partial pivoting at fast-algorithm cost, which the Levinson family for Toeplitz matrices lacks. The factors
are stored densely ($`O(n^2)`$ memory); `info()` reports `NumericalIssue` when a zero or non-finite entry prevents
the factorization.
## DPR1EigenSolver
`DPR1EigenSolver<RealScalar>` (`float`, `double` or `long double`) computes all eigenvalues and eigenvectors of the
real symmetric diagonal-plus-rank-one matrix $`A = D + \rho z z^T`$ in $`O(n^2)`$. It is the kernel at the heart of
divide-and-conquer symmetric eigensolvers (LAPACK's `xLAED2`/`xLAED3`/`xLAED4`), as a standalone class. After
deflation (entries with negligible $`|z_i|`$ are already eigenpairs, and nearly equal $`d_i`$ are merged by Givens
rotations whose dropped coupling is below a backward-stability threshold), the remaining eigenvalues are the roots of
the secular equation
```math
f(\lambda) = 1 + \rho \sum_i \frac{z_i^2}{d_i - \lambda} = 0,
```
one between each pair of consecutive poles and, for $`\rho > 0`$, one beyond the largest pole. Each root is bracketed
and bisected in coordinates shifted to its nearest pole, so every $`\lambda - d_i`$ is an exact data difference plus a
small offset. The eigenvectors are built from the Gu-Eisenstat vector [17]
```math
\hat z_i^2 = \frac{\prod_j (\lambda_j - d_i)}{\rho \prod_{j \ne i} (d_j - d_i)}, \qquad
v_j \propto (D - \lambda_j I)^{-1} \hat z,
```
for which the computed $`\lambda_j`$ are the exact eigenvalues of $`D + \rho \hat z \hat z^T`$; this makes the computed
$`V`$ numerically orthogonal without reorthogonalization. Either sign of $`\rho`$, $`\rho = 0`$, zero $`z`$, repeated
$`d_i`$ and any ordering of $`d`$ are supported; the problem is scaled internally by an exact power of two.
```cpp
DPR1EigenSolver<double> es(d, rho, z); // or es.compute(d, rho, z, EigenvaluesOnly)
VectorXd lambda = es.eigenvalues(); // ascending
MatrixXd V = es.eigenvectors(); // orthogonal
```
`info()` reports `InvalidInput` for non-finite input, an overflowing $`\rho \lVert z \rVert^2`$ or a non-finite
eigenvalue, and `NoConvergence` when a secular root could not be resolved.
## Tests and benchmarks
The tests are `contrib/test/structured_*.cpp`: `structured_matrices` (Circulant, Toeplitz, LookAheadLevinson,
Hankel), `structured_bccb`, `structured_kronecker`, `structured_kronecker_sum`, `structured_dplr`,
`structured_vandermonde` (and `structured_vandermonde_int_index`), `structured_cauchy` and `structured_dpr1`.
```bash
cmake -G Ninja -S . -B build
cmake --build build --target structured_matrices structured_bccb structured_kronecker structured_kronecker_sum \
structured_dplr structured_vandermonde structured_vandermonde_int_index structured_cauchy structured_dpr1
ctest --test-dir build -R '^structured_' --output-on-failure --no-tests=error
```
Benchmarks are in `contrib/benchmarks/StructuredMatrices` (`bench_structured_*`, one per operator and for the
solves, transposition, materialization and the Kronecker spectral paths) and `benchmarks/StructuredMatrices`
(FFT operators, Vandermonde and Cauchy, diagonal-plus-low-rank). Each tree is a standalone CMake project:
```bash
cmake -G Ninja -S contrib/benchmarks -B build-contrib-bench -DCMAKE_BUILD_TYPE=Release
cmake --build build-contrib-bench --target bench_structured_circulant
```
## References
1. T. F. Chan and P. C. Hansen, "A look-ahead Levinson algorithm for general Toeplitz systems," *IEEE Trans. Signal
Process.* 40(5):1079–1090, 1992.
2. T. F. Chan and P. C. Hansen, "A look-ahead Levinson algorithm for indefinite Toeplitz systems," *SIAM J. Matrix
Anal. Appl.* 13(2):490–506, 1992.
3. R. H. Bartels and G. W. Stewart, "Solution of the matrix equation AX + XB = C," *Comm. ACM* 15:820–826, 1972.
4. J. J. Dongarra, J. R. Bunch, C. B. Moler and G. W. Stewart, *LINPACK Users' Guide*, SIAM, 1979.
5. P. J. Davis, *Circulant Matrices*, Wiley, 1979.
6. R. H. Chan and X.-Q. Jin, *An Introduction to Iterative Toeplitz Solvers*, SIAM, 2007.
7. R. E. Lynch, J. R. Rice and D. H. Thomas, "Direct solution of partial difference equations by tensor product
methods," *Numer. Math.* 6:185–199, 1964.
8. M. A. Woodbury, "Inverting modified matrices," Memorandum Report 42, Statistical Research Group, Princeton
University, 1950.
9. W. W. Hager, "Updating the inverse of a matrix," *SIAM Review* 31(2):221–239, 1989.
10. E. L. Yip, "A note on the stability of solving a rank-p modification of a linear system by the
Sherman-Morrison-Woodbury formula," *SIAM J. Sci. Stat. Comput.* 7(2):507–513, 1986.
11. G. H. Golub and C. F. Van Loan, *Matrix Computations*, 4th ed., Johns Hopkins University Press, 2013.
12. P. H. Sterbenz, *Floating-Point Computation*, Prentice-Hall, 1974.
13. L. Reichel, "Newton interpolation at Leja points," *BIT* 30:332–346, 1990.
14. B. Beckermann, "The condition number of real Vandermonde, Krylov and positive definite Hankel matrices,"
*Numer. Math.* 85:553–577, 2000.
15. N. J. Higham, *Accuracy and Stability of Numerical Algorithms*, 2nd ed., SIAM, 2002.
16. I. Gohberg, T. Kailath and V. Olshevsky, "Fast Gaussian elimination with partial pivoting for matrices with
displacement structure," *Math. Comp.* 64:1557–1576, 1995.
17. M. Gu and S. C. Eisenstat, "A stable and efficient algorithm for the rank-one modification of the symmetric
eigenproblem," *SIAM J. Matrix Anal. Appl.* 15(4):1266–1276, 1994.