Skip to content

Commit 3cb957d

Browse files
Support starting vec (#141)
Adds a keyword arg `partialschur(...; v1)` which the vector that induces the Krylov subspace. Co-authored-by: Samuel Powell <samuelpowell@users.noreply.github.com>
1 parent 55a09aa commit 3cb957d

6 files changed

Lines changed: 81 additions & 38 deletions

File tree

src/ArnoldiMethod.jl

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,45 @@ using Base: RefValue, OneTo
88
export partialschur, LM, SR, LR, SI, LI, partialeigen
99

1010
"""
11-
Arnoldi(n, k) → Arnoldi
11+
ArnoldiWorkspace(n, k) → ArnoldiWorkspace
1212
13-
Pre-allocated Arnoldi relation of the Vₖ₊₁ and Hₖ matrices that satisfy
14-
A * Vₖ = Vₖ₊₁ * Hₖ, where Vₖ₊₁ is orthonormal of size n × (k+1) and Hₖ upper
15-
Hessenberg of size (k+1) × k. The constructor will just allocate sufficient
16-
space, but will *not* initialize the first vector of `v₁`. For the latter see
17-
`reinitialize!`.
13+
Holds the large arrays for the Arnoldi relation: Vₖ₊₁ and Hₖ are matrices that
14+
satisfy A * Vₖ = Vₖ₊₁ * Hₖ, where Vₖ₊₁ is orthonormal of size n × (k+1) and Hₖ upper
15+
Hessenberg of size (k+1) × k. The matrices V_tmp and Q are used for restarts, and
16+
have similar size as Vₖ₊₁ and Hₖ (but Q is k × k, not k+1 × k).
1817
"""
19-
struct Arnoldi{T,TV<:StridedMatrix{T},TH<:StridedMatrix{T}}
18+
struct ArnoldiWorkspace{T,TV<:AbstractMatrix{T},TH<:AbstractMatrix{T}}
19+
# Orthonormal basis of the Krylov subspace.
2020
V::TV
21+
22+
# The Hessenberg matrix of the Arnoldi relation.
2123
H::TH
2224

23-
function Arnoldi{T}(matrix_order::Int, krylov_dimension::Int) where {T}
25+
# Temporary matrix similar to V, used to restart.
26+
V_tmp::TV
27+
28+
# Unitary matrix size of (square) H to do a change of basis.
29+
Q::TH
30+
31+
function ArnoldiWorkspace(::Type{T}, matrix_order::Int, krylov_dimension::Int) where {T}
32+
# Without an initial vector.
2433
krylov_dimension <= matrix_order ||
2534
throw(ArgumentError("Krylov dimension should be less than matrix order."))
2635
V = Matrix{T}(undef, matrix_order, krylov_dimension + 1)
36+
V_tmp = similar(V)
2737
H = zeros(T, krylov_dimension + 1, krylov_dimension)
28-
return new{T,typeof(V),typeof(H)}(V, H)
38+
Q = similar(H, krylov_dimension, krylov_dimension)
39+
return new{T,typeof(V),typeof(H)}(V, H, V_tmp, Q)
40+
end
41+
42+
function ArnoldiWorkspace(v1::AbstractVector{T}, krylov_dimension::Int) where {T}
43+
# From an initial vector v1.
44+
V = similar(v1, length(v1), krylov_dimension + 1)
45+
V_tmp = similar(V)
46+
H = similar(V, krylov_dimension + 1, krylov_dimension)
47+
fill!(H, zero(eltype(H)))
48+
Q = similar(H, krylov_dimension, krylov_dimension)
49+
return new{T,typeof(V),typeof(H)}(V, H, V_tmp, Q)
2950
end
3051
end
3152

src/expansion.jl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@ Generate a random `j+1`th column orthonormal against V[:,1:j]
99
Returns true if the column is a valid new basis vector.
1010
Returns false if the column is numerically in the span of the previous vectors.
1111
"""
12-
function reinitialize!(arnoldi::Arnoldi{T}, j::Int = 0) where {T}
12+
function reinitialize!(
13+
arnoldi::ArnoldiWorkspace{T},
14+
j::Int = 0,
15+
populate! = rand!,
16+
) where {T}
1317
V = arnoldi.V
1418
v = view(V, :, j + 1)
1519

1620
# Generate a new random column
17-
rand!(v)
21+
populate!(v)
1822

1923
# Norm before orthogonalization
2024
rnorm = norm(v)
@@ -62,7 +66,7 @@ Orthogonalize arnoldi.V[:, j+1] against arnoldi.V[:, 1:j].
6266
Returns true if the column is a valid new basis vector.
6367
Returns false if the column is numerically in the span of the previous vectors.
6468
"""
65-
function orthogonalize!(arnoldi::Arnoldi{T}, j::Integer) where {T}
69+
function orthogonalize!(arnoldi::ArnoldiWorkspace{T}, j::Integer) where {T}
6670
V = arnoldi.V
6771
H = arnoldi.H
6872

@@ -109,7 +113,7 @@ end
109113
110114
Perform Arnoldi from `from` to `to`.
111115
"""
112-
function iterate_arnoldi!(A, arnoldi::Arnoldi{T}, range::UnitRange{Int}) where {T}
116+
function iterate_arnoldi!(A, arnoldi::ArnoldiWorkspace{T}, range::UnitRange{Int}) where {T}
113117
V, H = arnoldi.V, arnoldi.H
114118

115119
for j in range

src/run.jl

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ end
1313

1414
"""
1515
```julia
16-
partialschur(A; nev, which, tol, mindim, maxdim, restarts) → PartialSchur, History
16+
partialschur(A; v1, workspace, nev, which, tol, mindim, maxdim, restarts) → PartialSchur, History
1717
```
1818
1919
Find `nev` approximate eigenpairs of `A` with eigenvalues near a specified target.
@@ -33,6 +33,7 @@ The most important keyword arguments:
3333
| `nev` | `Int` | `min(6, size(A, 1))` |Number of eigenvalues |
3434
| `which` | `Symbol` or `Target` | `:LM` | One of `:LM`, `:LR`, `:SR`, `:LI`, `:SI`, see below. |
3535
| `tol` | `Real` | `√eps` | Tolerance for convergence: ‖Ax - xλ‖₂ < tol * ‖λ‖ |
36+
| `v1` | `AbstractVector` | `nothing` | Optional starting vector for the Krylov subspace |
3637
3738
The target `which` can be any of:
3839
@@ -93,6 +94,7 @@ is usually about two times `mindim`.
9394
"""
9495
function partialschur(
9596
A;
97+
v1::Union{AbstractVector,Nothing} = nothing,
9698
nev::Int = min(6, size(A, 1)),
9799
which::Union{Target,Symbol} = LM(),
98100
tol::Real = sqrt(eps(real(vtype(A)))),
@@ -101,14 +103,24 @@ function partialschur(
101103
restarts::Int = 200,
102104
)
103105
s = checksquare(A)
104-
if nev < 1
105-
throw(ArgumentError("nev cannot be less than 1"))
106-
end
106+
nev < 1 && throw(ArgumentError("nev cannot be less than 1"))
107107
nev mindim maxdim s || throw(
108-
ArgumentError("nev ≤ mindim ≤ maxdim does not hold, got $nev$mindim$maxdim"),
108+
ArgumentError(
109+
"nev ≤ mindim ≤ maxdim ≤ size(A, 1) does not hold, got $nev$mindim$maxdim$s",
110+
),
109111
)
110112
_which = which isa Target ? which : _symbol_to_target(which)
111-
_partialschur(A, vtype(A), mindim, maxdim, nev, tol, restarts, _which)
113+
114+
if v1 === nothing
115+
arnoldi = ArnoldiWorkspace(vtype(A), size(A, 1), maxdim)
116+
reinitialize!(arnoldi, 0, v -> rand!(v))
117+
else
118+
length(v1) == size(A, 1) ||
119+
throw(ArgumentError("v1 should have the same dimension as A"))
120+
arnoldi = ArnoldiWorkspace(v1, maxdim)
121+
reinitialize!(arnoldi, 0, v -> copyto!(v, v1))
122+
end
123+
_partialschur(A, arnoldi, mindim, maxdim, nev, tol, restarts, _which)
112124
end
113125

114126
_symbol_to_target(sym::Symbol) =
@@ -156,33 +168,25 @@ end
156168

157169
function _partialschur(
158170
A,
159-
::Type{T},
171+
arnoldi::ArnoldiWorkspace{T},
160172
mindim::Int,
161173
maxdim::Int,
162174
nev::Int,
163175
tol::Ttol,
164176
restarts::Int,
165177
which::Target,
166178
) where {T,Ttol<:Real}
167-
n = size(A, 1)
168-
169-
# Pre-allocated Arnoldi decomp
170-
arnoldi = Arnoldi{T}(n, maxdim)
171-
172179
# Unpack for convenience
173180
H = arnoldi.H
174181
V = arnoldi.V
182+
Vtmp = arnoldi.V_tmp
183+
Q = arnoldi.Q
175184

176-
# For a change of basis we have Vtmp as working space
177-
Vtmp = Matrix{T}(undef, n, maxdim)
178-
179-
# Unitary matrix used for change of basis of V.
180-
Q = Matrix{T}(undef, maxdim, maxdim)
181-
182-
# We only need to store one eignvector of the Hessenberg matrix
185+
# We only need to store one eigenvector of the Hessenberg matrix.
183186
x = zeros(complex(T), maxdim)
184187

185188
# And we store the reflector to transform H back to Hessenberg separately
189+
# TODO: can be in-place in H now.
186190
G = Reflector{T}(maxdim)
187191

188192
# Approximate residual norms for all Ritz values, and Ritz values
@@ -205,7 +209,6 @@ function _partialschur(
205209
prods = mindim
206210

207211
# Initialize an Arnoldi relation of size `mindim`
208-
reinitialize!(arnoldi)
209212
iterate_arnoldi!(A, arnoldi, 1:mindim)
210213

211214
for iter = 1:restarts

test/expansion.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Tests the Arnoldi relation AV = VH when expanding the search subspace
22

33
using Test, LinearAlgebra, SparseArrays
4-
using ArnoldiMethod: reinitialize!, Arnoldi, iterate_arnoldi!
4+
using ArnoldiMethod: reinitialize!, ArnoldiWorkspace, iterate_arnoldi!
55

66
@testset "Initialization" begin
7-
arnoldi = Arnoldi{Float64}(5, 3)
7+
arnoldi = ArnoldiWorkspace(Float64, 5, 3)
88
reinitialize!(arnoldi)
99
@test norm(arnoldi.V[:, 1]) 1
1010
end
@@ -15,7 +15,7 @@ end
1515
for T in (Float64, BigFloat)
1616
A = sprand(T, n, n, 0.1) + I
1717

18-
arnoldi = Arnoldi{T}(n, max)
18+
arnoldi = ArnoldiWorkspace(T, n, max)
1919
reinitialize!(arnoldi)
2020
V, H = arnoldi.V, arnoldi.H
2121

@@ -40,7 +40,7 @@ end
4040
]
4141

4242
# and an initial vector [1; 0; ... 0]
43-
vh = Arnoldi{T}(8, 5)
43+
vh = ArnoldiWorkspace(T, 8, 5)
4444
V, H = vh.V, vh.H
4545
V[:, 1] .= zero(T)
4646
V[1, 1] = one(T)

test/householder.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ end
7070
Q = qr(randn(T, k, k)).Q * Matrix(1.0I, k, k)
7171

7272
A = rand(T, n, n)
73-
arn = Arnoldi{T}(n, k)
73+
arn = ArnoldiWorkspace(T, n, k)
7474
reinitialize!(arn)
7575
iterate_arnoldi!(A, arn, 1:k)
7676

test/partial_schur.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ end
6161
@test_throws ArgumentError partialschur(A, nev = 10)
6262
end
6363

64+
65+
@testset "Eigenvector as initial vector is not problematic" begin
66+
A = rand(30, 30)
67+
A += A'
68+
λs, X = eigen(Symmetric(A)) # ensure real eigenvectors
69+
70+
λ, x = λs[end], X[:, end]
71+
decomp, history = partialschur(A, v1 = x, nev = 2, tol = 1e-8)
72+
73+
@test history.converged
74+
@test norm(A * decomp.Q - decomp.Q * decomp.R) < 1e-7
75+
@test abs(maximum(real(decomp.eigenvalues)) - λ) < 1e-7
76+
end
77+
78+
6479
@testset "Target non-dominant eigenvalues" begin
6580
# Dominant eigenvalues 50, 51, 52, 53, but we target the smallest real part
6681
A = Diagonal([1:0.1:10; 50:53])

0 commit comments

Comments
 (0)