@@ -14,13 +14,144 @@ or non-symmetric and either real or complex.
14
14
The method is * matrix-free* , meaning that it only requires multiplication with
15
15
the matrix $A$.
16
16
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
+
17
31
The package exports just two functions:
18
32
- [ ` partialschur ` ] ( @ref ) to compute a stable basis for an eigenspace;
19
33
- [ ` partialeigen ` ] ( @ref ) to compute an eigendecomposition from a partial Schur
20
34
decomposition.
21
35
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
+ ```
24
155
25
156
## What algorithm is ArnoldiMethod.jl?
26
157
@@ -39,22 +170,140 @@ replacing exact QR iterations with a direct method that reorders the Schur form.
39
170
In fact we see Krylov-Schur just as an implementation detail of the Arnoldi
40
171
method.
41
172
42
- ## What problems does this package solve specifically?
43
173
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 )
48
225
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
52
231
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
+ ```
56
306
57
- See [ Theory] (@ref theory) for more information.
58
307
59
308
## Goal of this package: an efficient, pure Julia implementation
60
309
This project started with two goals:
@@ -68,6 +317,7 @@ This project started with two goals:
68
317
achieved before the package was stable enough, since ARPACK moved to a
69
318
separate repository [ Arpack.jl] ( https://github.com/JuliaLinearAlgebra/Arpack.jl/ ) .
70
319
320
+
71
321
## Performance
72
322
73
323
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
77
327
identical defaults: each of the mentioned packages uses a slightly different default
78
328
convergence criterion.
79
329
330
+
80
331
## Status
81
332
An overview of what we have, how it's done and what we're missing.
82
333
@@ -106,7 +357,7 @@ An overview of what we have, how it's done and what we're missing.
106
357
original orthonormal basis. This is not in-place in ` V ` , so we allocate a bit
107
358
of scratch space ahead of time.
108
359
109
- ### Not implemented (yet) and future ideas
360
+ ### Not implemented
110
361
- Being able to kickstart the method from a given Arnoldi relation. This also
111
362
captures:
112
363
1 . Making an initial guess by providing a known approximate eigenvector;
0 commit comments