-
Notifications
You must be signed in to change notification settings - Fork 19
/
DispersionAndIntensities.jl
314 lines (268 loc) · 12.6 KB
/
DispersionAndIntensities.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# Bogoliubov transformation that diagonalizes a quadratic bosonic Hamiltonian,
# allowing for anomalous terms. The general procedure derives from Colpa,
# Physica A, 93A, 327-353 (1978). Overwrites data in H.
function bogoliubov!(T::Matrix{ComplexF64}, H::Matrix{ComplexF64})
L = div(size(H, 1), 2)
@assert size(T) == size(H) == (2L, 2L)
# H0 = copy(H)
# Initialize T to the para-unitary identity Ĩ = diagm([ones(L), -ones(L)])
T .= 0
for i in 1:L
T[i, i] = 1
T[i+L, i+L] = -1
end
# Solve generalized eigenvalue problem, Ĩ t = λ H t, for columns t of T.
# Eigenvalues are sorted such that positive values appear first, and are
# otherwise ascending in absolute value.
sortby(x) = (-sign(x), abs(x))
λ, T0 = eigen!(Hermitian(T), Hermitian(H); sortby)
# Note that T0 and T refer to the same data.
@assert T0 === T
# Normalize columns of T so that para-unitarity holds, T† Ĩ T = Ĩ.
for j in axes(T, 2)
c = 1 / sqrt(abs(λ[j]))
view(T, :, j) .*= c
end
# Inverse of λ are eigenvalues of Ĩ H. A factor of 2 yields the physical
# quasiparticle energies.
energies = λ # reuse storage
@. energies = 2 / λ
# The first L elements are positive, while the next L energies are negative.
# Their absolute values are excitation energies for the wavevectors q and
# -q, respectively.
@assert all(>(0), view(energies, 1:L)) && all(<(0), view(energies, L+1:2L))
# Disable tests below for speed. Note that the data in H has been
# overwritten by eigen!, so H0 should refer to an original copy of H.
#=
Ĩ = Diagonal([ones(L); -ones(L)])
@assert T' * Ĩ * T ≈ Ĩ
@assert diag(T' * H0 * T) ≈ Ĩ * energies / 2
# Reflection symmetry H(q) = H(-q) is identified as H11 = conj(H22). In this
# case, eigenvalues come in pairs.
if H0[1:L, 1:L] ≈ conj(H0[L+1:2L, L+1:2L])
@assert energies[1:L] ≈ -energies[L+1:2L]
end
=#
return energies
end
# Returns |1 + nB(ω)| where nB(ω) = 1 / (exp(βω) - 1) is the Bose function.
function thermal_prefactor(ω; kT)
@assert kT >= 0
iszero(ω) && return Inf
return abs(1 / (1 - exp(-ω/kT)))
end
"""
excitations!(T, tmp, swt::SpinWaveTheory, q)
Given a wavevector `q`, solves for the matrix `T` representing quasi-particle
excitations, and returns a list of quasi-particle energies. Both `T` and `tmp`
must be supplied as ``2L×2L`` complex matrices, where ``L`` is the number of
bands for a single ``𝐪`` value.
The columns of `T` are understood to be contracted with the Holstein-Primakoff
bosons ``[𝐛_𝐪, 𝐛_{-𝐪}^†]``. The first ``L`` columns provide the eigenvectors
of the quadratic Hamiltonian for the wavevector ``𝐪``. The next ``L`` columns
of `T` describe eigenvectors for ``-𝐪``. The return value is a vector with
similar grouping: the first ``L`` values are energies for ``𝐪``, and the next
``L`` values are the _negation_ of energies for ``-𝐪``.
excitations!(T, tmp, swt::SpinWaveTheorySpiral, q; branch)
Calculations on a [`SpinWaveTheorySpiral`](@ref) additionally require a `branch`
index. The possible branches ``(1, 2, 3)`` correspond to scattering processes
``𝐪 - 𝐤, 𝐪, 𝐪 + 𝐤`` respectively, where ``𝐤`` is the ordering wavevector.
Each branch will contribute ``L`` excitations, where ``L`` is the number of
spins in the magnetic cell. This yields a total of ``3L`` excitations for a
given momentum transfer ``𝐪``.
"""
function excitations!(T, tmp, swt::SpinWaveTheory, q)
L = nbands(swt)
size(T) == size(tmp) == (2L, 2L) || error("Arguments T and tmp must be $(2L)×$(2L) matrices")
q_reshaped = to_reshaped_rlu(swt.sys, q)
dynamical_matrix!(tmp, swt, q_reshaped)
try
return bogoliubov!(T, tmp)
catch _
error("Instability at wavevector q = $q")
end
end
"""
excitations(swt::SpinWaveTheory, q)
excitations(swt::SpinWaveTheorySpiral, q; branch)
Returns a pair `(energies, T)` providing the excitation energies and
eigenvectors. Prefer [`excitations!`](@ref) for performance, which avoids matrix
allocations. See the documentation of [`excitations!`](@ref) for more details.
"""
function excitations(swt::SpinWaveTheory, q)
L = nbands(swt)
T = zeros(ComplexF64, 2L, 2L)
H = zeros(ComplexF64, 2L, 2L)
energies = excitations!(T, copy(H), swt, q)
return (energies, T)
end
"""
dispersion(swt::SpinWaveTheory, qpts)
Given a list of wavevectors `qpts` in reciprocal lattice units (RLU), returns
excitation energies for each band. The return value `ret` is 2D array, and
should be indexed as `ret[band_index, q_index]`.
"""
function dispersion(swt::SpinWaveTheory, qpts)
L = nbands(swt)
qpts = convert(AbstractQPoints, qpts)
disp = zeros(L, length(qpts.qs))
for (iq, q) in enumerate(qpts.qs)
view(disp, :, iq) .= view(excitations(swt, q)[1], 1:L)
end
return reshape(disp, L, size(qpts.qs)...)
end
"""
intensities_bands(swt::SpinWaveTheory, qpts; kT=0)
Calculate spin wave excitation bands for a set of q-points in reciprocal space.
This calculation is analogous to [`intensities`](@ref), but does not perform
line broadening of the bands.
"""
function intensities_bands(swt::SpinWaveTheory, qpts; kT=0, with_negative=false)
(; sys, measure) = swt
isempty(measure.observables) && error("No observables! Construct SpinWaveTheory with a `measure` argument.")
with_negative && error("Option `with_negative=true` not yet supported.")
qpts = convert(AbstractQPoints, qpts)
cryst = orig_crystal(sys)
# Number of (magnetic) atoms in magnetic cell
@assert sys.dims == (1,1,1)
Na = nsites(sys)
# Number of chemical cells in magnetic cell
Ncells = Na / natoms(cryst)
# Number of quasiparticle modes
L = nbands(swt)
# Number of wavevectors
Nq = length(qpts.qs)
# Temporary storage for pair correlations
Nobs = num_observables(measure)
Ncorr = num_correlations(measure)
corrbuf = zeros(ComplexF64, Ncorr)
# Preallocation
T = zeros(ComplexF64, 2L, 2L)
H = zeros(ComplexF64, 2L, 2L)
Avec_pref = zeros(ComplexF64, Nobs, Na)
disp = zeros(Float64, L, Nq)
intensity = zeros(eltype(measure), L, Nq)
for (iq, q) in enumerate(qpts.qs)
q_global = cryst.recipvecs * q
view(disp, :, iq) .= view(excitations!(T, H, swt, q), 1:L)
for i in 1:Na, μ in 1:Nobs
r_global = global_position(sys, (1,1,1,i)) # + offsets[μ,i]
ff = get_swt_formfactor(measure, μ, i)
Avec_pref[μ, i] = exp(- im * dot(q_global, r_global))
Avec_pref[μ, i] *= compute_form_factor(ff, norm2(q_global))
end
Avec = zeros(ComplexF64, Nobs)
# Fill `intensity` array
for band in 1:L
fill!(Avec, 0)
if sys.mode == :SUN
data = swt.data::SWTDataSUN
N = sys.Ns[1]
t = reshape(view(T, :, band), N-1, Na, 2)
for i in 1:Na, μ in 1:Nobs
O = data.observables_localized[μ, i]
for α in 1:N-1
Avec[μ] += Avec_pref[μ, i] * (O[α, N] * t[α, i, 2] + O[N, α] * t[α, i, 1])
end
end
else
@assert sys.mode in (:dipole, :dipole_uncorrected)
data = swt.data::SWTDataDipole
t = reshape(view(T, :, band), Na, 2)
for i in 1:Na, μ in 1:Nobs
O = data.observables_localized[μ, i]
# This is the Avec of the two transverse and one
# longitudinal directions in the local frame. (In the
# local frame, z is longitudinal, and we are computing
# the transverse part only, so the last entry is zero)
displacement_local_frame = SA[t[i, 2] + t[i, 1], im * (t[i, 2] - t[i, 1]), 0.0]
Avec[μ] += Avec_pref[μ, i] * (data.sqrtS[i]/√2) * (O' * displacement_local_frame)[1]
end
end
map!(corrbuf, measure.corr_pairs) do (α, β)
Avec[α] * conj(Avec[β]) / Ncells
end
intensity[band, iq] = thermal_prefactor(disp[band]; kT) * measure.combiner(q_global, corrbuf)
end
end
disp = reshape(disp, L, size(qpts.qs)...)
intensity = reshape(intensity, L, size(qpts.qs)...)
return BandIntensities(cryst, qpts, disp, intensity)
end
"""
intensities!(data, swt::SpinWaveTheory, qpts; energies, kernel, kT=0)
intensities!(data, sc::SampledCorrelations, qpts; energies, kernel=nothing, kT=0)
Like [`intensities`](@ref), but makes use of storage space `data` to avoid
allocation costs.
"""
function intensities!(data, swt::AbstractSpinWaveTheory, qpts; energies, kernel::AbstractBroadening, kT=0)
@assert size(data) == (length(energies), size(qpts.qs)...)
bands = intensities_bands(swt, qpts; kT)
@assert eltype(bands) == eltype(data)
broaden!(data, bands; energies, kernel)
return Intensities(bands.crystal, bands.qpts, collect(Float64, energies), data)
end
"""
intensities(swt::SpinWaveTheory, qpts; energies, kernel, kT=0)
intensities(sc::SampledCorrelations, qpts; energies, kernel=nothing, kT)
Calculates dynamical pair correlation intensities for a set of ``𝐪``-points in
reciprocal space.
Linear spin wave theory calculations are performed with an instance of
[`SpinWaveTheory`](@ref). The alternative [`SpinWaveTheorySpiral`](@ref) allows
to study generalized spiral orders with a single, incommensurate-``𝐤`` ordering
wavevector. Another alternative [`SpinWaveTheoryKPM`](@ref) is favorable for
calculations on large magnetic cells, and allows to study systems with disorder.
An optional nonzero temperature `kT` will scale intensities by the quantum
thermal occupation factor ``|1 + n_B(ω)|`` where ``n_B(ω) = 1/(e^{βω}-1)`` is
the Bose function.
Intensities can also be calculated for `SampledCorrelations` associated with
classical spin dynamics. In this case, thermal broadening will already be
present, and the line-broadening `kernel` becomes optional. Conversely, the
parameter `kT` becomes required. If positive, it will introduce an intensity
correction factor ``|βω (1 + n_B(ω))|`` that undoes the occupation factor for
the classical Boltzmann distribution and applies the quantum thermal occupation
factor. The special choice `kT = nothing` will suppress the classical-to-quantum
correction factor, and yield statistics consistent with the classical Boltzmann
distribution.
"""
function intensities(swt::AbstractSpinWaveTheory, qpts; energies, kernel::AbstractBroadening, kT=0)
return broaden(intensities_bands(swt, qpts; kT); energies, kernel)
end
"""
intensities_static(swt::SpinWaveTheory, qpts; bounds=(-Inf, Inf), kernel=nothing, kT=0)
intensities_static(sc::SampledCorrelations, qpts; bounds=(-Inf, Inf), kT)
intensities_static(sc::SampledCorrelationsStatic, qpts)
Like [`intensities`](@ref), but integrates the dynamical correlations
``\\mathcal{S}(𝐪, ω)`` over a range of energies ``ω``. By default, the
integration `bounds` are ``(-∞, ∞)``, yielding the instantaneous (equal-time)
correlations.
In [`SpinWaveTheory`](@ref), the integral will be realized as a sum over
discrete bands. Alternative calculation methods are
[`SpinWaveTheorySpiral`](@ref) and [`SpinWaveTheoryKPM`](@ref).
Classical dynamics data in [`SampledCorrelations`](@ref) can also be used to
calculate static intensities. In this case, the domain of integration will be a
finite grid of available `energies`. Here, the parameter `kT` will be used to
account for the quantum thermal occupation of excitations, as documented in
[`intensities`](@ref).
Static intensities calculated from [`SampledCorrelationsStatic`](@ref) are
dynamics-independent. Instead, instantaneous correlations sampled from the
classical Boltzmann distribution will be reported.
"""
function intensities_static(swt::AbstractSpinWaveTheory, qpts; bounds=(-Inf, Inf), kernel=nothing, kT=0)
res = intensities_bands(swt, qpts; kT) # TODO: with_negative=true
data_reduced = zeros(eltype(res.data), size(res.data)[2:end])
for ib in axes(res.data, 1), iq in CartesianIndices(data_reduced)
ϵ = res.disp[ib, iq]
if isnothing(kernel) || bounds == (-Inf, Inf)
if bounds[1] <= ϵ < bounds[2]
data_reduced[iq] += res.data[ib, iq]
end
else
isnothing(kernel.integral) && error("Kernel must provide integral")
ihi = kernel.integral(bounds[2] - ϵ)
ilo = kernel.integral(bounds[1] - ϵ)
data_reduced[iq] += res.data[ib, iq] * (ihi - ilo)
end
end
StaticIntensities(res.crystal, res.qpts, data_reduced)
end