DFTK includes an implementation of the strategy from [^CDKL2022] to compute practical error bounds for forces and other quantities of interest.
This is an example showing how to compute error estimates for the forces on a TiO2 molecule, from which we can either compute asymptotically valid error bounds or increase the precision on the computation of the forces.
[^CDKL2022]: E. Cancès, G. Dusson, G. Kemlin, and A. Levitt Practical error bounds for properties in plane-wave electronic structure calculations SIAM Journal on Scientific Computing 44 (5), B1312-B1340
using DFTK
using PseudoPotentialData
using Printf
using LinearAlgebra
using ForwardDiff
We setup manually the TiO2 configuration from Materials Project.
pseudopotentials = PseudoFamily("cp2k.nc.sr.lda.v0_1.largecore.gth")
Ti = ElementPsp(:Ti, pseudopotentials)
O = ElementPsp(:O, pseudopotentials)
atoms = [Ti, Ti, O, O, O, O]
positions = [[0.5, 0.5, 0.5], # Ti
[0.0, 0.0, 0.0], # Ti
[0.19542, 0.80458, 0.5], # O
[0.80458, 0.19542, 0.5], # O
[0.30458, 0.30458, 0.0], # O
[0.69542, 0.69542, 0.0]] # O
lattice = [[8.79341 0.0 0.0];
[0.0 8.79341 0.0];
[0.0 0.0 5.61098]];
We apply a small displacement to one of the Ti atoms to get nonzero forces.
positions[1] .+= [-0.022, 0.028, 0.035]
3-element Vector{Float64}: 0.478 0.528 0.535
We build a model with one k-point only, not too high Ecut_ref
and small
tolerance to limit computational time. These parameters can be increased for
more precise results.
model = model_DFT(lattice, atoms, positions; functionals=LDA())
kgrid = [1, 1, 1]
Ecut_ref = 35
basis_ref = PlaneWaveBasis(model; Ecut=Ecut_ref, kgrid)
tol = 1e-5;
We also build a basis with smaller Ecut
, to compute a variational approximation of the reference solution.
Choice of convergence parameters
Be careful to choose
Ecut
not too close toEcut_ref
. Note also that the current choiceEcut_ref = 35
is such that the reference solution is not converged andEcut = 15
is such that the asymptotic regime (crucial to validate the approach) is barely established.
Ecut = 15
basis = PlaneWaveBasis(model; Ecut, kgrid);
Compute the solution on the smaller basis:
scfres = self_consistent_field(basis; tol, callback=identity);
Compute first order corrections refinement.δψ
and refinement.δρ
.
Note that refinement.ψ
and refinement.ρ
are the quantities computed with Ecut
and then extended to the reference grid.
This step is roughly as expensive as the self_consistent_field
call above.
refinement = refine_scfres(scfres, basis_ref; tol, callback=identity);
f = compute_forces(scfres)
6-element Vector{StaticArraysCore.SVector{3, Float64}}: [0.03418855950413535, -0.016276394305248854, 0.025767637977328173] [-0.5139216515045787, 0.6479201505659945, 0.2170681817193305] [-0.5238675354873454, 0.4801368623775526, -0.15971058202151486] [0.777613457067849, -0.8064496580824967, -0.12223539112573223] [0.6154633962454343, 0.3547562213960296, -0.0020629086600183477] [-0.3894877436373152, -0.6601071031231927, 0.04160786043271103]
force_refinement = refine_forces(refinement)
forces_refined = f + force_refinement.dF
6-element Vector{StaticArraysCore.SVector{3, Float64}}: [-0.21301722681824328, 0.2730113145648059, 0.29520434209420665] [-0.5681968786100761, 0.72046263411358, 0.2549407435641571] [-0.3622378313098461, 0.32855929296399394, -0.1598791007347069] [0.9488413396929462, -0.9818851370497104, -0.13380603475680183] [0.6787075799839407, 0.41328591303068946, -0.14153407483203972] [-0.48592754346491185, -0.7512568996296056, -0.11259874995984637]
A practical estimate of the error on the forces is then the following:
dF_estimate = forces_refined - f
6-element Vector{StaticArraysCore.SVector{3, Float64}}: [-0.24720578632237863, 0.2892877088700547, 0.2694367041168785] [-0.05427522710549737, 0.07254248354758552, 0.03787256184482657] [0.16162970417749933, -0.15157756941355865, -0.0001685187131920396] [0.17122788262509714, -0.17543547896721368, -0.011570643631069605] [0.06324418373850638, 0.058529691634659875, -0.13947116617202138] [-0.09643979982759665, -0.09114979650641297, -0.1542066103925574]
For practical computations one can stop at forces_refined
and dF_estimate
.
We continue here with a comparison of different ways to obtain the refined forces,
noting that the computational cost is much higher.
We compute the reference solution P∗ from which we will compute the references forces.
scfres_ref = self_consistent_field(basis_ref; tol, callback=identity)
ψ_ref = DFTK.select_occupied_orbitals(basis_ref, scfres_ref.ψ, scfres_ref.occupation).ψ;
function compute_error(ϕ, ψ)
map(zip(ϕ, ψ)) do (ϕk, ψk)
S = ψk'ϕk
U = S*(S'S)^(-1/2)
ϕk - ψk*U
end
end
error = compute_error(refinement.ψ, ψ_ref);
We start with different estimations of the forces:
f_ref = compute_forces(scfres_ref)
forces = Dict("F(P_*)" => f_ref)
relerror = Dict("F(P_*)" => 0.0)
compute_relerror(f) = norm(f - f_ref) / norm(f_ref);
forces["F(P)"] = f
relerror["F(P)"] = compute_relerror(f);
We then try to improve F(P) using the first order linearization:
F(P)=F(P∗)+dF(P)·(P−P∗).To this end, we use the ForwardDiff.jl
package to compute dF(P)
using automatic differentiation.
function df(basis, occupation, ψ, δψ, ρ)
δρ = DFTK.compute_δρ(basis, ψ, δψ, occupation)
ForwardDiff.derivative(ε -> compute_forces(basis, ψ.+ε.*δψ, occupation; ρ=ρ+ε.*δρ), 0)
end;
df_err = df(basis_ref, refinement.occupation, refinement.ψ,
DFTK.proj_tangent(error, refinement.ψ), refinement.ρ)
forces["F(P) - df(P)⋅(P-P_*)"] = f - df_err
relerror["F(P) - df(P)⋅(P-P_*)"] = compute_relerror(f - df_err);
forces["F(P) - df(P)⋅Rschur(P)"] = forces_refined
relerror["F(P) - df(P)⋅Rschur(P)"] = compute_relerror(forces_refined);
Summary of all forces on the first atom (Ti)
for (key, value) in pairs(forces)
@printf("%30s = [%7.5f, %7.5f, %7.5f] (rel. error: %7.5f)\n",
key, (value[1])..., relerror[key])
end
F(P_*) = [-0.29996, 0.37533, 0.44083] (rel. error: 0.00000) F(P) = [0.03419, -0.01628, 0.02577] (rel. error: 0.41550) F(P) - df(P)⋅Rschur(P) = [-0.21302, 0.27301, 0.29520] (rel. error: 0.14681) F(P) - df(P)⋅(P-P_*) = [-0.32017, 0.38917, 0.43183] (rel. error: 0.08583)
Notice how close the computable expression F(P)−dF(P)⋅RSchur(P) is to the best linearization ansatz F(P)−dF(P)⋅(P−P∗).