Skip to content

Commit 5049ffb

Browse files
authored
Factorisation of phonon test cases (#822)
1 parent 950b26b commit 5049ffb

File tree

7 files changed

+241
-270
lines changed

7 files changed

+241
-270
lines changed

src/postprocess/phonon.jl

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,19 @@ in reduced coordinates.
6363
@timing function compute_dynmat(basis::PlaneWaveBasis{T}, ψ, occupation; q=zero(Vec3{T}),
6464
ρ=nothing, ham=nothing, εF=nothing, eigenvalues=nothing,
6565
kwargs...) where {T}
66-
model = basis.model
67-
positions = model.positions
68-
n_atoms = length(positions)
69-
n_dim = model.n_dim
70-
66+
n_atoms = length(basis.model.positions)
7167
δρs = [zeros(complex(T), basis.fft_size..., basis.model.n_spin_components)
7268
for _ = 1:3, _ = 1:n_atoms]
7369
δoccupations = [zero.(occupation) for _ = 1:3, _ = 1:n_atoms]
7470
δψs = [zero.(ψ) for _ = 1:3, _ = 1:n_atoms]
75-
if !isempty(ψ)
76-
for s = 1:n_atoms, α = 1:n_dim
77-
δHψs_αs = compute_δHψ_αs(basis, ψ, α, s, q)
78-
(; δψ, δρ, δoccupation) = solve_ΩplusK_split(ham, ρ, ψ, occupation, εF,
79-
eigenvalues, -δHψs_αs; q,
80-
kwargs...)
81-
δoccupations[α, s] = δoccupation
82-
δρs[α, s] = δρ
83-
δψs[α, s] = δψ
84-
end
71+
for s = 1:n_atoms, α = 1:basis.model.n_dim
72+
δHψs_αs = compute_δHψ_αs(basis, ψ, α, s, q)
73+
isnothing(δHψs_αs) && continue
74+
(; δψ, δρ, δoccupation) = solve_ΩplusK_split(ham, ρ, ψ, occupation, εF, eigenvalues,
75+
-δHψs_αs; q, kwargs...)
76+
δoccupations[α, s] = δoccupation
77+
δρs[α, s] = δρ
78+
δψs[α, s] = δψ
8579
end
8680

8781
dynmats_per_term = [compute_dynmat(term, basis, ψ, occupation; ρ, δψs, δρs,
@@ -116,5 +110,7 @@ potential produced by a displacement of the atom s in the direction α.
116110
"""
117111
@timing function compute_δHψ_αs(basis::PlaneWaveBasis, ψ, α, s, q)
118112
δHψ_per_term = [compute_δHψ_αs(term, basis, ψ, α, s, q) for term in basis.terms]
119-
sum(filter(!isnothing, δHψ_per_term))
113+
filter!(!isnothing, δHψ_per_term)
114+
isempty(δHψ_per_term) && return nothing
115+
sum(δHψ_per_term)
120116
end

src/response/cg.jl

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,21 @@ function cg!(x::AbstractVector{T}, A::LinearMap{T}, b::AbstractVector{T};
2828
# p is the descent direction
2929
p = copy(c)
3030
n_iter = 0
31-
residual_norm = zero(real(T))
31+
residual_norm = norm(r)
3232

3333
# convergence history
3434
converged = false
3535

3636
# preconditioned conjugate gradient
3737
while n_iter < maxiter
38+
# output
39+
info = (; A, b, n_iter, x, r, residual_norm, converged, stage=:iterate)
40+
callback(info)
3841
n_iter += 1
42+
if (n_iter miniter) && residual_norm tol
43+
converged = true
44+
break
45+
end
3946
mul!(c, A, p)
4047
α = γ / dot(p, c)
4148

@@ -44,14 +51,6 @@ function cg!(x::AbstractVector{T}, A::LinearMap{T}, b::AbstractVector{T};
4451
r .= proj(r .- α .* c)
4552
residual_norm = norm(r)
4653

47-
# output
48-
info = (; A, b, n_iter, x, r, residual_norm, converged, stage=:iterate)
49-
callback(info)
50-
if (n_iter > miniter) && residual_norm <= tol
51-
converged = true
52-
break
53-
end
54-
5554
# apply preconditioner and prepare next iteration
5655
ldiv!(c, precon, r)
5756
γ_prev, γ = γ, dot(r, c)

src/response/chi0.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ end
349349
# occupation_threshold for which we compute the associated response δψn,
350350
# the others being discarded to ψ_extra.
351351
# We then use the extra information we have from these additional bands,
352-
# non-necessarily converged, to split the sternheimer_solver with a Schur
352+
# non-necessarily converged, to split the Sternheimer_solver with a Schur
353353
# complement.
354354
occ_thresh = occupation_threshold
355355
mask_occ = map(occk -> findall(occnk -> abs(occnk) occ_thresh, occk), occupation)

test/phonon/ewald.jl

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,67 @@
1+
@testsetup module PhononEwald
2+
using DFTK
3+
4+
function model_tested(lattice::AbstractMatrix, atoms::Vector{<:DFTK.Element},
5+
positions::Vector{<:AbstractVector}; kwargs...)
6+
terms = [Kinetic(),
7+
Ewald()]
8+
if :temperature in keys(kwargs) && kwargs[:temperature] != 0
9+
terms = [terms..., Entropy()]
10+
end
11+
Model(lattice, atoms, positions; model_name="atomic", terms, kwargs...)
12+
end
13+
end
14+
115
@testitem "Phonon: Ewald: comparison to ref testcase" #=
2-
=# tags=[:phonon, :dont_test_mpi] setup=[Phonon, TestCases] begin
16+
=# tags=[:phonon, :dont_test_mpi] setup=[Phonon, PhononEwald, TestCases] begin
317
using DFTK
18+
using .Phonon: test_frequencies
19+
using .PhononEwald: model_tested
420

5-
testcase = TestCases.silicon
6-
terms = [Ewald()]
7-
ω_ref = [ -0.2442311083805831
8-
-0.24423110838058237
9-
-0.23442208238107232
10-
-0.23442208238107184
11-
-0.1322944535508822
12-
-0.13229445355088176
13-
-0.10658869539441493
14-
-0.10658869539441468
15-
-0.10658869539441346
16-
-0.10658869539441335
17-
-4.891274318712944e-16
18-
-3.773447798738169e-17
19-
1.659776058962626e-15
20-
0.09553958285993536
21-
0.18062696253387409
22-
0.18062696253387464
23-
0.4959725605665635
24-
0.4959725605665648
25-
0.49597256056656597
26-
0.5498259359834827
27-
0.5498259359834833
28-
0.6536453595829087
29-
0.6536453595829091
30-
0.6536453595829103
31-
0.6536453595829105
32-
0.6961890494198791
33-
0.6961890494198807
34-
0.7251587593311752
35-
0.7251587593311782
36-
0.9261195383192374
37-
0.9261195383192381
38-
1.2533843205271504
39-
1.2533843205271538
40-
1.7010950724885228
41-
1.7010950724885254
42-
1.752506588801463 ]
43-
Phonon.test_frequencies(testcase, terms, ω_ref)
21+
ω_ref = [ -3.720615299046614e-12
22+
1.969314371029982e-11
23+
1.9739956911274832e-11
24+
0.00029302379784864935
25+
0.0002930237978486494
26+
0.000293023797851601
27+
0.0002930237978516018
28+
0.0005105451353059533
29+
0.0005105451353059533
30+
0.000510545135311239
31+
0.0005105451353112397
32+
0.0005676024288436319
33+
0.000591265950289604
34+
0.0005912659502958081
35+
0.0007328535013566558
36+
0.0007328535013566561
37+
0.0008109743140779055
38+
0.0008109743140779056
39+
0.000938673782810113
40+
0.000987619635716976
41+
0.0009876196357169761
42+
0.0010949497272589232
43+
0.0011998186659486743
44+
0.0011998186659486745
45+
0.001523238357971607
46+
0.0019593679918607546
47+
0.0022394777249719524
48+
0.0022394777249719524
49+
0.0024681196094789985
50+
0.0024681196094789993
51+
0.0024809296524054506
52+
0.0025805236057819345
53+
0.002614761988704579
54+
0.002614761988704579
55+
0.0026807773193116675
56+
0.0026807773193116675 ]
57+
test_frequencies(model_tested, TestCases.magnesium; ω_ref)
4458
end
4559

4660
@testitem "Phonon: Ewald: comparison to automatic differentiation" #=
47-
=# tags=[:phonon, :slow, :dont_test_mpi] setup=[Phonon, TestCases] begin
61+
=# tags=[:phonon, :slow, :dont_test_mpi] setup=[Phonon, PhononEwald, TestCases] begin
4862
using DFTK
49-
testcase = TestCases.silicon
63+
using .Phonon: test_frequencies
64+
using .PhononEwald: model_tested
5065

51-
terms = [Ewald()]
52-
Phonon.test_rand_frequencies(testcase, terms)
66+
test_frequencies(model_tested, TestCases.magnesium)
5367
end

test/phonon/helpers.jl

Lines changed: 84 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,10 @@
22
@testsetup module Phonon
33
using Test
44
using DFTK
5-
using DFTK: TermAtomicLocal, TermAtomicNonlocal
6-
using DFTK: compute_dynmat_cart, setindex, dynmat_red_to_cart, normalize_kpoint_coordinate
5+
using DFTK: normalize_kpoint_coordinate
76
using LinearAlgebra
87
using ForwardDiff
9-
10-
# We do not take the square root to compare eigenvalues with machine precision.
11-
function squared_frequencies(matrix)
12-
n, m = size(matrix, 1), size(matrix, 2)
13-
Ω = eigvals(reshape(matrix, n*m, n*m))
14-
real(Ω)
15-
end
16-
17-
# Reference against automatic differentiation.
18-
function reference_squared_frequencies(basis; kwargs...)
19-
model = basis.model
20-
n_atoms = length(model.positions)
21-
n_dim = model.n_dim
22-
T = eltype(model.lattice)
23-
dynmat_ad = zeros(T, 3, n_atoms, 3, n_atoms)
24-
for s = 1:n_atoms, α = 1:n_dim
25-
displacement = zero.(model.positions)
26-
displacement[s] = setindex(displacement[s], one(T), α)
27-
dynmat_ad[:, :, α, s] = -ForwardDiff.derivative(zero(T)) do ε
28-
lattice = convert(Matrix{eltype(ε)}, model.lattice)
29-
positions = ε*displacement .+ model.positions
30-
model_disp = Model(convert(Model{eltype(ε)}, model); lattice, positions)
31-
# TODO: Would be cleaner with PR #675.
32-
basis_disp_bs = PlaneWaveBasis(model_disp; Ecut=5)
33-
forces = compute_forces(basis_disp_bs, nothing, nothing)
34-
stack(forces)
35-
end
36-
end
37-
hessian_ad = DFTK.dynmat_red_to_cart(model, dynmat_ad)
38-
sort(squared_frequencies(hessian_ad))
39-
end
8+
using FiniteDifferences
409

4110
function generate_random_supercell(; max_length=6)
4211
n_max = min(max_length, 5)
@@ -57,47 +26,97 @@ function generate_supercell_qpoints(; supercell_size=generate_random_supercell()
5726
(; supercell_size, qpoints)
5827
end
5928

60-
# Test against a reference array.
61-
function test_frequencies(testcase, terms, ω_ref; tol=1e-9, supercell_size=[2, 1, 3])
62-
model = Model(testcase.lattice, testcase.atoms, testcase.positions; terms)
63-
basis_bs = PlaneWaveBasis(model; Ecut=5)
29+
function test_approx_frequencies(ω_uc, ω_ref; tol=1e-10)
30+
# Because three eigenvalues should be close to zero and the square root near
31+
# zero decrease machine accuracy, we expect at least ``3×2×2 - 3 = 9``
32+
# eigenvalues to have norm related to the accuracy of the SCF convergence
33+
# parameter and the rest to be larger.
34+
n_dim = 3
35+
n_atoms = length(ω_uc) ÷ 3
36+
37+
@test count(abs.(ω_uc - ω_ref) .< sqrt(tol)) n_dim*n_atoms - n_dim
38+
@test count(sqrt(tol) .< abs.(ω_uc - ω_ref) .< tol) n_dim
39+
end
40+
41+
function test_frequencies(model_tested, testcase; ω_ref=nothing, Ecut=7, kgrid=[2, 1, 3],
42+
tol=1e-12, randomize=false, compute_ref=nothing)
43+
supercell_size = randomize ? generate_random_supercell() : kgrid
44+
qpoints = generate_supercell_qpoints(; supercell_size).qpoints
45+
scf_tol = tol
46+
χ0_tol = scf_tol/10
47+
scf_kwargs = (; is_converged=ScfConvergenceDensity(scf_tol),
48+
diagtolalg=AdaptiveDiagtol(; diagtol_max=scf_tol))
6449

65-
phonon = (; supercell_size, generate_supercell_qpoints(; supercell_size).qpoints)
50+
model = model_tested(testcase.lattice, testcase.atoms, testcase.positions;
51+
symmetries=false, testcase.temperature)
52+
nbandsalg = AdaptiveBands(model; occupation_threshold=1e-10)
53+
scf_kwargs = merge(scf_kwargs, (; nbandsalg))
54+
basis = PlaneWaveBasis(model; Ecut, kgrid)
55+
scfres = self_consistent_field(basis; scf_kwargs...)
6656

67-
ω_uc = sort!(reduce(vcat, map(phonon.qpoints) do q
68-
hessian = compute_dynmat_cart(basis_bs, [], []; q)
69-
squared_frequencies(hessian)
57+
ω_uc = sort!(reduce(vcat, map(qpoints) do q
58+
dynamical_matrix = compute_dynmat(scfres; q, tol=χ0_tol)
59+
phonon_modes_cart(basis, dynamical_matrix).frequencies
7060
end))
7161

72-
@test norm(ω_uc - ω_ref) < tol
73-
end
62+
!isnothing(ω_ref) && return test_approx_frequencies(ω_uc, ω_ref; tol=10scf_tol)
7463

75-
# Random test. Slow but more robust than against some reference.
76-
# TODO: Will need rework for local term in future PR.
77-
function test_rand_frequencies(testcase, terms; tol=1e-9)
78-
model = Model(testcase.lattice, testcase.atoms, testcase.positions; terms)
79-
basis_bs = PlaneWaveBasis(model; Ecut=5)
64+
supercell = create_supercell(testcase.lattice, testcase.atoms, testcase.positions,
65+
supercell_size)
66+
model_supercell = model_tested(supercell.lattice, supercell.atoms, supercell.positions;
67+
symmetries=false, testcase.temperature)
68+
nbandsalg = AdaptiveBands(model_supercell; occupation_threshold=1e-10)
69+
scf_kwargs = merge(scf_kwargs, (; nbandsalg))
70+
basis_supercell = PlaneWaveBasis(model_supercell; Ecut, kgrid=[1, 1, 1])
71+
scfres_supercell = self_consistent_field(basis_supercell; scf_kwargs...)
8072

81-
supercell_size = supercell_size=generate_random_supercell()
82-
phonon = (; supercell_size, generate_supercell_qpoints(; supercell_size).qpoints)
73+
dynamical_matrix_sc = compute_dynmat(scfres_supercell; tol=χ0_tol)
74+
ω_sc = sort(phonon_modes_cart(basis_supercell, dynamical_matrix_sc).frequencies)
75+
test_approx_frequencies(ω_uc, ω_sc; tol=10scf_tol)
8376

84-
ω_uc = []
85-
for q in phonon.qpoints
86-
hessian = compute_dynmat_cart(basis_bs, [], []; q)
87-
push!(ω_uc, squared_frequencies(hessian))
88-
end
89-
ω_uc = sort!(collect(Iterators.flatten(ω_uc)))
77+
isnothing(compute_ref) && return
9078

91-
supercell = create_supercell(testcase.lattice, testcase.atoms, testcase.positions,
92-
phonon.supercell_size)
93-
model_supercell = Model(supercell.lattice, supercell.atoms, supercell.positions; terms)
94-
basis_supercell_bs = PlaneWaveBasis(model_supercell; Ecut=5)
95-
hessian_supercell = compute_dynmat_cart(basis_supercell_bs, [], [])
96-
ω_supercell = sort(squared_frequencies(hessian_supercell))
97-
@test norm(ω_uc - ω_supercell) < tol
79+
dynamical_matrix_ref = compute_dynmat_ref(scfres_supercell.basis, model_tested; Ecut,
80+
kgrid=[1, 1, 1], scf_tol, method=compute_ref)
81+
ω_ref = sort(phonon_modes_cart(basis_supercell, dynamical_matrix_ref).frequencies)
82+
83+
test_approx_frequencies(ω_uc, ω_ref; tol=10scf_tol)
84+
end
9885

99-
ω_ad = reference_squared_frequencies(basis_supercell_bs)
86+
# Reference results using finite differences or automatic differentiation.
87+
# This should be run by hand to obtain the reference values of the quick computations of the
88+
# tests, as they are too slow for CI runs.
89+
function compute_dynmat_ref(basis, model_tested; Ecut=5, kgrid=[1,1,1], scf_tol, method=:ad)
90+
# TODO: Cannot use symmetries: https://github.com/JuliaMolSim/DFTK.jl/issues/817
91+
@assert isone(only(basis.model.symmetries))
92+
@assert method [:ad, :fd]
10093

101-
@test norm(ω_ad - ω_supercell) < tol
94+
model = basis.model
95+
n_atoms = length(model.positions)
96+
n_dim = model.n_dim
97+
T = eltype(model.lattice)
98+
dynmat = zeros(T, 3, n_atoms, 3, n_atoms)
99+
scf_kwargs = (; is_converged=ScfConvergenceDensity(scf_tol),
100+
diagtolalg=AdaptiveDiagtol(; diagtol_max=scf_tol))
101+
102+
diff_fn = method == :ad ? ForwardDiff.derivative : FiniteDifferences.central_fdm(5, 1)
103+
for s = 1:n_atoms, α = 1:n_dim
104+
displacement = zero.(model.positions)
105+
displacement[s] = DFTK.setindex(displacement[s], one(T), α)
106+
dynmat[:, :, α, s] = -diff_fn(zero(T)) do ε
107+
lattice = convert(Matrix{eltype(ε)}, model.lattice)
108+
positions = ε*displacement .+ model.positions
109+
model_disp = model_tested(lattice, model.atoms, positions; symmetries=false,
110+
model.temperature)
111+
# TODO: Would be cleaner with PR #675.
112+
basis_disp = PlaneWaveBasis(model_disp; Ecut, kgrid)
113+
nbandsalg = AdaptiveBands(model_disp; occupation_threshold=1e-10)
114+
scf_kwargs = merge(scf_kwargs, (; nbandsalg))
115+
scfres_disp = self_consistent_field(basis_disp; scf_kwargs...)
116+
forces = compute_forces(scfres_disp)
117+
stack(forces)
118+
end
119+
end
120+
dynmat
102121
end
103122
end

0 commit comments

Comments
 (0)