Skip to content

Instantly share code, notes, and snippets.

@Jutho
Created August 4, 2025 21:40
Show Gist options
  • Save Jutho/fe2f09f8961513761f92e46304fee73f to your computer and use it in GitHub Desktop.
Save Jutho/fe2f09f8961513761f92e46304fee73f to your computer and use it in GitHub Desktop.
# 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