Skip to content

Commit 28a0212

Browse files
Phonons docs (#953)
1 parent 27a96a5 commit 28a0212

File tree

14 files changed

+179
-194
lines changed

14 files changed

+179
-194
lines changed

src/DFTK.jl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,6 @@ include("response/hessian.jl")
222222
export compute_current
223223
include("postprocess/current.jl")
224224
export phonon_modes
225-
export phonon_modes_cart
226-
export compute_dynmat
227-
export compute_dynmat_cart
228225
include("postprocess/phonon.jl")
229226

230227
# Workarounds

src/PlaneWaveBasis.jl

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@ abstract type AbstractBasis{T <: Real} end
1313
"""
1414
Discretization information for ``k``-point-dependent quantities such as orbitals.
1515
More generally, a ``k``-point is a block of the Hamiltonian;
16-
eg collinear spin is treated by doubling the number of kpoints.
16+
e.g. collinear spin is treated by doubling the number of ``k``-points.
1717
"""
1818
struct Kpoint{T <: Real, GT <: AbstractVector{Vec3{Int}}}
1919
spin::Int # Spin component can be 1 or 2 as index into what is
2020
# # returned by the `spin_components` function
2121
coordinate::Vec3{T} # Fractional coordinate of k-point
22+
G_vectors::GT # Wave vectors in integer coordinates (vector of Vec3{Int})
23+
# # ({G, 1/2 |k+G|^2 ≤ Ecut})
24+
# This is not assumed to be in any particular order
2225
mapping::Vector{Int} # Index of G_vectors[i] on the FFT grid:
2326
# # G_vectors(basis)[kpt.mapping[i]] == G_vectors(basis, kpt)[i]
2427
mapping_inv::Dict{Int, Int} # Inverse of `mapping`:
2528
# # G_vectors(basis)[i] == G_vectors(basis, kpt)[mapping_inv[i]]
26-
G_vectors::GT # Wave vectors in integer coordinates (vector of Vec3{Int})
27-
# # ({G, 1/2 |k+G|^2 ≤ Ecut})
2829
end
2930

3031
@doc raw"""
@@ -145,12 +146,36 @@ function Kpoint(spin::Integer, coordinate::AbstractVector{<:Real},
145146
Gvecs_k = to_device(architecture, Gvecs_k)
146147

147148
mapping_inv = Dict(ifull => iball for (iball, ifull) in enumerate(mapping))
148-
Kpoint(spin, k, mapping, mapping_inv, Gvecs_k)
149+
Kpoint(spin, k, Gvecs_k, mapping, mapping_inv)
149150
end
150151
function Kpoint(basis::PlaneWaveBasis, coordinate::AbstractVector, spin::Int)
151152
Kpoint(spin, coordinate, basis.model.recip_lattice, basis.fft_size, basis.Ecut;
152153
basis.variational, basis.architecture)
153154
end
155+
# Construct the kpoint with coordinate equivalent_kpt.coordinate + ΔG.
156+
# Equivalent to (but faster than) Kpoint(equivalent_kpt.coordinate + ΔG).
157+
function construct_from_equivalent_kpt(basis, equivalent_kpt, coordinate, ΔG)
158+
linear = LinearIndices(basis.fft_size)
159+
# Mapping is the same as if created from scratch, although it is not ordered.
160+
mapping = map(CartesianIndices(basis.fft_size)[equivalent_kpt.mapping]) do G
161+
linear[CartesianIndex(mod1.(Tuple(G + CartesianIndex(ΔG...)), basis.fft_size))]
162+
end
163+
mapping_inv = Dict(ifull => iball for (iball, ifull) in enumerate(mapping))
164+
Kpoint(equivalent_kpt.spin, Vec3(coordinate), equivalent_kpt.G_vectors .+ Ref(ΔG),
165+
mapping, mapping_inv)
166+
end
167+
# Returns the kpoint at given coordinate. If outside the Brillouin zone, it is created
168+
# from an equivalent kpoint in the basis (also returned)
169+
function get_kpoint(basis::PlaneWaveBasis{T}, kcoord, spin) where {T}
170+
index, ΔG = find_equivalent_kpt(basis, kcoord, spin)
171+
equivalent_kpt = basis.kpoints[index]
172+
if iszero(ΔG)
173+
kpt = equivalent_kpt
174+
else
175+
kpt = construct_from_equivalent_kpt(basis, equivalent_kpt, kcoord, ΔG)
176+
end
177+
(; kpt, equivalent_kpt)
178+
end
154179

155180
@timing function build_kpoints(model::Model{T}, fft_size, kcoords, Ecut;
156181
variational=true,
@@ -163,7 +188,7 @@ end
163188
for= 1:model.n_spin_components
164189
for kpt in kpoints_spin_1
165190
push!(all_kpoints, Kpoint(iσ, kpt.coordinate,
166-
kpt.mapping, kpt.mapping_inv, kpt.G_vectors))
191+
kpt.G_vectors, kpt.mapping, kpt.mapping_inv))
167192
end
168193
end
169194
all_kpoints

src/densities.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ end
5858
Tδρ = iszero(q) ? real(Tψ) :
5959
real_qzero = iszero(q) ? real : identity
6060

61-
ordering(kdata) = kdata[k_to_equivalent_kpq_permutation(basis, q)]
62-
6361
# occupation should be on the CPU as we are going to be doing scalar indexing.
6462
occupation = [to_cpu(oc) for oc in occupation]
6563
mask_occ = [findall(occnk -> abs(occnk) occupation_threshold, occk)
@@ -72,14 +70,16 @@ end
7270
end
7371
range = [(ik, n) for ik = 1:length(basis.kpoints) for n = mask_occ[ik]]
7472

73+
k_to_k_plus_q = k_to_kpq_permutation(basis, q)
7574
storages = parallel_loop_over_range(allocate_local_storage, range) do storage, kn
7675
(ik, n) = kn
7776

7877
kpt = basis.kpoints[ik]
7978
ifft!(storage.ψnk_real, basis, kpt, ψ[ik][:, n])
8079
# We return the δψk in the basis k+q which are associated to a displacement of the ψk.
81-
kpt_plus_q, δψk_plus_q = kpq_equivalent_blochwave_to_kpq(basis, kpt, q,
82-
ordering(δψ)[ik])
80+
kpt_plus_q, equivalent_kpt_plus_q = get_kpoint(basis, kpt.coordinate + q, kpt.spin)
81+
δψk_plus_q = transfer_blochwave_kpt(δψ[k_to_k_plus_q[ik]], basis,
82+
equivalent_kpt_plus_q, basis, kpt_plus_q)
8383
# The perturbation of the density
8484
# |ψ_nk|² is 2 ψ_{n,k} * δψ_{n,k+q}.
8585
ifft!(storage.δψnk_real, basis, kpt_plus_q, δψk_plus_q[:, n])
@@ -106,7 +106,7 @@ end
106106
dαψnk_real = zeros(complex(eltype(basis)), basis.fft_size)
107107
for (ik, kpt) in enumerate(basis.kpoints)
108108
G_plus_k = [[p[α] for p in Gplusk_vectors_cart(basis, kpt)] for α = 1:3]
109-
for n = 1:size(ψ[ik], 2), α = 1:3
109+
for n in axes(ψ[ik], 2), α = 1:3
110110
ifft!(dαψnk_real, basis, kpt, im .* G_plus_k[α] .* ψ[ik][:, n])
111111
@. τ[:, :, :, kpt.spin] += occupation[ik][n] * basis.kweights[ik] / 2 * abs2(dαψnk_real)
112112
end

src/postprocess/phonon.jl

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,19 @@
22
function dynmat_red_to_cart(model::Model, dynmat)
33
inv_lattice = model.inv_lattice
44

5-
# The dynamical matrix `D` acts on vectors `dr` and gives a covector `dF`:
6-
# dF = D · dr.
7-
# Thus the transformation between reduced and Cartesian coordinates is not a comatrix.
8-
# To transform `dynamical_matrix` from reduced coordinates to `dynmat_cart` in Cartesian
9-
# coordinates, we write
10-
# dr_cart = lattice · dr_red,
11-
# ⇒ dr_redᵀ · D_red · dr_red = dr_cartᵀ · lattice⁻ᵀ · D_red · lattice⁻¹ · dr_cart
12-
# = dr_cartᵀ · D_cart · dr_cart
13-
# ⇒ D_cart = lattice⁻ᵀ · D_red · lattice⁻¹.
14-
5+
# The dynamical matrix `D` acts on vectors `δr` and gives a covector `δF`:
6+
# δF = D δr
7+
# We have δr_cart = lattice * δr_red, δF_cart = lattice⁻ᵀ δF_red, so
8+
# δF_cart = lattice⁻ᵀ D_red lattice⁻¹ δr_cart
159
dynmat_cart = zero.(dynmat)
16-
for s = 1:size(dynmat_cart, 2), α = 1:size(dynmat_cart, 4)
10+
for s in axes(dynmat_cart, 2), α in axes(dynmat_cart, 4)
1711
dynmat_cart[:, α, :, s] = inv_lattice' * dynmat[:, α, :, s] * inv_lattice
1812
end
1913
dynmat_cart
2014
end
2115

22-
# Create a ``3×n_{\rm atoms}×3×n_{\rm atoms}`` tensor (for consistency with the format of
23-
# dynamical matrices) equivalent to a diagonal matrix with the atomic masses of the atoms in
24-
# a.u. on the diagonal.
16+
# Create a ``3×n_{\rm atoms}×3×n_{\rm atoms}`` tensor equivalent to a diagonal matrix with
17+
# the atomic masses of the atoms in a.u. on the diagonal.
2518
function mass_matrix(T, atoms)
2619
n_atoms = length(atoms)
2720
atoms_mass = atomic_mass.(atoms)
@@ -34,26 +27,42 @@ function mass_matrix(T, atoms)
3427
end
3528
mass_matrix(model::Model{T}) where {T} = mass_matrix(T, model.atoms)
3629

37-
@doc raw"""
38-
Solve the eigenproblem for a dynamical matrix: returns the `frequencies` and eigenvectors
39-
(`vectors`).
4030
"""
41-
function phonon_modes(basis::PlaneWaveBasis{T}, dynmat) where {T}
31+
Get phonon quantities. We return the frequencies, the mass matrix and reduced and Cartesian
32+
eigenvectors and dynamical matrices.
33+
"""
34+
function phonon_modes(basis::PlaneWaveBasis{T}, ψ, occupation; kwargs...) where {T}
35+
dynmat = compute_dynmat(basis::PlaneWaveBasis, ψ, occupation; kwargs...)
36+
dynmat_cart = dynmat_red_to_cart(basis.model, dynmat)
37+
38+
modes = _phonon_modes(basis, dynmat_cart)
39+
vectors = similar(modes.vectors_cart)
40+
for s in axes(vectors, 2), t in axes(vectors, 4)
41+
vectors[:, s, :, t] = vector_cart_to_red(basis.model, modes.vectors_cart[:, s, :, t])
42+
end
43+
44+
(; modes.mass_matrix, modes.frequencies, dynmat, dynmat_cart, vectors, modes.vectors_cart)
45+
end
46+
# Compute the frequencies and vectors. Internal because of the potential misuse:
47+
# the diagonalization of the phonon modes has to be done in Cartesian coordinates.
48+
function _phonon_modes(basis::PlaneWaveBasis{T}, dynmat_cart) where {T}
4249
n_atoms = length(basis.model.positions)
4350
M = reshape(mass_matrix(T, basis.model.atoms), 3*n_atoms, 3*n_atoms)
4451

45-
res = eigen(reshape(dynmat, 3*n_atoms, 3*n_atoms), M)
52+
res = eigen(reshape(dynmat_cart, 3*n_atoms, 3*n_atoms), M)
4653
maximum(abs, imag(res.values)) > sqrt(eps(T)) &&
47-
@warn "Some eigenvalues of the dynamical matrix have a large imaginary part"
54+
@warn "Some eigenvalues of the dynamical matrix have a large imaginary part."
4855

4956
signs = sign.(real(res.values))
5057
frequencies = signs .* sqrt.(abs.(real(res.values)))
5158

52-
(; frequencies, res.vectors)
59+
vectors_cart =
60+
(; mass_matrix=M, frequencies, vectors_cart=reshape(res.vectors, 3, n_atoms, 3, n_atoms))
5361
end
54-
function phonon_modes_cart(basis::PlaneWaveBasis{T}, dynmat) where {T}
55-
dynmat_cart = dynmat_red_to_cart(basis.model, dynmat)
56-
phonon_modes(basis, dynmat_cart)
62+
# For convenience
63+
function phonon_modes(scfres::NamedTuple; kwargs...)
64+
phonon_modes(scfres.basis, scfres.ψ, scfres.occupation; scfres.ρ, scfres.ham,
65+
scfres.occupation_threshold, scfres.εF, scfres.eigenvalues, kwargs...)
5766
end
5867

5968
@doc raw"""
@@ -69,44 +78,27 @@ in reduced coordinates.
6978
δoccupations = [zero.(occupation) for _ = 1:3, _ = 1:n_atoms]
7079
δψs = [zero.(ψ) for _ = 1:3, _ = 1:n_atoms]
7180
for s = 1:n_atoms, α = 1:basis.model.n_dim
81+
# Get δH ψ
7282
δHψs_αs = compute_δHψ_αs(basis, ψ, α, s, q)
7383
isnothing(δHψs_αs) && continue
84+
# Response solver to get δψ
7485
(; δψ, δρ, δoccupation) = solve_ΩplusK_split(ham, ρ, ψ, occupation, εF, eigenvalues,
7586
-δHψs_αs; q, kwargs...)
7687
δoccupations[α, s] = δoccupation
7788
δρs[α, s] = δρ
7889
δψs[α, s] = δψ
7990
end
80-
91+
# Query each energy term for their contribution to the dynamical matrix.
8192
dynmats_per_term = [compute_dynmat(term, basis, ψ, occupation; ρ, δψs, δρs,
8293
δoccupations, q)
8394
for term in basis.terms]
8495
sum(filter(!isnothing, dynmats_per_term))
8596
end
8697

8798
"""
88-
Cartesian form of [`compute_dynmat`](@ref).
89-
"""
90-
function compute_dynmat_cart(basis::PlaneWaveBasis, ψ, occupation; kwargs...)
91-
dynmats_reduced = compute_dynmat(basis, ψ, occupation; kwargs...)
92-
dynmat_red_to_cart(basis.model, dynmats_reduced)
93-
end
94-
95-
function compute_dynmat(scfres::NamedTuple; kwargs...)
96-
compute_dynmat(scfres.basis, scfres.ψ, scfres.occupation; scfres.ρ, scfres.ham,
97-
scfres.occupation_threshold, scfres.εF, scfres.eigenvalues, kwargs...)
98-
end
99-
100-
function compute_dynmat_cart(scfres; kwargs...)
101-
compute_dynmat_cart(scfres.basis, scfres.ψ, scfres.occupation; scfres.ρ, scfres.ham,
102-
scfres.occupation_threshold, scfres.εF, scfres.eigenvalues, kwargs...)
103-
end
104-
105-
# TODO: Document relation to non-local potential in future phonon PR.
106-
"""
107-
Assemble the right-hand side term for the Sternheimer equation for all relevant quantities:
108-
Compute the perturbation of the Hamiltonian with respect to a variation of the local
109-
potential produced by a displacement of the atom s in the direction α.
99+
Get ``δH·ψ``, with ``δH`` the perturbation of the Hamiltonian with respect to a position
100+
displacement ``e^{iq·r}`` of the ``α`` coordinate of atom ``s``.
101+
`δHψ[ik]` is ``δH·ψ_{k-q}``, expressed in `basis.kpoints[ik]`.
110102
"""
111103
@timing function compute_δHψ_αs(basis::PlaneWaveBasis, ψ, α, s, q)
112104
δHψ_per_term = [compute_δHψ_αs(term, basis, ψ, α, s, q) for term in basis.terms]

src/response/chi0.jl

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ end
290290
Perform in-place computations of the derivatives of the wave functions by solving
291291
a Sternheimer equation for each `k`-points. It is assumed the passed `δψ` are initialised
292292
to zero.
293+
For phonon, `δHψ[ik]` is ``δH·ψ_{k-q}``, expressed in `basis.kpoints[ik]`.
293294
"""
294295
function compute_δψ!(δψ, basis::PlaneWaveBasis{T}, H, ψ, εF, ε, δHψ, ε_minus_q=ε;
295296
ψ_extra=[zeros_like(ψk, size(ψk,1), 0) for ψk in ψ],
@@ -299,7 +300,7 @@ function compute_δψ!(δψ, basis::PlaneWaveBasis{T}, H, ψ, εF, ε, δHψ, ε
299300
# where P_{k} is the projector on ψ_{k} and with the conventions:
300301
# * δψ_{k} is the variation of ψ_{k-q}, which implies (for ℬ_{k} the `basis.kpoints`)
301302
# δψ_{k-q} ∈ ℬ_{k-q} and δHψ_{k-q} ∈ ℬ_{k};
302-
# * δHψ[ik] = δHψ_{k-q};
303+
# * δHψ[ik] = δH ψ_{k-q};
303304
# * ε_minus_q[ik] = ε_{·, k-q}.
304305
model = basis.model
305306
temperature = model.temperature
@@ -343,7 +344,7 @@ end
343344
occupation_threshold, q=zero(Vec3{eltype(ham.basis)}),
344345
kwargs_sternheimer...)
345346
basis = ham.basis
346-
ordering(kdata) = kdata[k_to_equivalent_kpq_permutation(basis, -q)]
347+
k_to_k_minus_q = k_to_kpq_permutation(basis, -q)
347348

348349
# We first select orbitals with occupation number higher than
349350
# occupation_threshold for which we compute the associated response δψn,
@@ -358,9 +359,10 @@ end
358359
ψ_occ = [ψ[ik][:, maskk] for (ik, maskk) in enumerate(mask_occ)]
359360
ψ_extra = [ψ[ik][:, maskk] for (ik, maskk) in enumerate(mask_extra)]
360361
ε_occ = [eigenvalues[ik][maskk] for (ik, maskk) in enumerate(mask_occ)]
361-
δHψ_minus_q_occ = [δHψ[ik][:, maskk] for (ik, maskk) in enumerate(ordering(mask_occ))]
362+
δHψ_minus_q_occ = [δHψ[ik][:, mask_occ[k_to_k_minus_q[ik]]] for ik = 1:length(basis.kpoints)]
362363
# Only needed for phonon calculations.
363-
ε_minus_q_occ = ordering([eigenvalues[ik][maskk] for (ik, maskk) in enumerate(mask_occ)])
364+
ε_minus_q_occ = [eigenvalues[k_to_k_minus_q[ik]][mask_occ[k_to_k_minus_q[ik]]]
365+
for ik = 1:length(basis.kpoints)]
364366

365367
# First we compute δoccupation. We only need to do this for the actually occupied
366368
# orbitals. So we make a fresh array padded with zeros, but only alter the elements
@@ -371,15 +373,17 @@ end
371373
δoccupation = zero.(occupation)
372374
if iszero(q)
373375
δocc_occ = [δoccupation[ik][maskk] for (ik, maskk) in enumerate(mask_occ)]
374-
δεF = compute_δocc!(δocc_occ, basis, ψ_occ, εF, ε_occ, δHψ_minus_q_occ).δεF
376+
(; δεF) = compute_δocc!(δocc_occ, basis, ψ_occ, εF, ε_occ, δHψ_minus_q_occ)
375377
else
378+
# When δH is not periodic, δH ψnk is a Bloch wave at k+q and ψnk at k,
379+
# so that δεnk = <ψnk|δH|ψnk> = 0 and there is no occupation shift
376380
δεF = zero(εF)
377381
end
378382

379383
# Then we compute δψ (again in-place into a zero-padded array) with elements of
380384
# `basis.kpoints` that are equivalent to `k+q`.
381-
δψ = zero.(δHψ) # δHψ ≠ δHψ_minus_q
382-
δψ_occ = [δψ[ik][:, maskk] for (ik, maskk) in enumerate(ordering(mask_occ))]
385+
δψ = zero.(δHψ)
386+
δψ_occ = [δψ[ik][:, maskk] for (ik, maskk) in enumerate(mask_occ[k_to_k_minus_q])]
383387
compute_δψ!(δψ_occ, basis, ham.blocks, ψ_occ, εF, ε_occ, δHψ_minus_q_occ, ε_minus_q_occ;
384388
ψ_extra, q, kwargs_sternheimer...)
385389

src/response/hessian.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ Solve the problem `(Ω+K) δψ = rhs` using a split algorithm, where `rhs` is ty
173173
δVind = apply_kernel(basis, δρ; ρ, q) # Change in potential induced by δρ
174174
# For phonon calculations, assemble
175175
# δHψ_k = δV_{q} · ψ_{k-q}.
176-
δHψ = multiply_ψ_by_blochwave(basis, ψ, δVind, q) - rhs
176+
δHψ = multiply_ψ_by_blochwave(basis, ψ, δVind, q) .- rhs
177177

178178
# Compute total change in eigenvalues
179179
δeigenvalues = map(ψ, δHψ) do ψk, δHψk

0 commit comments

Comments
 (0)