Skip to content

Commit 526219d

Browse files
authored
docs: simplify (#134)
1 parent d8ad834 commit 526219d

File tree

4 files changed

+266
-361
lines changed

4 files changed

+266
-361
lines changed

docs/make.jl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ makedocs(
66
format = Documenter.HTML(),
77
pages = [
88
"Home" => "index.md",
9-
"Theory" => "theory.md",
10-
"Using ArnoldiMethod.jl" => "usage.md",
119
],
1210
warnonly = [:missing_docs, :cross_references],
1311
)

docs/src/index.md

Lines changed: 266 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,144 @@ or non-symmetric and either real or complex.
1414
The method is *matrix-free*, meaning that it only requires multiplication with
1515
the matrix $A$.
1616

17+
## Installing
18+
19+
In Julia open the package manager in the REPL via `]` and run:
20+
21+
```julia
22+
(v1.6) pkg> add ArnoldiMethod
23+
```
24+
25+
Then use the package.
26+
27+
```julia
28+
using ArnoldiMethod
29+
```
30+
1731
The package exports just two functions:
1832
- [`partialschur`](@ref) to compute a stable basis for an eigenspace;
1933
- [`partialeigen`](@ref) to compute an eigendecomposition from a partial Schur
2034
decomposition.
2135

22-
See **[Using ArnoldiMethod.jl](@ref getting_started)** on how to use these
23-
functions.
36+
## Example
37+
38+
Here we compute the first ten eigenvalues and eigenvectors of a tridiagonal
39+
sparse matrix.
40+
41+
```julia
42+
julia> using ArnoldiMethod, LinearAlgebra, SparseArrays
43+
44+
julia> A = spdiagm(
45+
-1 => fill(-1.0, 99),
46+
0 => fill(2.0, 100),
47+
1 => fill(-1.0, 99)
48+
);
49+
50+
julia> decomp, history = partialschur(A, nev=10, tol=1e-6, which=:SR);
51+
52+
julia> decomp
53+
PartialSchur decomposition (Float64) of dimension 10
54+
eigenvalues:
55+
10-element Array{Complex{Float64},1}:
56+
0.0009674354160236865 + 0.0im
57+
0.003868805732811139 + 0.0im
58+
0.008701304061962657 + 0.0im
59+
0.01546025527344699 + 0.0im
60+
0.024139120518486677 + 0.0im
61+
0.0347295035554728 + 0.0im
62+
0.04722115887278571 + 0.0im
63+
0.06160200160067088 + 0.0im
64+
0.0778581192025522 + 0.0im
65+
0.09597378493453936 + 0.0im
66+
67+
julia> history
68+
Converged: 10 of 10 eigenvalues in 174 matrix-vector products
69+
70+
julia> norm(A * decomp.Q - decomp.Q * decomp.R)
71+
6.39386920955869e-8
72+
73+
julia> λs, X = partialeigen(decomp);
74+
75+
julia> norm(A * X - X * Diagonal(λs))
76+
6.393869211477937e-8
77+
```
78+
79+
80+
## Partial Schur decomposition
81+
82+
The [`partialschur`](@ref) method forms the backbone of the package. It computes
83+
an approximate, partial Schur decomposition of a matrix $A$:
84+
85+
```math
86+
AQ = QR
87+
```
88+
89+
where $Q$ is orthonormal of size $n \times \texttt{nev}$ and $R$ is upper
90+
triangular of size $\texttt{nev} \times \texttt{nev}.$ with eigenvalues of
91+
$A$ on the diagonal.
92+
93+
!!! note "2x2 blocks in real arithmetic"
94+
95+
In real arithmetic $R$ is quasi upper triangular, with $2 \times 2$ blocks on the
96+
diagonal corresponding to conjugate complex-valued eigenpairs. These $2 \times 2$
97+
blocks are used for efficiency, since otherwise the entire Schur form would have to
98+
use complex arithmetic.
99+
100+
!!! note "A partial Schur decomposition is often enough"
101+
102+
Often a partial Schur decomposition is all you need, cause it's more stable
103+
to compute and work with than a partial eigendecomposition.
104+
105+
Also note that for complex Hermitian and real symmetric matrices, the partial
106+
Schur form coincides with the partial eigendecomposition (the $R$ matrix is
107+
diagonal).
108+
109+
```@docs
110+
partialschur
111+
```
112+
113+
## Partial eigendecomposition
114+
115+
After computing the partial Schur decomposition, it can be transformed into a
116+
partial eigendecomposition via the [`partialeigen`](@ref) helper function. The
117+
basic math is to determine the eigendecomposition of the upper triangular matrix
118+
$RY = Y\Lambda$ such that
119+
120+
```math
121+
A(QY) = (QY)\Lambda
122+
```
123+
124+
forms the full eigendecomposition of $A$, where $QY$ are the eigenvectors and
125+
$\Lambda$ is a $\texttt{nev} \times \texttt{nev}$ diagonal matrix of eigenvalues.
126+
127+
This step is a relatively cheap post-processing step.
128+
129+
```@docs
130+
partialeigen
131+
```
132+
133+
## Stopping criterion
134+
ArnoldiMethod.jl considers an approximate eigenpair converged when the
135+
condition
136+
137+
```math
138+
\|Ax - x\lambda\|_2 < \texttt{tol}|\lambda|
139+
```
140+
141+
is met, where `tol` is a user provided tolerance. Note that this stopping
142+
criterion is scale-invariant. For a scaled matrix $B = \alpha A$ the same
143+
approximate eigenvector together with the scaled eigenvalue $\alpha\lambda$
144+
would satisfy the stopping criterion.
145+
146+
147+
## The PartialSchur and History structs
148+
149+
For completeness, the return values of the [`partialschur`](@ref) function:
150+
151+
```@docs
152+
ArnoldiMethod.PartialSchur
153+
ArnoldiMethod.History
154+
```
24155

25156
## What algorithm is ArnoldiMethod.jl?
26157

@@ -39,22 +170,140 @@ replacing exact QR iterations with a direct method that reorders the Schur form.
39170
In fact we see Krylov-Schur just as an implementation detail of the Arnoldi
40171
method.
41172

42-
## What problems does this package solve specifically?
43173

44-
By design the Arnoldi method is best at finding eigenvalues on the boundary of
45-
the convex hull of eigenvalues. For instance eigenvalues of largest modulus and
46-
largest or smallest real part. In the case of complex matrices one can target
47-
eigenvalues of largest and smallest imaginary part as well.
174+
## Bringing problems to standard form
175+
176+
ArnoldiMethod.jl by default can only compute an approximate, partial Schur decomposition
177+
$AQ = QR$ and (from there) a partial eigendecomposition $AX = XD$ of a matrix $A$, for
178+
*extremal* eigenvalues $d_{ii}$. These are eigenvalues at the boundary of the convex
179+
hull of the spectrum of $A$. Search targets for eigenvalues are: large magnitude, and
180+
large or small real or imaginary parts.
181+
182+
Whenever one targets eigenvalues close to a specific point in the complex plane,
183+
or whenever one solves generalized eigenvalue problems, suitable transformations
184+
will enable you to recast the problem into something that ArnoldiMethod.jl can
185+
handle well. In this section we provide some examples.
186+
187+
### Shift-and-invert with LinearMaps.jl
188+
To find eigenvalues closest to the origin of $A$, one can find the eigenvalues
189+
of largest magnitude of $A^{-1}$. [LinearMaps.jl](https://github.com/Jutho/LinearMaps.jl)
190+
is a neat way to implement this.
191+
192+
```julia
193+
using ArnoldiMethod, LinearAlgebra, LinearMaps
194+
195+
# Define a matrix whose eigenvalues you want
196+
A = rand(100,100)
197+
198+
# Factorizes A and builds a linear map that applies inv(A) to a vector.
199+
function construct_linear_map(A)
200+
F = factorize(A)
201+
LinearMap{eltype(A)}((y, x) -> ldiv!(y, F, x), size(A,1), ismutating=true)
202+
end
203+
204+
# Target the largest eigenvalues of the inverted problem
205+
decomp, = partialschur(construct_linear_map(A), nev=4, tol=1e-5, restarts=100, which=:LM)
206+
λs_inv, X = partialeigen(decomp)
207+
208+
# Eigenvalues have to be inverted to find the smallest eigenvalues of the non-inverted problem.
209+
λs = 1 ./ λs_inv
210+
211+
# Show that Ax = xλ
212+
@show norm(A * X - X * Diagonal(λs)) # 7.38473677258669e-6
213+
```
214+
215+
### Shift-and-invert for generalized eigenvalue problems
216+
When targeting the eigenvalues closest to the origin of a generalized eigenvalue
217+
problem $Ax = Bx\lambda$, one can apply the shift-and-invert trick, recasting
218+
the problem to $A^{-1}Bx = x\theta$ where $\lambda = 1 / \theta$.
219+
220+
```julia
221+
using ArnoldiMethod, LinearAlgebra, LinearMaps
222+
223+
# Define the matrices of the generalized eigenvalue problem
224+
A, B = rand(100, 100), rand(100, 100)
48225

49-
The scope is much broader though, since there is a whole zoo of spectral
50-
transformations possible to find for instance interior eigenvalues or
51-
eigenvalues closest to the imaginary axis.
226+
struct ShiftAndInvert{TA,TB,TT}
227+
A_lu::TA
228+
B::TB
229+
temp::TT
230+
end
52231

53-
Further, one can solve generalized eigenvalue problems $Ax = Bx \lambda$ by
54-
applying a suitable spectral transformation as well. Also quadratic eigenvalue
55-
problems can be casted to standard form.
232+
function (M::ShiftAndInvert)(y, x)
233+
mul!(M.temp, M.B, x)
234+
ldiv!(y, M.A_lu, M.temp)
235+
end
236+
237+
function construct_linear_map(A, B)
238+
a = ShiftAndInvert(factorize(A), B, Vector{eltype(A)}(undef, size(A, 1)))
239+
LinearMap{eltype(A)}(a, size(A, 1), ismutating = true)
240+
end
241+
242+
# Target the largest eigenvalues of the inverted problem
243+
decomp, = partialschur(
244+
construct_linear_map(A, B),
245+
nev = 4,
246+
tol = 1e-5,
247+
restarts = 100,
248+
which = :LM,
249+
)
250+
λs_inv, X = partialeigen(decomp)
251+
252+
# Eigenvalues have to be inverted to find the smallest eigenvalues of the non-inverted problem.
253+
λs = 1 ./ λs_inv
254+
255+
# Show that Ax = Bxλ
256+
@show norm(A * X - B * X * Diagonal(λs)) # 2.8043149027575927e-6
257+
```
258+
259+
### Largest eigenvalues of a generalized eigenvalue problem with symmetric positive-definite B
260+
261+
When $B$ is a symmetric positive-definite matrix, and it's affordable to compute a Cholesky
262+
decomposition of $B$, one can use ArnoldiMethod.jl to create a partial Schur decomposition of
263+
$A$ where the Schur vectors are $B$-orthonormal:
264+
265+
Solve $Q^*AQ = R$ where $Q^*BQ = I$ and $R$ is upper triangular. If $A = A^*$ as well, then
266+
$R$ is diagonal and we have a partial eigendecomposition of $A$.
267+
268+
First we take the Cholesky decomposition $B = LL^*$ and plug it into $AQ = BQR$ to obtain
269+
$L^{-*}AL^{-1}L^*Q = L^*QR$.
270+
271+
Then define $C = L^{-*}AL^{-1}$ and $Y = L^*Q$ and we have a standard Schur decomposition
272+
$CY = YR$ which we can solve using `partialschur`.
273+
274+
The linear map $C$ can be defined as follows:
275+
276+
```julia
277+
using ArnoldiMethod, LinearAlgebra, LinearMaps
278+
struct WithBInnerProduct{TA,TL}
279+
A::TA
280+
L::TL
281+
end
282+
283+
function (M::WithBInnerProduct)(y, x)
284+
# Julia unfortunately does not have in-place CHOLMOD solve, so this is far from optimal.
285+
tmp = M.L \ x
286+
mul!(y, M.A, tmp)
287+
y .= M.L' \ y
288+
return y
289+
end
290+
291+
# Define the matrices of the generalized eigenvalue problem
292+
A = rand(100, 100)
293+
B = Diagonal(range(1.0, 2.0, length = 100))
294+
295+
# Reformulate the problem as standard Schur decomposition
296+
F = cholesky(B)
297+
linear_map = LinearMap{eltype(A)}(WithBInnerProduct(A, F.L), size(A, 1), ismutating = true)
298+
decomp, info = partialschur(linear_map, nev = 4, which = :LM, tol = 1e-10)
299+
300+
# Translate back to the original problem
301+
Q = F.L' \ decomp.Q
302+
303+
@show norm(Q' * A * Q - decomp.R) # 3.883933945390996e-14
304+
@show norm(Q' * B * Q - I) # 3.1672155003480583e-15
305+
```
56306

57-
See [Theory](@ref theory) for more information.
58307

59308
## Goal of this package: an efficient, pure Julia implementation
60309
This project started with two goals:
@@ -68,6 +317,7 @@ This project started with two goals:
68317
achieved before the package was stable enough, since ARPACK moved to a
69318
separate repository [Arpack.jl](https://github.com/JuliaLinearAlgebra/Arpack.jl/).
70319

320+
71321
## Performance
72322

73323
ArnoldiMethod.jl should be roughly on par with Arpack.jl, and slightly faster than
@@ -77,6 +327,7 @@ Do note that for an apples to apples comparison, it's important to compare with
77327
identical defaults: each of the mentioned packages uses a slightly different default
78328
convergence criterion.
79329

330+
80331
## Status
81332
An overview of what we have, how it's done and what we're missing.
82333

@@ -106,7 +357,7 @@ An overview of what we have, how it's done and what we're missing.
106357
original orthonormal basis. This is not in-place in `V`, so we allocate a bit
107358
of scratch space ahead of time.
108359

109-
### Not implemented (yet) and future ideas
360+
### Not implemented
110361
- Being able to kickstart the method from a given Arnoldi relation. This also
111362
captures:
112363
1. Making an initial guess by providing a known approximate eigenvector;

0 commit comments

Comments
 (0)