Skip to content

Commit 6b6f674

Browse files
authored
Lanczos-based spectrum method (#476)
1 parent a965621 commit 6b6f674

File tree

7 files changed

+201
-2
lines changed

7 files changed

+201
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased](https://github.com/qutip/QuantumToolbox.jl/tree/main)
99

10+
- Introduce `Lanczos` solver for `spectrum`. ([#476])
11+
1012
## [v0.31.1]
1113
Release date: 2025-05-16
1214

@@ -228,3 +230,4 @@ Release date: 2024-11-13
228230
[#455]: https://github.com/qutip/QuantumToolbox.jl/issues/455
229231
[#456]: https://github.com/qutip/QuantumToolbox.jl/issues/456
230232
[#460]: https://github.com/qutip/QuantumToolbox.jl/issues/460
233+
[#476]: https://github.com/qutip/QuantumToolbox.jl/issues/476

benchmarks/correlations_and_spectrum.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ function benchmark_correlations_and_spectrum!(SUITE)
1919

2020
PI_solver = PseudoInverse()
2121

22+
L_solver = Lanczos()
23+
2224
SUITE["Correlations and Spectrum"]["FFT Correlation"] =
2325
@benchmarkable _calculate_fft_spectrum($H, $t_l, $c_ops, $(a'), $a)
2426

@@ -28,5 +30,8 @@ function benchmark_correlations_and_spectrum!(SUITE)
2830
SUITE["Correlations and Spectrum"]["Spectrum"]["Pseudo Inverse"] =
2931
@benchmarkable spectrum($H, $ω_l, $c_ops, $(a'), $a, solver = $PI_solver)
3032

33+
SUITE["Correlations and Spectrum"]["Spectrum"]["Lanczos"] =
34+
@benchmarkable spectrum($H, $ω_l, $c_ops, $(a'), $a, solver = $L_solver)
35+
3136
return nothing
3237
end

docs/src/resources/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ spectrum_correlation_fft
248248
spectrum
249249
ExponentialSeries
250250
PseudoInverse
251+
Lanczos
251252
```
252253

253254
## [Entropy and Metrics](@id doc-API:Entropy-and-Metrics)

src/QuantumToolbox.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,14 @@ include("time_evolution/time_evolution_dynamical.jl")
115115

116116
# Others
117117
include("correlations.jl")
118-
include("spectrum.jl")
119118
include("wigner.jl")
120119
include("spin_lattice.jl")
121120
include("arnoldi.jl")
122121
include("entropy.jl")
123122
include("metrics.jl")
124123
include("negativity.jl")
125124
include("steadystate.jl")
125+
include("spectrum.jl")
126126
include("visualization.jl")
127127

128128
# deprecated functions

src/spectrum.jl

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export spectrum, spectrum_correlation_fft
2-
export SpectrumSolver, ExponentialSeries, PseudoInverse
2+
export SpectrumSolver, ExponentialSeries, PseudoInverse, Lanczos
33

44
abstract type SpectrumSolver end
55

@@ -26,6 +26,20 @@ end
2626

2727
PseudoInverse(; alg::SciMLLinearSolveAlgorithm = KrylovJL_GMRES()) = PseudoInverse(alg)
2828

29+
@doc raw"""
30+
Lanczos(; tol = 1e-8, maxiter = 5000, verbose = 0)
31+
32+
A solver which solves [`spectrum`](@ref) by using a non-symmetric Lanczos variant of the algorithm in [Koch2011](https://www.cond-mat.de/events/correl11/manuscripts/koch.pdf).
33+
The nonsymmetric Lanczos algorithm is adapted from Algorithm 6.6 in [Saad2011](https://www-users.cse.umn.edu/~saad/eig_book_2ndEd.pdf).
34+
The running estimate is updated via a [Wallis-Euler recursion](https://en.wikipedia.org/wiki/Continued_fraction).
35+
"""
36+
Base.@kwdef struct Lanczos{T<:Real,SS<:Union{Nothing,<:SteadyStateSolver}} <: SpectrumSolver
37+
tol::T = 1e-8
38+
maxiter::Int = 5000
39+
verbose::Int = 0
40+
steadystate_solver::SS = nothing
41+
end
42+
2943
@doc raw"""
3044
spectrum(H::QuantumObject,
3145
ωlist::AbstractVector,
@@ -44,6 +58,7 @@ S(\omega) = \int_{-\infty}^\infty \lim_{t \rightarrow \infty} \left\langle \hat{
4458
See also the following list for `SpectrumSolver` docstrings:
4559
- [`ExponentialSeries`](@ref)
4660
- [`PseudoInverse`](@ref)
61+
- [`Lanczos`](@ref)
4762
"""
4863
function spectrum(
4964
H::QuantumObject{HOpType},
@@ -144,6 +159,142 @@ function _spectrum(
144159
return spec
145160
end
146161

162+
function _spectrum(
163+
L::QuantumObject{SuperOperator},
164+
ωlist::AbstractVector,
165+
A::QuantumObject{Operator},
166+
B::QuantumObject{Operator},
167+
solver::Lanczos,
168+
)
169+
check_dimensions(L, A, B)
170+
171+
# Define type shortcuts
172+
fT = _FType(L)
173+
cT = _CType(L)
174+
175+
# Calculate |v₁> = B|ρss>
176+
ρss =
177+
isnothing(solver.steadystate_solver) ? mat2vec(steadystate(L)) :
178+
mat2vec(steadystate(L; solver = solver.steadystate_solver))
179+
vₖ = (spre(B) * ρss).data
180+
181+
# Define (possibly GPU) vector type
182+
vT = typeof(vₖ)
183+
184+
# Calculate <w₁| = <I|A
185+
D = prod(L.dimensions)
186+
Ivec = SparseVector(D^2, [1 + n * (D + 1) for n in 0:(D-1)], ones(cT, D)) # same as vec(system_identity_matrix)
187+
wₖ = spre(A).data' * vT(Ivec)
188+
189+
# Store the norm of the Green's function before renormalizing |v₁> and <w₁|
190+
gfNorm = abs(dot(wₖ, vₖ))
191+
vₖ ./= sqrt(gfNorm)
192+
wₖ ./= sqrt(gfNorm)
193+
194+
# Handle input frequency range
195+
ωList = vT(convert(Vector{fT}, ωlist)) # Make sure they're real frequencies and potentially on GPU
196+
Length = length(ωList)
197+
198+
# Current and previous estimates of the spectrum
199+
lanczosFactor = vT(zeros(cT, Length))
200+
lanczosFactor₋₁ = vT(zeros(cT, Length))
201+
202+
# Tridiagonal matrix elements
203+
αₖ = cT(0)
204+
βₖ = cT(-1)
205+
δₖ = fT(+1)
206+
207+
# Current and up to second-to-last A and B Euler sequences
208+
A₋₂ = vT(ones(cT, Length))
209+
A₋₁ = vT(zeros(cT, Length))
210+
Aₖ = vT(zeros(cT, Length))
211+
B₋₂ = vT(zeros(cT, Length))
212+
B₋₁ = vT(ones(cT, Length))
213+
Bₖ = vT(zeros(cT, Length))
214+
215+
# Maximum norm and residue
216+
maxNorm = vT(zeros(cT, length(ωList)))
217+
maxResidue = fT(0.0)
218+
219+
# Previous and next left/right Krylov vectors
220+
v₋₁ = vT(zeros(cT, D^2))
221+
v₊₁ = vT(zeros(cT, D^2))
222+
w₋₁ = vT(zeros(cT, D^2))
223+
w₊₁ = vT(zeros(cT, D^2))
224+
225+
# Frequency of renormalization
226+
renormFrequency = 1
227+
228+
# Loop over the Krylov subspace(s)
229+
for k in 1:solver.maxiter
230+
# k-th diagonal element
231+
mul!(w₊₁, L.data', wₖ)
232+
αₖ = dot(w₊₁, vₖ)
233+
234+
# Update A(k), B(k) and continuous fraction; normalization avoids overflow
235+
Aₖ .= (-1im .* ωList .+ αₖ) .* A₋₁ .- (βₖ * δₖ) .* A₋₂
236+
Bₖ .= (-1im .* ωList .+ αₖ) .* B₋₁ .- (βₖ * δₖ) .* B₋₂
237+
lanczosFactor₋₁ .= lanczosFactor
238+
lanczosFactor .= Aₖ ./ Bₖ
239+
240+
# Renormalize Euler sequences to avoid overflow
241+
if k % renormFrequency == 0
242+
map!((x, y) -> max(abs(x), abs(y)), maxNorm, Aₖ, Bₖ)
243+
Aₖ ./= maxNorm
244+
Bₖ ./= maxNorm
245+
A₋₁ ./= maxNorm
246+
B₋₁ ./= maxNorm
247+
end
248+
249+
# Check for convergence
250+
251+
residueNorm = max(maximum(abs, lanczosFactor), maximum(abs, lanczosFactor₋₁))
252+
lanczosFactor₋₁ .-= lanczosFactor
253+
maxResidue = maximum(abs, lanczosFactor₋₁) / residueNorm
254+
if maxResidue <= solver.tol
255+
if solver.verbose > 1
256+
println("spectrum(): solver::Lanczos converged after $(k) iterations")
257+
end
258+
break
259+
end
260+
261+
# (k+1)-th left/right vectors, orthogonal to previous ones
262+
mul!(v₊₁, L.data, vₖ)
263+
v₊₁ .= v₊₁ .- αₖ .* vₖ .- βₖ .* v₋₁
264+
w₊₁ .= w₊₁ .- conj(αₖ) .* wₖ .- conj(δₖ) .* w₋₁
265+
v₋₁, vₖ = vₖ, v₋₁
266+
vₖ, v₊₁ = v₊₁, vₖ
267+
w₋₁, wₖ = wₖ, w₋₁
268+
wₖ, w₊₁ = w₊₁, wₖ
269+
270+
# k-th off-diagonal elements
271+
buf = dot(wₖ, vₖ)
272+
δₖ = sqrt(abs(buf))
273+
βₖ = buf / δₖ
274+
275+
# Normalize (k+1)-th left/right vectors
276+
vₖ ./= δₖ
277+
wₖ ./= conj(βₖ)
278+
279+
# Update everything for the next cycle
280+
A₋₂, A₋₁ = A₋₁, A₋₂
281+
A₋₁, Aₖ = Aₖ, A₋₁
282+
B₋₂, B₋₁ = B₋₁, B₋₂
283+
B₋₁, Bₖ = Bₖ, B₋₁
284+
end
285+
286+
if solver.verbose > 0 && maxResidue > solver.tol
287+
println("spectrum(): maxiter = $(solver.maxiter) reached before convergence!")
288+
println("spectrum(): Max residue = $maxResidue")
289+
println("spectrum(): Consider increasing maxiter and/or tol")
290+
end
291+
292+
# Restore the norm
293+
lanczosFactor .= gfNorm .* lanczosFactor
294+
295+
return -2 .* real(lanczosFactor)
296+
end
297+
147298
@doc raw"""
148299
spectrum_correlation_fft(tlist, corr; inverse=false)
149300

test/core-test/correlations_and_spectrum.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
ω_l2 = range(0, 3, length = 1000)
1414
spec2 = spectrum(H, ω_l2, c_ops, a', a)
1515
spec3 = spectrum(H, ω_l2, c_ops, a', a; solver = PseudoInverse())
16+
spec4 = spectrum(H, ω_l2, c_ops, a', a; solver = Lanczos())
1617

1718
spec1 = spec1 ./ maximum(spec1)
1819
spec2 = spec2 ./ maximum(spec2)
1920
spec3 = spec3 ./ maximum(spec3)
21+
spec4 = spec4 ./ maximum(spec4)
2022

2123
test_func1 = maximum(real.(spec1)) * (0.1 / 2)^2 ./ ((ω_l1 .- 1) .^ 2 .+ (0.1 / 2)^2)
2224
test_func2 = maximum(real.(spec2)) * (0.1 / 2)^2 ./ ((ω_l2 .- 1) .^ 2 .+ (0.1 / 2)^2)
@@ -26,12 +28,26 @@
2628
@test sum(abs2.(spec2[idxs2] .- test_func2[idxs2])) / sum(abs2.(test_func2[idxs2])) < 0.01
2729
@test all(corr1 .≈ corr2)
2830
@test all(spec2 .≈ spec3)
31+
@test all(spec2 .≈ spec4)
2932

3033
@testset "Type Inference spectrum" begin
3134
@inferred correlation_2op_1t(H, nothing, t_l, c_ops, a', a; progress_bar = Val(false))
3235
@inferred spectrum_correlation_fft(t_l, corr1)
3336
@inferred spectrum(H, ω_l2, c_ops, a', a)
3437
@inferred spectrum(H, ω_l2, c_ops, a', a; solver = PseudoInverse())
38+
@inferred spectrum(H, ω_l2, c_ops, a', a; solver = Lanczos())
39+
end
40+
41+
@testset "Verbose mode Lanczos" begin
42+
cout = stdout
43+
r, w = redirect_stdout()
44+
nout = @async read(r, String)
45+
spectrum(H, ω_l2, c_ops, a', a; solver = Lanczos(verbose = 2, maxiter = 2, tol = 1e-16));
46+
redirect_stdout(cout)
47+
close(w)
48+
out = fetch(nout)
49+
outlines = split(out, '\n', keepempty = false)
50+
@test last(outlines) == "spectrum(): Consider increasing maxiter and/or tol"
3551
end
3652

3753
# tlist and τlist checks

test/ext-test/gpu/cuda_ext.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,29 @@ end
154154
@test ρ_ss_cpu.data Array(ρ_ss_gpu_csr.data) atol = 1e-8 * length(ρ_ss_cpu)
155155
end
156156

157+
@testset "CUDA spectrum" begin
158+
N = 10
159+
a = cu(destroy(N))
160+
H = a' * a
161+
c_ops = [sqrt(0.1 * (0.01 + 1)) * a, sqrt(0.1 * (0.01)) * a']
162+
solver = Lanczos(steadystate_solver = SteadyStateLinearSolver())
163+
164+
ω_l = range(0, 3, length = 1000)
165+
spec = spectrum(H, ω_l, c_ops, a', a; solver = solver)
166+
167+
spec = collect(spec)
168+
spec = spec ./ maximum(spec)
169+
170+
test_func = maximum(real.(spec)) * (0.1 / 2)^2 ./ ((ω_l .- 1) .^ 2 .+ (0.1 / 2)^2)
171+
idxs = test_func .> 0.05
172+
@test sum(abs2.(spec[idxs] .- test_func[idxs])) / sum(abs2.(test_func[idxs])) < 0.01
173+
174+
# TODO: Fix this
175+
# @testset "Type Inference spectrum" begin
176+
# @inferred spectrum(H, ω_l, c_ops, a', a; solver = solver)
177+
# end
178+
end
179+
157180
@testset "CUDA ptrace" begin
158181
g = fock(2, 1)
159182
e = fock(2, 0)

0 commit comments

Comments
 (0)