Created
August 4, 2025 21:40
-
-
Save Jutho/fe2f09f8961513761f92e46304fee73f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# gkl.jl | |
""" | |
mutable struct GKLFactorization{TU,TV,S<:Real} | |
Structure to store a Golub-Kahan-Lanczos (GKL) bidiagonal factorization of a linear map `A` | |
of the form | |
```julia | |
A * V = U * B + r * b' | |
A' * U = V * B' | |
``` | |
For a given GKL factorization `fact` of length `k = length(fact)`, the two bases `U` and `V` | |
are obtained via [`basis(fact, Val(:U))`](@ref basis) and `basis(fact, Val(:V))`. Here, | |
`U` and `V` are instances of [`OrthonormalBasis{T}`](@ref Basis), with also | |
`length(U) == length(V) == k` and where `T` denotes the type of vector like objects used in | |
the problem. The Rayleigh quotient `B` is obtained as [`rayleighquotient(fact)`](@ref) and | |
is of type `Bidiagonal{S<:Number}` with `size(B) == (k,k)`. The residual `r` is | |
obtained as [`residual(fact)`](@ref) and is of type `T`. One can also query | |
[`normres(fact)`](@ref) to obtain `norm(r)`, the norm of the residual. The vector `b` has no | |
dedicated name but can be obtained via [`rayleighextension(fact)`](@ref). It takes the | |
default value ``e_k``, i.e. the unit vector of all zeros and a one in the last entry, which | |
is represented using [`SimpleBasisVector`](@ref). | |
A GKL factorization `fact` can be destructured as `U, V, B, r, nr, b = fact` with | |
`nr = norm(r)`. | |
`GKLFactorization` is mutable because it can [`expand!`](@ref) or [`shrink!`](@ref). | |
See also [`GKLIterator`](@ref) for an iterator that constructs a progressively expanding | |
GKL factorizations of a given linear map and a starting vector `u₀`. | |
""" | |
mutable struct GKLFactorization{TU, TV, S <: Real} | |
k::Int # current Krylov dimension | |
U::OrthonormalBasis{TU} # basis of length k | |
V::OrthonormalBasis{TV} # basis of length k | |
αs::Vector{S} | |
βs::Vector{S} | |
r::TU | |
end | |
Base.length(F::GKLFactorization) = F.k | |
Base.sizehint!(F::GKLFactorization, n) = begin | |
sizehint!(F.U, n) | |
sizehint!(F.V, n) | |
sizehint!(F.αs, n) | |
sizehint!(F.βs, n) | |
return F | |
end | |
Base.eltype(F::GKLFactorization) = eltype(typeof(F)) | |
Base.eltype(::Type{<:GKLFactorization{<:Any, <:Any, S}}) where {S} = S | |
# iteration for destructuring into components | |
Base.iterate(F::GKLFactorization) = (basis(F, Val(:U)), Val(:V)) | |
Base.iterate(F::GKLFactorization, ::Val{:V}) = (basis(F, Val(:V)), Val(:rayleighquotient)) | |
function Base.iterate(F::GKLFactorization, ::Val{:rayleighquotient}) | |
return (rayleighquotient(F), Val(:residual)) | |
end | |
Base.iterate(F::GKLFactorization, ::Val{:residual}) = (residual(F), Val(:normres)) | |
Base.iterate(F::GKLFactorization, ::Val{:normres}) = (normres(F), Val(:rayleighextension)) | |
function Base.iterate(F::GKLFactorization, ::Val{:rayleighextension}) | |
return (rayleighextension(F), Val(:done)) | |
end | |
Base.iterate(F::GKLFactorization, ::Val{:done}) = nothing | |
""" | |
basis(fact::GKLFactorization, ::Val{which}) | |
Return the list of basis vectors of a [`GKLFactorization`](@ref), where `which` should take | |
the value `:U` or `:V` and indicates which set of basis vectors (in the domain or in the | |
codomain of the corresponding linear map) should be returned. The return type is an | |
`OrthonormalBasis{T}`, where `T` represents the type of the vectors used by the problem. | |
""" | |
function basis(F::GKLFactorization, ::Val{UV}) where {UV} | |
length(F.U) == F.k || error("Not keeping vectors during GKL bidiagonalization") | |
UV == :U || UV == :V || error("invalid flag for specifying basis") | |
return UV == :U ? F.U : F.V | |
end | |
function rayleighquotient(F::GKLFactorization) | |
return Bidiagonal(view(F.αs, 1:(F.k)), view(F.βs, 1:(F.k - 1)), :L) | |
end | |
residual(F::GKLFactorization) = F.r | |
@inbounds normres(F::GKLFactorization) = F.βs[F.k] | |
rayleighextension(F::GKLFactorization) = SimpleBasisVector(F.k, F.k) | |
# GKL iteration for constructing the orthonormal basis of a Krylov subspace. | |
""" | |
struct GKLIterator{F,TU,O<:Orthogonalizer} | |
GKLIterator(f, u₀, [orth::Orthogonalizer = KrylovDefaults.orth, keepvecs::Bool = true]) | |
Iterator that takes a general linear map `f::F` and an initial vector `u₀::TU` and generates | |
an expanding `GKLFactorization` thereof. In particular, `GKLIterator` implements the | |
[Golub-Kahan-Lanczos bidiagonalization procedure](http://www.netlib.org/utk/people/JackDongarra/etemplates/node198.html). | |
Note, however, that this implementation starts from a vector `u₀` in the codomain of the | |
linear map `f`, which will end up (after normalisation) as the first column of `U`. | |
The argument `f` can be a matrix, a tuple of two functions where the first represents the | |
normal action and the second the adjoint action, or a function accepting two arguments, | |
where the first argument is the vector to which the linear map needs to be applied, and the | |
second argument is either `Val(false)` for the normal action and `Val(true)` for the adjoint | |
action. Note that the flag is thus a `Val` type to allow for type stability in cases where | |
the vectors in the domain and the codomain of the linear map have a different type. | |
The optional argument `orth` specifies which [`Orthogonalizer`](@ref) to be used. The | |
default value in [`KrylovDefaults`](@ref) is to use [`ModifiedGramSchmidtIR`](@ref), which | |
possibly uses reorthogonalization steps. | |
When iterating over an instance of `GKLIterator`, the values being generated are | |
instances `fact` of [`GKLFactorization`](@ref), which can be immediately destructured into a | |
[`basis(fact, Val(:U))`](@ref), [`basis(fact, Val(:V))`](@ref), [`rayleighquotient`](@ref), | |
[`residual`](@ref), [`normres`](@ref) and [`rayleighextension`](@ref), for example as | |
```julia | |
for (U, V, B, r, nr, b) in GKLIterator(f, u₀) | |
# do something | |
nr < tol && break # a typical stopping criterion | |
end | |
``` | |
Since the iterator does not know the dimension of the underlying vector space of | |
objects of type `T`, it keeps expanding the Krylov subspace until the residual norm `nr` | |
falls below machine precision `eps(typeof(nr))`. | |
The internal state of `GKLIterator` is the same as the return value, i.e. the corresponding | |
`GKLFactorization`. However, as Julia's Base iteration interface (using `Base.iterate`) | |
requires that the state is not mutated, a `deepcopy` is produced upon every next iteration | |
step. | |
Instead, you can also mutate the `GKLFactorization` in place, using the following | |
interface, e.g. for the same example above | |
```julia | |
iterator = GKLIterator(f, u₀) | |
factorization = initialize(iterator) | |
while normres(factorization) > tol | |
expand!(iterator, factorization) | |
U, V, B, r, nr, b = factorization | |
# do something | |
end | |
``` | |
Here, [`initialize(::GKLIterator)`](@ref) produces the first GKL factorization of length 1, | |
and `expand!(::GKLIterator, ::GKLFactorization)`(@ref) expands the factorization in place. | |
See also [`initialize!(::GKLIterator, ::GKLFactorization)`](@ref) to initialize in an | |
already existing factorization (most information will be discarded) and | |
[`shrink!(::GKLIterator, k)`](@ref) to shrink an existing factorization down to length `k`. | |
""" | |
struct GKLIterator{F, TU, O <: Orthogonalizer} | |
operator::F | |
u₀::TU | |
orth::O | |
keepvecs::Bool | |
function GKLIterator{F, TU, O}( | |
operator::F, u₀::TU, orth::O, keepvecs::Bool | |
) where {F, TU, O <: Orthogonalizer} | |
if !keepvecs && isa(orth, Reorthogonalizer) | |
error("Cannot use reorthogonalization without keeping all Krylov vectors") | |
end | |
return new{F, TU, O}(operator, u₀, orth, keepvecs) | |
end | |
end | |
function GKLIterator( | |
operator::F, u₀::TU, orth::O = KrylovDefaults.orth, keepvecs::Bool = true | |
) where {F, TU, O <: Orthogonalizer} | |
return GKLIterator{F, TU, O}(operator, u₀, orth, keepvecs) | |
end | |
Base.IteratorSize(::Type{<:GKLIterator}) = Base.SizeUnknown() | |
Base.IteratorEltype(::Type{<:GKLIterator}) = Base.EltypeUnknown() | |
function Base.iterate(iter::GKLIterator) | |
state = initialize(iter) | |
return state, state | |
end | |
function Base.iterate(iter::GKLIterator, state::GKLFactorization) | |
nr = normres(state) | |
if nr < eps(typeof(nr)) | |
return nothing | |
else | |
state = expand!(iter, deepcopy(state)) | |
return state, state | |
end | |
end | |
function initialize(iter::GKLIterator; verbosity::Int = KrylovDefaults.verbosity[]) | |
# initialize without using eltype | |
u₀ = iter.u₀ | |
β₀ = norm(u₀) | |
iszero(β₀) && throw(ArgumentError("initial vector should not have norm zero")) | |
v₀ = apply_adjoint(iter.operator, u₀) | |
α = norm(v₀) / β₀ | |
Av₀ = apply_normal(iter.operator, v₀) # apply operator | |
α² = inner(u₀, Av₀) / β₀^2 | |
α² ≈ α * α || throw(ArgumentError("operator and its adjoint are not compatible")) | |
T = typeof(α²) # scalar type of the Rayleigh quotient | |
# these lines determines the vector types that we will henceforth use | |
u = scale(u₀, one(T) / β₀) | |
v = scale(v₀, one(T) / (α * β₀)) | |
if typeof(Av₀) == typeof(u) | |
r = scale!!(Av₀, 1 / (α * β₀)) | |
else | |
r = scale!!(zerovector(u), Av₀, 1 / (α * β₀)) | |
end | |
r = add!!(r, u, -α) | |
β = norm(r) | |
U = OrthonormalBasis([u]) | |
V = OrthonormalBasis([v]) | |
S = real(T) | |
αs = S[α] | |
βs = S[β] | |
if verbosity > EACHITERATION_LEVEL | |
@info "GKL initiation at dimension 1: subspace normres = $(normres2string(β))" | |
end | |
return GKLFactorization(1, U, V, αs, βs, r) | |
end | |
function initialize!( | |
iter::GKLIterator, state::GKLFactorization; | |
verbosity::Int = KrylovDefaults.verbosity[] | |
) | |
U = state.U | |
while length(U) > 1 | |
pop!(U) | |
end | |
V = empty!(state.V) | |
αs = empty!(state.αs) | |
βs = empty!(state.βs) | |
u = scale!!(U[1], iter.u₀, 1 / norm(iter.u₀)) | |
v = apply_adjoint(iter.operator, u) | |
α = norm(v) | |
v = scale!!(v, inv(α)) | |
r = apply_normal(iter.operator, v) | |
r = add!!(r, u, -α) | |
β = norm(r) | |
state.k = 1 | |
push!(V, v) | |
push!(αs, α) | |
push!(βs, β) | |
state.r = r | |
if verbosity > EACHITERATION_LEVEL | |
@info "GKL initiation at dimension 1: subspace normres = $(normres2string(β))" | |
end | |
return state | |
end | |
function expand!( | |
iter::GKLIterator, state::GKLFactorization; | |
verbosity::Int = KrylovDefaults.verbosity[] | |
) | |
βold = normres(state) | |
U = state.U | |
V = state.V | |
r = state.r | |
U = push!(U, scale!!(r, 1 / βold)) | |
v, r, α, β = gklrecurrence(iter.operator, U, V, βold, iter.orth) | |
push!(V, v) | |
push!(state.αs, α) | |
push!(state.βs, β) | |
#!iter.keepvecs && popfirst!(state.V) # remove oldest V if not keepvecs | |
state.k += 1 | |
state.r = r | |
if verbosity > EACHITERATION_LEVEL | |
@info "GKL expension to dimension $(state.k): subspace normres = $(normres2string(β))" | |
end | |
return state | |
end | |
function shrink!(state::GKLFactorization, k; verbosity::Int = KrylovDefaults.verbosity[]) | |
length(state) == length(state.V) || | |
error("we cannot shrink GKLFactorization without keeping vectors") | |
length(state) <= k && return state | |
U = state.U | |
V = state.V | |
while length(V) > k + 1 | |
pop!(U) | |
pop!(V) | |
end | |
pop!(V) | |
r = pop!(U) | |
resize!(state.αs, k) | |
resize!(state.βs, k) | |
state.k = k | |
β = normres(state) | |
if verbosity > EACHITERATION_LEVEL | |
@info "GKL reduction to dimension $k: subspace normres = $(normres2string(β))" | |
end | |
state.r = scale!!(r, β) | |
return state | |
end | |
# Golub-Kahan-Lanczos recurrence relation | |
function gklrecurrence( | |
operator, U::OrthonormalBasis, V::OrthonormalBasis, | |
β, orth::Union{ClassicalGramSchmidt, ModifiedGramSchmidt} | |
) | |
u = U[end] | |
v = apply_adjoint(operator, u) | |
v = add!!(v, V[end], -β) | |
α = norm(v) | |
v = scale!!(v, inv(α)) | |
r = apply_normal(operator, v) | |
r = add!!(r, u, -α) | |
β = norm(r) | |
return v, r, α, β | |
end | |
function gklrecurrence( | |
operator, | |
U::OrthonormalBasis, | |
V::OrthonormalBasis, | |
β, | |
orth::ClassicalGramSchmidt2 | |
) | |
u = U[end] | |
v = apply_adjoint(operator, u) | |
v = add!!(v, V[end], -β) # not necessary if we definitely reorthogonalize next step and previous step | |
# v, = orthogonalize!(v, V, ClassicalGramSchmidt()) | |
α = norm(v) | |
v = scale!!(v, inv(α)) | |
r = apply_normal(operator, v) | |
r = add!!(r, u, -α) | |
r, = orthogonalize!!(r, U, ClassicalGramSchmidt()) | |
β = norm(r) | |
return v, r, α, β | |
end | |
function gklrecurrence( | |
operator, | |
U::OrthonormalBasis, | |
V::OrthonormalBasis, | |
β, | |
orth::ModifiedGramSchmidt2 | |
) | |
u = U[end] | |
v = apply_adjoint(operator, u) | |
v = add!!(v, V[end], -β) | |
for q in V | |
# it is claimed that this is not necessary if we definitely | |
# reorthogonalize next step and previous step, but some of | |
# my experiments show that this does make a significant difference | |
v, = orthogonalize!!(v, q, ModifiedGramSchmidt()) | |
end | |
α = norm(v) | |
v = scale!!(v, inv(α)) | |
r = apply_normal(operator, v) | |
r = add!!(r, u, -α) | |
for q in U | |
r, = orthogonalize!!(r, q, ModifiedGramSchmidt()) | |
end | |
β = norm(r) | |
return v, r, α, β | |
end | |
function gklrecurrence( | |
operator, | |
U::OrthonormalBasis, | |
V::OrthonormalBasis, | |
β, | |
orth::ClassicalGramSchmidtIR | |
) | |
u = U[end] | |
v = apply_adjoint(operator, u) | |
v = add!!(v, V[end], -β) | |
α = norm(v) | |
nold = sqrt(abs2(α) + abs2(β)) | |
while α < orth.η * nold | |
nold = α | |
v, = orthogonalize!!(v, V, ClassicalGramSchmidt()) | |
α = norm(v) | |
end | |
v = scale!!(v, inv(α)) | |
r = apply_normal(operator, v) | |
r = add!!(r, u, -α) | |
β = norm(r) | |
nold = sqrt(abs2(α) + abs2(β)) | |
while eps(one(β)) < β < orth.η * nold | |
nold = β | |
r, = orthogonalize!!(r, U, ClassicalGramSchmidt()) | |
β = norm(r) | |
end | |
return v, r, α, β | |
end | |
function gklrecurrence( | |
operator, | |
U::OrthonormalBasis, | |
V::OrthonormalBasis, | |
β, | |
orth::ModifiedGramSchmidtIR | |
) | |
u = U[end] | |
v = apply_adjoint(operator, u) | |
v = add!!(v, V[end], -β) | |
α = norm(v) | |
nold = sqrt(abs2(α) + abs2(β)) | |
while eps(one(α)) < α < orth.η * nold | |
nold = α | |
for q in V | |
v, = orthogonalize!!(v, q, ModifiedGramSchmidt()) | |
end | |
α = norm(v) | |
end | |
v = scale!!(v, inv(α)) | |
r = apply_normal(operator, v) | |
r = add!!(r, u, -α) | |
β = norm(r) | |
nold = sqrt(abs2(α) + abs2(β)) | |
while eps(one(β)) < β < orth.η * nold | |
nold = β | |
for q in U | |
r, = orthogonalize!!(r, q, ModifiedGramSchmidt()) | |
end | |
β = norm(r) | |
end | |
return v, r, α, β | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment