Skip to content

Commit 908e3c8

Browse files
fsicignanoTechnici4nmfherbst
authored
Add Hubbard corrections and DFT+U (#1158)
--------- Co-authored-by: Bruno Ploumhans <[email protected]> Co-authored-by: Michael F. Herbst <[email protected]>
1 parent 3551ad3 commit 908e3c8

File tree

18 files changed

+535
-80
lines changed

18 files changed

+535
-80
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ ASEconvert = "0.1"
7777
AbstractFFTs = "1"
7878
Aqua = "0.8.5"
7979
Artifacts = "1"
80-
AtomsBase = "0.5"
80+
AtomsBase = "0.5.2"
8181
AtomsBuilder = "0.2"
8282
AtomsCalculators = "0.2.3"
8383
AtomsIO = "0.3"

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ PAGES = [
4242
"examples/graphene.jl",
4343
"examples/geometry_optimization.jl",
4444
"examples/energy_cutoff_smearing.jl",
45+
"examples/hubbard.jl",
4546
],
4647
"Response and properties" => [
4748
"examples/elastic_constants.jl",

docs/src/features.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and runs out of the box on Linux, windows and macOS, see [Installation](@ref).
88
## Standard methods and models
99
- DFT models (LDA, GGA, meta-GGA): Any functional from the
1010
[libxc](https://libxc.gitlab.io/) library
11+
- DFT+U and Hubbard corrections
1112
- **Norm-conserving pseudopotentials**: Goedecker-type (GTH)
1213
or numerical (in UPF pseudopotential format),
1314
see [Pseudopotentials](@ref).
@@ -25,7 +26,7 @@ and runs out of the box on Linux, windows and macOS, see [Installation](@ref).
2526

2627
## Ground-state properties and post-processing
2728
- Total energy, forces, stresses
28-
- Density of states (DOS), local density of states (LDOS)
29+
- Density of states (DOS), local density of states (LDOS), projected density of states (PDOS)
2930
- Band structures
3031
- Easy access to all intermediate quantities (e.g. density, Bloch waves)
3132

examples/hubbard.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# # Hubbard correction (DFT+U)
2+
# In this example, we'll plot the DOS and projected DOS of Nickel Oxide
3+
# with and without the Hubbard term correction.
4+
5+
using DFTK
6+
using PseudoPotentialData
7+
using Unitful
8+
using UnitfulAtomic
9+
using Plots
10+
11+
# Define the geometry and pseudopotential
12+
a = 7.9 # Nickel Oxide lattice constant in Bohr
13+
lattice = a * [[ 1.0 0.5 0.5];
14+
[ 0.5 1.0 0.5];
15+
[ 0.5 0.5 1.0]]
16+
pseudopotentials = PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf")
17+
Ni = ElementPsp(:Ni, pseudopotentials)
18+
O = ElementPsp(:O, pseudopotentials)
19+
atoms = [Ni, O, Ni, O]
20+
positions = [zeros(3), ones(3) / 4, ones(3) / 2, ones(3) * 3 / 4]
21+
magnetic_moments = [2, 0, -1, 0]
22+
23+
# First, we run an SCF and band computation without the Hubbard term
24+
model = model_DFT(lattice, atoms, positions; temperature=5e-3,
25+
functionals=PBE(), magnetic_moments)
26+
basis = PlaneWaveBasis(model; Ecut=20, kgrid=[2, 2, 2])
27+
scfres = self_consistent_field(basis; tol=1e-6, ρ=guess_density(basis, magnetic_moments))
28+
bands = compute_bands(scfres, MonkhorstPack(4, 4, 4))
29+
lowest_unocc_band = findfirst-> ε-bands.εF > 0, bands.eigenvalues[1])
30+
band_gap = bands.eigenvalues[1][lowest_unocc_band] - bands.eigenvalues[1][lowest_unocc_band-1]
31+
32+
# Then we plot the DOS and the PDOS for the relevant 3D (pseudo)atomic projector
33+
εF = bands.εF
34+
width = 5.0u"eV"
35+
εrange = (εF - austrip(width), εF + austrip(width))
36+
p = plot_dos(bands; εrange, colors=[:red, :red])
37+
plot_pdos(bands; p, iatom=1, label="3D", colors=[:yellow, :orange], εrange)
38+
39+
# To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant.
40+
#
41+
# In DFTK there are a few ways to construct the `OrbitalManifold`.
42+
# Here, we will apply the Hubbard correction on the 3D orbital of all nickel atoms.
43+
#
44+
# Note that "manifold" is the standard term used in the literature for the set of atomic orbitals
45+
# used to compute the Hubbard correction, but it is not meant in the mathematical sense.
46+
U = 10u"eV"
47+
manifold = OrbitalManifold(atoms, Ni, "3D")
48+
49+
# Run SCF with a DFT+U setup, notice the `extra_terms` keyword argument, setting up the Hubbard +U term.
50+
model = model_DFT(lattice, atoms, positions; extra_terms=[Hubbard(manifold, U)],
51+
functionals=PBE(), temperature=5e-3, magnetic_moments)
52+
basis = PlaneWaveBasis(model; Ecut=20, kgrid=[2, 2, 2])
53+
scfres = self_consistent_field(basis; tol=1e-6, ρ=guess_density(basis, magnetic_moments))
54+
55+
# Run band computation
56+
bands_hub = compute_bands(scfres, MonkhorstPack(4, 4, 4))
57+
lowest_unocc_band = findfirst-> ε-bands_hub.εF > 0, bands_hub.eigenvalues[1])
58+
band_gap = bands_hub.eigenvalues[1][lowest_unocc_band] - bands_hub.eigenvalues[1][lowest_unocc_band-1]
59+
60+
# With the electron localization introduced by the Hubbard term, the band gap has now opened,
61+
# reflecting the experimental insulating behaviour of Nickel Oxide.
62+
εF = bands_hub.εF
63+
εrange = (εF - austrip(width), εF + austrip(width))
64+
p = plot_dos(bands_hub; p, colors=[:blue, :blue], εrange)
65+
plot_pdos(bands_hub; p, iatom=1, label="3D", colors=[:green, :purple], εrange)

ext/DFTKPlotsExt.jl

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ end
7676
function plot_dos(basis, eigenvalues; εF=nothing, unit=u"hartree",
7777
temperature=basis.model.temperature,
7878
smearing=basis.model.smearing,
79+
colors=[:blue, :red], p=nothing,
7980
εrange=default_band_εrange(eigenvalues; εF), n_points=1000, kwargs...)
8081
# TODO Should also split this up into one stage doing the DOS computation
8182
# and one stage doing the DOS plotting (like now for the bands.)
@@ -87,9 +88,9 @@ function plot_dos(basis, eigenvalues; εF=nothing, unit=u"hartree",
8788
# Constant to convert from AU to the desired unit
8889
to_unit = ustrip(auconvert(unit, 1.0))
8990

90-
p = Plots.plot(; kwargs...)
91+
isnothing(p) && (p = Plots.plot())
92+
p = Plots.plot(p; kwargs...)
9193
spinlabels = spin_components(basis.model)
92-
colors = [:blue, :red]
9394
Dεs = compute_dos.(εs, Ref(basis), Ref(eigenvalues); smearing, temperature)
9495
for σ = 1:n_spin
9596
D = [Dσ[σ] forin Dεs]
@@ -141,7 +142,7 @@ function plot_ldos(basis, eigenvalues, ψ; εF=nothing, unit=u"hartree",
141142
end
142143
plot_ldos(scfres; kwargs...) = plot_ldos(scfres.basis, scfres.eigenvalues, scfres.ψ; scfres.εF, kwargs...)
143144

144-
function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom, label=nothing,
145+
function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom=nothing, label=nothing,
145146
positions=basis.model.positions,
146147
εF=nothing, unit=u"hartree",
147148
temperature=basis.model.temperature,
@@ -154,16 +155,17 @@ function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom, label=nothi
154155
n_spin = basis.model.n_spin_components
155156
to_unit = ustrip(auconvert(unit, 1.0))
156157

157-
species = isnothing(iatom) ? "all atoms" : "atom $(iatom) ($(basis.model.atoms[iatom].species))"
158+
species = isnothing(iatom) ? "all atoms" : "atom $(iatom) ($(basis.model.atoms[iatom].species))"
158159
orb_name = isnothing(label) ? "all orbitals" : label
159160

160161
# Plot pdos
161-
isnothing(p) && (p = Plots.plot(; kwargs...))
162+
isnothing(p) && (p = Plots.plot())
162163
p = Plots.plot(p; kwargs...)
163164
spinlabels = spin_components(basis.model)
164165
pdos = DFTK.sum_pdos(compute_pdos(εs, basis, ψ, eigenvalues;
165-
positions, temperature, smearing),
166-
[DFTK.OrbitalManifold(;iatom, label)])
166+
positions, temperature, smearing),
167+
[l -> ((isnothing(iatom) || l.iatom == iatom)
168+
&& (isnothing(label) || l.label == label))])
167169
for σ = 1:n_spin
168170
plot_label = n_spin > 1 ? "$(species) $(orb_name) $(spinlabels[σ]) spin" : "$(species) $(orb_name)"
169171
Plots.plot!(p, (εs .- eshift) .* to_unit, pdos[:, σ]; label=plot_label, color=colors[σ])

src/DFTK.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ export AtomicNonlocal
112112
export Ewald
113113
export PspCorrection
114114
export Entropy
115+
export Hubbard
116+
export OrbitalManifold
115117
export Magnetic
116118
export PairwisePotential
117119
export Anyonic

src/common/spherical_harmonics.jl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,33 @@ function ylm_real(l::Integer, m::Integer, rvec::AbstractVector{T}) where {T}
4747

4848
error("The case l = $l and m = $m is not implemented")
4949
end
50+
51+
"""
52+
This function returns the Wigner D matrix for real spherical harmonics,
53+
for a given l and orthogonal matrix, solving a randomized linear system.
54+
Such D matrix gives the decomposition of a spherical harmonic after application
55+
of an orthogonal matrix back into the basis of spherical harmonics.
56+
57+
Yₗₘ₁(Wr) = Σₘ₂ D(l,R̂)ₘ₁ₘ₂ * Yₗₘ₂(r)
58+
"""
59+
function wigner_d_matrix(l::Integer, Wcart::AbstractMatrix{T}) where {T}
60+
if l == 0 # In this case no computation is needed
61+
return [one(T);;]
62+
end
63+
rng = Xoshiro(1234)
64+
neq = 2l+2 # We need at least 2l+1 equations, we add one for numerical stability
65+
B = Matrix{T}(undef, 2l+1, neq)
66+
A = Matrix{T}(undef, 2l+1, neq)
67+
for n in 1:neq
68+
r = rand(rng, T, 3)
69+
r = r / norm(r)
70+
r0 = Wcart * r
71+
for m in -l:l
72+
B[m+l+1, n] = ylm_real(l, m, r0)
73+
A[m+l+1, n] = ylm_real(l, m, r)
74+
end
75+
end
76+
κ = cond(A)
77+
@assert κ < 100.0 "The Wigner matrix computation is badly conditioned. κ(A)=$(κ)"
78+
B / A
79+
end

src/postprocess/band_structure.jl

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref):
1414
@timing function compute_bands(basis::PlaneWaveBasis,
1515
kgrid::Union{AbstractKgrid,AbstractKgridGenerator};
1616
n_bands=default_n_bands_bandstructure(basis.model),
17-
n_extra=3, ρ=nothing, τ=nothing, εF=nothing,
18-
eigensolver=lobpcg_hyper, tol=1e-3, seed=nothing,
19-
kwargs...)
17+
n_extra=3, ρ=nothing, τ=nothing, hubbard_n=nothing,
18+
εF=nothing, eigensolver=lobpcg_hyper, tol=1e-3,
19+
seed=nothing, kwargs...)
2020
# kcoords are the kpoint coordinates in fractional coordinates
2121
if isnothing(ρ)
2222
if any(t isa TermNonlinear for t in basis.terms)
@@ -36,7 +36,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref):
3636
# Create new basis with new kpoints
3737
bs_basis = PlaneWaveBasis(basis, kgrid)
3838

39-
ham = Hamiltonian(bs_basis; ρ, τ)
39+
ham = Hamiltonian(bs_basis; ρ, τ, hubbard_n)
4040
eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands + n_extra;
4141
n_conv_check=n_bands, tol, kwargs...)
4242
if !eigres.converged
@@ -68,7 +68,10 @@ function compute_bands(scfres::NamedTuple,
6868
kgrid::Union{AbstractKgrid,AbstractKgridGenerator};
6969
n_bands=default_n_bands_bandstructure(scfres), kwargs...)
7070
τ = haskey(scfres, ) ? scfres.τ : nothing
71-
compute_bands(scfres.basis, kgrid; scfres.ρ, τ, scfres.εF, n_bands, kwargs...)
71+
hubbard_n = haskey(scfres, :hubbard_n) ? scfres.hubbard_n : nothing
72+
compute_bands(scfres.basis, kgrid;
73+
scfres.ρ, τ, hubbard_n,
74+
scfres.εF, n_bands, kwargs...)
7275
end
7376

7477
"""

src/postprocess/dos.jl

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -119,31 +119,6 @@ function compute_pdos(εs, bands; kwargs...)
119119
compute_pdos(εs, bands.basis, bands.ψ, bands.eigenvalues; kwargs...)
120120
end
121121

122-
"""
123-
Structure for manifold choice and projectors extraction.
124-
125-
Overview of fields:
126-
- `iatom`: Atom position in the atoms array.
127-
- `species`: Chemical Element as in ElementPsp.
128-
- `label`: Orbital name, e.g.: "3S".
129-
130-
All fields are optional, only the given ones will be used for selection.
131-
Can be called with an orbital NamedTuple and returns a boolean
132-
stating whether the orbital belongs to the manifold.
133-
"""
134-
@kwdef struct OrbitalManifold
135-
iatom ::Union{Int64, Nothing} = nothing
136-
species ::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing} = nothing
137-
label ::Union{String, Nothing} = nothing
138-
end
139-
function (s::OrbitalManifold)(orb)
140-
iatom_match = isnothing(s.iatom) || (s.iatom == orb.iatom)
141-
# See JuliaMolSim/AtomsBase.jl#139 why both species equalities are needed
142-
species_match = isnothing(s.species) || (s.species == orb.species) || (orb.species == s.species)
143-
label_match = isnothing(s.label) || (s.label == orb.label)
144-
iatom_match && species_match && label_match
145-
end
146-
147122
"""
148123
atomic_orbital_projectors(basis; [isonmanifold, positions])
149124
@@ -161,8 +136,8 @@ The projectors are computed by decomposition into a form factor multiplied by a
161136
162137
Overview of inputs:
163138
- `positions` : Positions of the atoms in the unit cell. Default is model.positions.
164-
- `isonmanifold` (opt) : (see notes below) OrbitalManifold struct to select only a subset of orbitals
165-
for the computation.
139+
- `isonmanifold` (opt) : (see notes below) A function, typically a lambda,
140+
to select projectors to include in the pdos.
166141
167142
Overview of outputs:
168143
- `projectors`: Vector of matrices of projectors
@@ -191,6 +166,7 @@ function atomic_orbital_projectors(basis::PlaneWaveBasis{T};
191166
labels = []
192167
for (iatom, atom) in enumerate(basis.model.atoms)
193168
psp = atom.psp
169+
@assert count_n_pswfc(psp) > 0 # We need this to check if we have any atomic orbital projector
194170
for l in 0:psp.lmax, n in 1:DFTK.count_n_pswfc_radial(psp, l)
195171
label = DFTK.pswfc_label(psp, n, l)
196172
if !isonmanifold((; iatom, atom.species, label))
@@ -236,23 +212,24 @@ function atomic_orbital_projections(basis::PlaneWaveBasis{T}, ψ;
236212
end
237213

238214
"""
239-
sum_pdos(pdos_res, manifolds)
215+
sum_pdos(pdos_res, projector_filters)
240216
241217
This function extracts and sums up all the PDOSes, directly from the output of the `compute_pdos` function,
242-
that match any of the manifolds.
218+
that match any of the filters.
243219
244220
Overview of inputs:
245221
- `pdos_res`: Whole output from compute_pdos.
246-
- `manifolds`: Vector of OrbitalManifolds to select the desired projectors pdos.
222+
- `projector_filters`: Vector of functions, typically lambdas,
223+
to select projectors to include in the pdos.
247224
248225
Overview of outputs:
249226
- `pdos`: Vector containing the pdos(ε).
250227
"""
251-
function sum_pdos(pdos_res, manifolds::AbstractVector)
228+
function sum_pdos(pdos_res, projector_filters::AbstractVector)
252229
pdos = zeros(Float64, length(pdos_res.εs), size(pdos_res.pdos, 3))
253230
for σ in 1:size(pdos_res.pdos, 3)
254231
for (j, orb) in enumerate(pdos_res.projector_labels)
255-
if any(manifold(orb) for manifold in manifolds)
232+
if any(filt(orb) for filt in projector_filters)
256233
pdos[:, σ] += pdos_res.pdos[:, j, σ]
257234
end
258235
end

src/pseudo/NormConservingPsp.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,13 @@ count_n_pswfc(psp::NormConservingPsp, l) = count_n_pswfc_radial(psp, l) * (2l +
221221
function count_n_pswfc(psp::NormConservingPsp)
222222
sum(l -> count_n_pswfc(psp, l), 0:psp.lmax; init=0)::Int
223223
end
224+
225+
function find_pswfc(psp::NormConservingPsp, label::String)
226+
for l = 0:psp.lmax, i = 1:count_n_pswfc_radial(psp, l)
227+
if pswfc_label(psp, i, l) == label
228+
return (; l, i)
229+
end
230+
end
231+
error("Could not find pseudo atomic orbital with label $label "
232+
* "in pseudopotential $(psp.identifier).")
233+
end

0 commit comments

Comments
 (0)