From bc0e2ebd1583023551e3d120ce6d76bfc2fe4e0d Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 25 Sep 2025 13:48:36 +0200 Subject: [PATCH 01/50] Adding Hubbard --- src/common/spherical_harmonics.jl | 47 +++++ src/eigen/diag.jl | 2 +- src/postprocess/band_structure.jl | 7 +- src/postprocess/dos.jl | 25 --- src/scf/self_consistent_field.jl | 25 ++- src/symmetry.jl | 2 +- src/terms/hubbard.jl | 303 ++++++++++++++++++++++++++++++ src/terms/operators.jl | 32 ++++ src/terms/terms.jl | 1 + test/hamiltonian_consistency.jl | 7 +- test/testcases.jl | 33 ++++ 11 files changed, 445 insertions(+), 39 deletions(-) create mode 100644 src/terms/hubbard.jl diff --git a/src/common/spherical_harmonics.jl b/src/common/spherical_harmonics.jl index ec46b871eb..3842c3dbbf 100644 --- a/src/common/spherical_harmonics.jl +++ b/src/common/spherical_harmonics.jl @@ -47,3 +47,50 @@ function ylm_real(l::Integer, m::Integer, rvec::AbstractVector{T}) where {T} error("The case l = $l and m = $m is not implemented") end + +""" +Returns the ``(l,m)`` real spherical harmonic ``Y_l^m(r)``. Consistent with +[Wikipedia](https://en.wikipedia.org/wiki/Table_of_spherical_harmonics#Real_spherical_harmonics). +""" +function ylm_complex(l::Integer, m::Integer, rvec::AbstractVector{T}) where {T} + @assert 0 ≤ l + @assert -l ≤ m ≤ l + @assert length(rvec) == 3 + x, y, z = rvec + r = norm(rvec) + + if l == 0 # s + (m == 0) && return sqrt(1 / 4T(π)) + end + + # Catch cases of numerically very small r + if r <= 10 * eps(eltype(rvec)) + return zero(T) + end + + if l == 1 # p + (m == 0) && return sqrt(3 / 4T(π)) * z / r + (m == -1) && return sqrt(3 / 8T(π)) * (x - im*y) / r + (m == 1) && return - sqrt(3 / 8T(π)) * (x + im*y) / r + end + + if l == 2 # d + (m == -2) && return sqrt(15 / 32T(π)) * (x - im*y)^2 / r^2 + (m == -1) && return sqrt(15 / 8T(π)) * (x - im*y) * z / r^2 + (m == 0) && return sqrt( 5 / 16T(π)) * (2z^2 - x^2 - y^2) / r^2 + (m == 1) && return - sqrt(15 / 8T(π)) * (x + im*y) * z / r^2 + (m == 2) && return sqrt(15 / 32T(π)) * (x + im*y)^2 / r^2 + end + + if l == 3 # f + (m == -3) && return sqrt( 35 / 64T(π)) * (x - im*y)^3 / r^3 + (m == -2) && return sqrt(105 / 32T(π)) * (x - im*y)^2 * z / r^3 + (m == -1) && return sqrt( 21 / 64T(π)) * (x - im*y) * (4z^2 - x^2 - y^2) / r^3 + (m == 0) && return sqrt( 7 / 16T(π)) * z * (2z^2 - 3x^2 - 3y^2) / r^3 + (m == 1) && return - sqrt( 21 / 64T(π)) * (x + im*y) * (4z^2 - x^2 - y^2) / r^3 + (m == 2) && return sqrt(105 / 32T(π)) * (x + im*y)^2 * z / r^3 + (m == 3) && return - sqrt( 35 / 64T(π)) * (x + im*y)^3 / r^3 + end + + error("The case l = $l and m = $m is not implemented") +end diff --git a/src/eigen/diag.jl b/src/eigen/diag.jl index 116621d55f..700efd8d5e 100644 --- a/src/eigen/diag.jl +++ b/src/eigen/diag.jl @@ -9,7 +9,7 @@ that really does the work, operating on a single ``k``-Block. function diagonalize_all_kblocks(eigensolver, ham::Hamiltonian, nev_per_kpoint::Int; ψguess=nothing, prec_type=PreconditionerTPA, interpolate_kpoints=true, - tol=1e-6, miniter=1, maxiter=100, n_conv_check=nothing) + tol=1e-6, miniter=1, maxiter=100, n_conv_check=nothing, kwargs...) kpoints = ham.basis.kpoints results = Vector{Any}(undef, length(kpoints)) diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index c17a88477c..d28328cca4 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -15,6 +15,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): kgrid::Union{AbstractKgrid,AbstractKgridGenerator}; n_bands=default_n_bands_bandstructure(basis.model), n_extra=3, ρ=nothing, τ=nothing, εF=nothing, + occupation=nothing, n_hub=nothing, eigensolver=lobpcg_hyper, tol=1e-3, kwargs...) # kcoords are the kpoint coordinates in fractional coordinates if isnothing(ρ) @@ -34,7 +35,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): # Create new basis with new kpoints bs_basis = PlaneWaveBasis(basis, kgrid) - ham = Hamiltonian(bs_basis; ρ, τ) + ham = Hamiltonian(bs_basis; ρ, τ, n_hub, occupation, kwargs...) eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands + n_extra; n_conv_check=n_bands, tol, kwargs...) if !eigres.converged @@ -66,7 +67,9 @@ function compute_bands(scfres::NamedTuple, kgrid::Union{AbstractKgrid,AbstractKgridGenerator}; n_bands=default_n_bands_bandstructure(scfres), kwargs...) τ = haskey(scfres, :τ) ? scfres.τ : nothing - compute_bands(scfres.basis, kgrid; scfres.ρ, τ, scfres.εF, n_bands, kwargs...) + n_hub = haskey(scfres, :n_hub) ? scfres.n_hub : nothing + occupation = scfres.occupation + compute_bands(scfres.basis, kgrid; scfres.ρ, τ, n_hub, occupation, scfres.εF, n_bands, kwargs...) end """ diff --git a/src/postprocess/dos.jl b/src/postprocess/dos.jl index 3edf643ec4..3f264d49e5 100644 --- a/src/postprocess/dos.jl +++ b/src/postprocess/dos.jl @@ -119,31 +119,6 @@ function compute_pdos(εs, bands; kwargs...) compute_pdos(εs, bands.basis, bands.ψ, bands.eigenvalues; kwargs...) end -""" -Structure for manifold choice and projectors extraction. - -Overview of fields: -- `iatom`: Atom position in the atoms array. -- `species`: Chemical Element as in ElementPsp. -- `label`: Orbital name, e.g.: "3S". - -All fields are optional, only the given ones will be used for selection. -Can be called with an orbital NamedTuple and returns a boolean - stating whether the orbital belongs to the manifold. -""" -@kwdef struct OrbitalManifold - iatom ::Union{Int64, Nothing} = nothing - species ::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing} = nothing - label ::Union{String, Nothing} = nothing -end -function (s::OrbitalManifold)(orb) - iatom_match = isnothing(s.iatom) || (s.iatom == orb.iatom) - # See JuliaMolSim/AtomsBase.jl#139 why both species equalities are needed - species_match = isnothing(s.species) || (s.species == orb.species) || (orb.species == s.species) - label_match = isnothing(s.label) || (s.label == orb.label) - iatom_match && species_match && label_match -end - """ atomic_orbital_projectors(basis; [isonmanifold, positions]) diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index 6d04cd8435..be1f647c32 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -131,6 +131,7 @@ Overview of parameters: basis::PlaneWaveBasis{T}; ρ=guess_density(basis), τ=any(needs_τ, basis.terms) ? zero(ρ) : nothing, + n_hub=nothing, ψ=nothing, tol=1e-6, is_converged=ScfConvergenceDensity(tol), @@ -157,12 +158,12 @@ Overview of parameters: # We do density mixing in the real representation # TODO support other mixing types function fixpoint_map(ρin, info) - (; ψ, occupation, eigenvalues, εF, n_iter, converged, timedout, τ) = info + (; ψ, occupation, eigenvalues, εF, n_iter, converged, timedout, τ, n_hub) = info n_iter += 1 # Note that ρin is not the density of ψ, and the eigenvalues # are not the self-consistent ones, which makes this energy non-variational - energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρin, τ, eigenvalues, εF) + energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρin, τ, n_hub, eigenvalues, εF) # Diagonalize `ham` to get the new state nextstate = next_density(ham, nbandsalg, fermialg; eigensolver, ψ, eigenvalues, @@ -178,16 +179,24 @@ Overview of parameters: if any(needs_τ, basis.terms) τ = compute_kinetic_energy_density(basis, ψ, occupation) end + for (iterm, term) in enumerate(basis.terms) + if typeof(term)==TermHubbard + n_hub = Vector{Array{Matrix{ComplexF64}}}(undef, 0) + for (iman, manifold) in enumerate(term.manifold) + push!(n_hub, compute_hubbard_nIJ(manifold, basis, ψ, occupation).n_IJ) + end + end + end # Update info with results gathered so far info_next = (; ham, basis, converged, stage=:iterate, algorithm="SCF", - ρin, τ, α=damping, n_iter, nbandsalg.occupation_threshold, + ρin, τ, n_hub, α=damping, n_iter, nbandsalg.occupation_threshold, runtime_ns=time_ns() - start_ns, nextstate..., diagonalization=[nextstate.diagonalization]) # Compute the energy of the new state if compute_consistent_energies - (; energies) = energy(basis, ψ, occupation; ρ=ρout, τ, eigenvalues, εF) + (; energies) = energy(basis, ψ, occupation; ρ=ρout, τ, n_hub, eigenvalues, εF) end history_Etot = vcat(info.history_Etot, energies.total) history_Δρ = vcat(info.history_Δρ, norm(Δρ) * sqrt(basis.dvol)) @@ -209,7 +218,7 @@ Overview of parameters: ρnext, info_next end - info_init = (; ρin=ρ, τ, ψ, occupation=nothing, eigenvalues=nothing, εF=nothing, + info_init = (; ρin=ρ, τ, n_hub, ψ, occupation=nothing, eigenvalues=nothing, εF=nothing, n_iter=0, n_matvec=0, timedout=false, converged=false, history_Etot=T[], history_Δρ=T[]) @@ -219,13 +228,13 @@ Overview of parameters: # We do not use the return value of solver but rather the one that got updated by fixpoint_map # ψ is consistent with ρout, so we return that. We also perform a last energy computation # to return a correct variational energy - (; ρin, ρout, τ, ψ, occupation, eigenvalues, εF, converged) = info - energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρout, τ, eigenvalues, εF) + (; ρin, ρout, τ, n_hub, ψ, occupation, eigenvalues, εF, converged) = info + energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρout, τ, n_hub, eigenvalues, εF) # Callback is run one last time with final state to allow callback to clean up scfres = (; ham, basis, energies, converged, nbandsalg.occupation_threshold, ρ=ρout, τ, α=damping, eigenvalues, occupation, εF, info.n_bands_converge, - info.n_iter, info.n_matvec, ψ, info.diagonalization, stage=:finalize, + info.n_iter, info.n_matvec, ψ, n_hub, info.diagonalization, stage=:finalize, info.history_Δρ, info.history_Etot, info.timedout, mixing, runtime_ns=time_ns() - start_ns, algorithm="SCF") callback(scfres) diff --git a/src/symmetry.jl b/src/symmetry.jl index 24ce4ed332..2e1d16cb22 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -305,7 +305,7 @@ function accumulate_over_symmetries!(ρaccu, ρin, basis::PlaneWaveBasis{T}, sym # Looping over symmetries inside of map! on G vectors allow for a single GPU kernel launch map!(ρaccu, Gs) do G acc = zero(complex(T)) - # Explicit loop over indicies because AMDGPU does not support zip() in map! + # Explicit loop over indices because AMDGPU does not support zip() in map! for i_symm in 1:n_symm invS = symm_invS[i_symm] τ = symm_τ[i_symm] diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl new file mode 100644 index 0000000000..63522b29e7 --- /dev/null +++ b/src/terms/hubbard.jl @@ -0,0 +1,303 @@ +using LinearAlgebra + +""" +Structure for manifold choice and projectors extraction. + +Overview of fields: +- `iatom`: Atom position in the atoms array. +- `species`: Chemical Element as in ElementPsp. +- `label`: Orbital name, e.g.: "3S". + +All fields are optional, only the given ones will be used for selection. +Can be called with an orbital NamedTuple and returns a boolean + stating whether the orbital belongs to the manifold. +""" +@kwdef struct OrbitalManifold + iatom ::Union{Int64, Nothing} = nothing + species ::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing} = nothing + label ::Union{String, Nothing} = nothing +end +function (s::OrbitalManifold)(orb) + iatom_match = isnothing(s.iatom) || (s.iatom == orb.iatom) + # See JuliaMolSim/AtomsBase.jl#139 why both species equalities are needed + species_match = isnothing(s.species) || (s.species == orb.species) || (orb.species == s.species) + label_match = isnothing(s.label) || (s.label == orb.label) + iatom_match && species_match && label_match +end + +function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, + manifold::OrbitalManifold) where {T} + manifold_labels = [] + manifold_projectors = Vector{Matrix{Complex{T}}}(undef, length(basis.kpoints)) + for (iproj, orb) in enumerate(labels) + if Symbol(manifold.species) == Symbol(orb.species) && lowercase(manifold.label) == lowercase(orb.label) + # If the label matches the manifold, we add it to the labels + # This is useful for extracting specific orbitals from the basis + # e.g., (:Si, "3S") will match all 3S orbitals of Si atoms + push!(manifold_labels, (; orb.iatom, orb.species, orb.n, orb.l, orb.m, orb.label)) + end + end + for (ik, projk) in enumerate(projectors) + manifold_projectors[ik] = zeros(Complex{T}, size(projectors[ik], 1), length(manifold_labels)) + for (iproj, orb) in enumerate(manifold_labels) + # Find the index of the projector that matches the manifold label + proj_index = findfirst(p -> p.iatom == orb.iatom && p.species == orb.species && + p.n == orb.n && p.l == orb.l && p.m == orb.m, labels) + if proj_index !== nothing + manifold_projectors[ik][:, iproj] = projk[:, proj_index] + else + @warn "Projector for $(orb.species) with n=$(orb.n), l=$(orb.l), m=$(orb.m) + not found in projectors." + end + end + end + return (; manifold_labels, manifold_projectors) +end + +function compute_overlap_matrix(basis::PlaneWaveBasis{T}; + manifold = nothing, + positions = basis.model.positions) where {T} + proj = atomic_orbital_projectors(basis; manifold, positions) + projectors = proj.projectors + labels = proj.labels + overlap_matrix = Vector{Matrix{T}}(undef, length(basis.kpoints)) + + for (ik, projk) in enumerate(projectors) + overlap_matrix[ik] = abs2.(projk' * projk) + end + + return (; overlap_matrix, projectors, labels) +end + +""" +Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. +""" +function symmetrize_nhub(n_IJ::Array{Matrix{Complex{T}}}, lattice, symmetry, positions) where {T} + # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) + # WARNING: To implement +V this will need to be changed! + + nspins = size(n_IJ, 1) + natoms = size(n_IJ, 2) + nsym = length(symmetry) + l = Int64((size(n_IJ[1, 1, 1], 1)-1)/2) + WigD = Wigner_sym(l, lattice, symmetry) + + # Initialize the n_IJ matrix + ns = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) + for σ in 1:nspins, iatom in 1:natoms, jatom in 1:natoms + ns[σ, iatom, jatom] = zeros(Complex{T}, + size(n_IJ[σ, iatom, jatom],1), + size(n_IJ[σ, iatom, jatom],2)) + end + + for σ in 1:nspins, iatom in 1:natoms, isym in 1:nsym + for m1 in 1:size(ns[σ, iatom, iatom], 1), m2 in 1:size(ns[σ, iatom, iatom], 2) + sym_atom = find_symmetric(iatom, symmetry, isym, positions) + # TODO: Here QE flips spin for time-reversal in collinear systems, should we? + for m0 in 1:size(n_IJ[σ, iatom, iatom], 1), m00 in 1:size(n_IJ[σ, iatom, iatom], 2) + ns[σ, iatom, iatom][m1, m2] += WigD[m0, m1, isym] * + n_IJ[σ, sym_atom, sym_atom][m0, m00] * + WigD[m00, m2, isym] + end + end + end + ns .= ns / nsym + + return ns +end + +""" +Find the symmetric atom index for a given atom and symmetry operation +""" +function find_symmetric(iatom::Int64, symmetry::Vector{SymOp{T}}, + isym::Int64, positions) where {T} + sym_atom = iatom + W, w = symmetry[isym].W, symmetry[isym].w + p = positions[iatom] + p2 = W * p + w + for (jatom, pos) in enumerate(positions) + if isapprox(pos, p2, atol=1e-8) + sym_atom = jatom + break + end + end + return sym_atom +end + +""" + This function returns the Wigner matrix for a given l and symmetry operation + solving a randomized linear system. + The lattice L is needed to convert reduced symmetries to Cartesian space. +""" +function Wigner_sym(l::Int64, L, symmetries::Vector{SymOp{T}}) where {T} + nsym = length(symmetries) + D = Array{Float64}(undef, 2*l+1, 2*l+1, nsym) + if l == 0 + return D .= 1 + end + for (isym, symmetry) in enumerate(symmetries) + W = symmetry.W + for m1 in -l:l + b = Vector{Float64}(undef, 2*l+1) + A = Matrix{Float64}(undef, 2*l+1, 2*l+1) + for n in 1:2*l+1 + r = rand(Float64, 3) + r0 = L * W * inv(L) * r + b[n] = DFTK.ylm_real(l, m1, r0) + for m2 in -l:l + A[n,m2+l+1] = DFTK.ylm_real(l, m2, r) + end + end + D[m1+l+1,:,isym] = A\b + end + end + + return D +end + +""" + compute_hubbard_nIJ(manifold, basis, ψ, occupation; [projectors, labels, positions]) + +Computes a matrix nᴵᴶ of size (nspins, natoms, natoms), where each entry nᴵᴶ[iatom, jatom] + contains the submatrix of the occupation matrix corresponding to the projectors + of atom iatom and atom jatom, with dimensions determined by the number of projectors for each atom. + The atoms and orbitals are defined by the manifold tuple. + +Overview of inputs: +- `manifold`: OrbitalManifold with the atomic orbital type to define the Hubbard manifold. +- `occupation`: Occupation matrix for the bands. +- `positions`: Positions of the atoms in the unit cell. Default is model.positions. + +Overviw of outputs: +- `n_IJ`: 3-tensor of matrices. Outer indices select spin, iatom and jatom, + inner indices select m1 and m2 in the manifold. +- `manifold_labels`: Labels for all manifold orbitals, corresponding to different columns of p_I. +- `p_I`: Projectors for the manifold. Those are orthonormalized against all orbitals, + also against those outside of the manifold. +""" +function compute_hubbard_nIJ(manifold::OrbitalManifold, + basis::PlaneWaveBasis{T}, + ψ, occupation; + projectors = nothing, labels = nothing, + positions = basis.model.positions) where {T} + for (iatom, atom) in enumerate(basis.model.atoms) + @assert !iszero(size(atom.psp.r2_pswfcs[1], 1)) + "FATAL ERROR: No Atomic projector found within the provided PseudoPotential." + end + + filled_occ = filled_occupation(basis.model) + nprojs = length(labels) + nspins = basis.model.n_spin_components + n_matrix = zeros(Complex{T}, nspins, nprojs, nprojs) + + for σ in 1:nspins, ik = krange_spin(basis, σ) + # We divide by filled_occ to deal with the physical two spin channels separately. + ψk, projk, nk = @views ψ[ik], projectors[ik], occupation[ik]/filled_occ + c = projk' * ψk # <ϕ|ψ> + # The matrix product is done over the bands. + # In QE, basis.kweights[ik]*nk[ibnd] would be wg(ik,ibnd) + n_matrix[σ, :, :] .+= basis.kweights[ik] * c * diagm(nk) * c' + end + n_matrix = mpi_sum(n_matrix, basis.comm_kpts) + + # Now I want to reshape it to match the notation used in the papers. + # Reshape into n[I, J, σ][m1, m2] where I, J indicate the atom in the Hubbard manifold, σ is the spin, + # m1 and m2 are magnetic quantum numbers (n, l are fixed) + natoms = max([labels[i].iatom for i in 1:length(labels)]...) + n_IJ = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) + p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for ik in 1:length(basis.kpoints)] + # Very low-level, but works + for σ in 1:nspins + i = 1 + while i <= nprojs + il = labels[i].l + iatom = labels[i].iatom + j = 1 + while j <= nprojs + jl = labels[j].l + jatom = labels[j].iatom + n_IJ[σ, iatom, jatom] = n_matrix[σ, i:i+2*il, j:j+2*jl] + j += 2*jl + 1 + end + for (ik, projk) in enumerate(projectors) + p_I[ik][iatom] = projk[:, i:i+2*il] + end + i += 2*il + 1 + end + end + + n_IJ = symmetrize_nhub(n_IJ, basis.model.lattice, basis.symmetries, basis.model.positions) + + return (; n_IJ=n_IJ, manifold_labels=labels, p_I=p_I) +end + +function reshape_hubbard_proj(projectors::Vector{Matrix{Complex{T}}}, labels) where {T} + natoms = max([labels[i].iatom for i in 1:length(labels)]...) + p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for i in 1:length(projectors)] + i = 1 + while i <= size(projectors, 2) + il = labels[i].l + iatom = labels[i].iatom + for (ik, projk) in enumerate(projectors) + p_I[ik][iatom] = Matrix{Complex{T}}(undef, size(projk,1), 2*il + 1) + p_I[ik][iatom] = copy(projk[:, i:i+2*il]) + end + i += 2*il + 1 + end + + return p_I +end + +struct Hubbard + manifold :: OrbitalManifold + U :: Float64 +end +function (hubbard::Hubbard)(basis::AbstractBasis) + isempty(hubbard.U) && return TermNoop() + projs, labs = atomic_orbital_projectors(basis) + labels, projectors = extract_manifold(basis, projs, labs, hubbard.manifold) + TermHubbard(hubbard.manifold, hubbard.U, projectors, labels) +end + +struct TermHubbard{PT, L} <: Term + manifold :: OrbitalManifold + U :: Float64 + P :: PT + labels :: L +end + +@timing "ene_ops: hubbard" function ene_ops(term::TermHubbard, + basis::PlaneWaveBasis{T}, + ψ, occupation; n_hub=nothing, ψ_hub=nothing, + labels=term.labels, + kwargs...) where {T} + if isnothing(ψ) + if isnothing(n_hub) + return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) + end + n = n_hub + ψ = ψ_hub + proj = reshape_hubbard_proj(term.P, term.labels) + else + Hubbard = compute_hubbard_nIJ(term.manifold, basis, ψ, occupation; projectors=term.P, + labels) + n = Hubbard.n_IJ + n_hub = n + proj = Hubbard.p_I + end + to_unit = ustrip(auconvert(u"eV", 1.0)) + U = term.U / to_unit # U is usally expressed in eV in the literature, but we work in au + + ops = [HubbardUOperator(basis, kpt, U, n, proj[ik]) for (ik,kpt) in enumerate(basis.kpoints)] + + filled_occ = filled_occupation(basis.model) + types = findall(at -> at.species == Symbol(term.manifold.species), basis.model.atoms) + natoms = length(types) + nspins = basis.model.n_spin_components + + E = zero(T) + for σ in 1:nspins, iatom in 1:natoms + E += filled_occ * 0.5 * U * real(tr(n[σ, iatom,iatom] * (I - n[σ, iatom,iatom]))) + end + return (; E, ops, n) +end diff --git a/src/terms/operators.jl b/src/terms/operators.jl index 40c8a523e6..52c336e474 100644 --- a/src/terms/operators.jl +++ b/src/terms/operators.jl @@ -155,6 +155,38 @@ function apply!(Hψ, op::DivAgradOperator, ψ; end # TODO Implement Matrix(op::DivAgrad) +@doc raw""" +"Hubbard U" operator ``Hψ = Σᵢ Σₘ₁ₘ₂ U * (1 - 2n[i,i][m1,m2]) * Pᵢₘ₁ * Pᵢₘ₂' * ψ`` +where ``Pᵢₘ₁`` is the projector for atom i and orbital m₁. +(m₁ is usually just the magnetic quantum number, since l is usually fixed) +""" +struct HubbardUOperator{T <: Real} <: RealFourierOperator + basis :: PlaneWaveBasis{T} + kpoint :: Kpoint{T} + Us :: Real # Hubbard U parameter + n_IJs :: Array{Matrix{Complex{T}}} + proj_Is :: Vector{Matrix{Complex{T}}} # It is the projector for the given kpoint only +end +function apply!(Hψ, op::HubbardUOperator, ψ) + σ = op.kpoint.spin + n_IJ = op.n_IJs + proj_I = op.proj_Is + natoms = size(op.n_IJs, 2) + for iatom in 1:natoms + n_ii = n_IJ[σ, iatom, iatom] + iszero(n_ii) && continue + for m1 = 1:size(n_ii, 1) + P_i_m1 = proj_I[iatom][:,m1] + for m2 = 1:size(n_ii, 2) + P_i_m2 = proj_I[iatom][:,m2] + δm = (m1 == m2) ? one(eltype(n_ii)) : zero(eltype(n_ii)) + coefficient = 0.5 * op.Us * (δm - 2*n_ii[m1, m2]) + projection = P_i_m2' * ψ.fourier + Hψ.fourier .+= coefficient * projection * P_i_m1 + end + end + end +end # Optimize RFOs by combining terms that can be combined function optimize_operators(ops) diff --git a/src/terms/terms.jl b/src/terms/terms.jl index 11b9b77970..292ad59454 100644 --- a/src/terms/terms.jl +++ b/src/terms/terms.jl @@ -64,6 +64,7 @@ include("ewald.jl") include("psp_correction.jl") include("entropy.jl") include("pairwise.jl") +include("hubbard.jl") include("magnetic.jl") breaks_symmetries(::Magnetic) = true diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index d633ae4b4f..aa38059569 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -5,6 +5,7 @@ using Logging using DFTK: mpi_sum using LinearAlgebra using ..TestCases: silicon +using PseudoPotentialData testcase = silicon function test_matrix_repr_operator(hamk, ψk; atol=1e-8) @@ -14,7 +15,8 @@ function test_matrix_repr_operator(hamk, ψk; atol=1e-8) @test norm(operator_matrix*ψk - operator*ψk) < atol catch e allowed_missing_operators = Union{DFTK.DivAgradOperator, - DFTK.MagneticFieldOperator} + DFTK.MagneticFieldOperator, + DFTK.HubbardUOperator} @assert operator isa allowed_missing_operators @info "Matrix of operator $(nameof(typeof(operator))) not yet supported" maxlog=1 end @@ -28,7 +30,7 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, xc = term isa Xc ? "($(first(term.functionals)))" : "" @testset "$(typeof(term))$xc $sspol" begin n_dim = 3 - count(iszero, eachcol(lattice)) - Si = n_dim == 3 ? ElementPsp(14, load_psp(testcase.psp_gth)) : ElementCoulomb(:Si) + Si = n_dim == 3 ? ElementPsp(14, PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf")) : ElementCoulomb(:Si) atoms = [Si, Si] model = Model(lattice, atoms, testcase.positions; terms=[term], spin_polarization, symmetries=true) @@ -92,6 +94,7 @@ end test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) test_consistency_term(Hartree()) + test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3S"), 1.0)) test_consistency_term(Ewald()) test_consistency_term(PspCorrection()) test_consistency_term(Xc([:lda_xc_teter93])) diff --git a/test/testcases.jl b/test/testcases.jl index c178c290ac..b8c558b3f8 100644 --- a/test/testcases.jl +++ b/test/testcases.jl @@ -10,6 +10,7 @@ gth_lda_semi = PseudoFamily("cp2k.nc.sr.lda.v0_1.semicore.gth") pd_lda_family = PseudoFamily("dojo.nc.sr.lda.v0_4_1.standard.upf") silicon = (; + symbol = :Si, lattice = [0.0 5.131570667152971 5.131570667152971; 5.131570667152971 0.0 5.131570667152971; 5.131570667152971 5.131570667152971 0.0], @@ -31,6 +32,7 @@ silicon = merge(silicon, (; atoms=fill(ElementPsp(silicon.atnum, load_psp(silicon.psp_gth)), 2))) magnesium = (; + symbol = :Mg, lattice = [-3.0179389205999998 -3.0179389205999998 0.0000000000000000; -5.2272235447000002 5.2272235447000002 0.0000000000000000; 0.0000000000000000 0.0000000000000000 -9.7736219469000005], @@ -55,6 +57,7 @@ magnesium = merge(magnesium, aluminium = (; + symbol = :Al, lattice = Matrix(Diagonal([4 * 7.6324708938577865, 7.6324708938577865, 7.6324708938577865])), atnum = 13, @@ -71,6 +74,7 @@ aluminium = merge(aluminium, aluminium_primitive = (; + symbol = :Al, lattice = [5.39697192863632 2.69848596431816 2.69848596431816; 0.00000000000000 4.67391479368660 1.55797159787754; 0.00000000000000 0.00000000000000 4.40660912710674], @@ -89,6 +93,7 @@ aluminium_primitive = merge(aluminium_primitive, platinum_hcp = (; + symbol = :Pt, lattice = [10.00000000000000 0.00000000000000 0.00000000000000; 5.00000000000000 8.66025403784439 0.00000000000000; 0.00000000000000 0.00000000000000 16.3300000000000], @@ -106,6 +111,7 @@ platinum_hcp = merge(platinum_hcp, load_psp(platinum_hcp.psp_gth)), 2))) iron_bcc = (; + symbol = :Fe, lattice = 2.71176 .* [[-1 1 1]; [1 -1 1]; [1 1 -1]], atnum = 26, mass = 55.8452u"u", @@ -133,6 +139,33 @@ o2molecule = merge(o2molecule, (; atoms=fill(ElementPsp(o2molecule.atnum, load_psp(o2molecule.psp_gth)), 2))) +nickel = (; + symbol = :Ni, + atnum = 28, + mass = 58.6934u"u", + n_electrons = 10, + psp_gth = gth_lda_semi[:Ni], + psp_upf = pd_lda_family[:Ni], + temperature = 0.02, + is_metal = true, +) + +oxygen = (; + symbol = :O, + atnum = 8, + mass = 15.999u"u", + n_electrons = 6, + psp_gth = gth_lda_semi[:O], + psp_upf = pd_lda_family[:O], + temperature = 0.02, + is_metal = true, +) +nio2 = merge(nio2, + (; atoms=vcat(fill(ElementPsp(nickel.atnum, + load_psp(nickel.psp_gth)), 2), + fill(ElementPsp(oxygen.atnum, + load_psp(oxygen.psp_gth)), 2)))) + all_testcases = (; silicon, magnesium, aluminium, aluminium_primitive, platinum_hcp, iron_bcc, o2molecule) end From d4dfa7b3aebe94607080c73d883e514644b52f4b Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 25 Sep 2025 14:59:03 +0200 Subject: [PATCH 02/50] Complex Spherical Harmonics have been removed --- src/common/spherical_harmonics.jl | 47 ------------------------------- 1 file changed, 47 deletions(-) diff --git a/src/common/spherical_harmonics.jl b/src/common/spherical_harmonics.jl index 3842c3dbbf..ec46b871eb 100644 --- a/src/common/spherical_harmonics.jl +++ b/src/common/spherical_harmonics.jl @@ -47,50 +47,3 @@ function ylm_real(l::Integer, m::Integer, rvec::AbstractVector{T}) where {T} error("The case l = $l and m = $m is not implemented") end - -""" -Returns the ``(l,m)`` real spherical harmonic ``Y_l^m(r)``. Consistent with -[Wikipedia](https://en.wikipedia.org/wiki/Table_of_spherical_harmonics#Real_spherical_harmonics). -""" -function ylm_complex(l::Integer, m::Integer, rvec::AbstractVector{T}) where {T} - @assert 0 ≤ l - @assert -l ≤ m ≤ l - @assert length(rvec) == 3 - x, y, z = rvec - r = norm(rvec) - - if l == 0 # s - (m == 0) && return sqrt(1 / 4T(π)) - end - - # Catch cases of numerically very small r - if r <= 10 * eps(eltype(rvec)) - return zero(T) - end - - if l == 1 # p - (m == 0) && return sqrt(3 / 4T(π)) * z / r - (m == -1) && return sqrt(3 / 8T(π)) * (x - im*y) / r - (m == 1) && return - sqrt(3 / 8T(π)) * (x + im*y) / r - end - - if l == 2 # d - (m == -2) && return sqrt(15 / 32T(π)) * (x - im*y)^2 / r^2 - (m == -1) && return sqrt(15 / 8T(π)) * (x - im*y) * z / r^2 - (m == 0) && return sqrt( 5 / 16T(π)) * (2z^2 - x^2 - y^2) / r^2 - (m == 1) && return - sqrt(15 / 8T(π)) * (x + im*y) * z / r^2 - (m == 2) && return sqrt(15 / 32T(π)) * (x + im*y)^2 / r^2 - end - - if l == 3 # f - (m == -3) && return sqrt( 35 / 64T(π)) * (x - im*y)^3 / r^3 - (m == -2) && return sqrt(105 / 32T(π)) * (x - im*y)^2 * z / r^3 - (m == -1) && return sqrt( 21 / 64T(π)) * (x - im*y) * (4z^2 - x^2 - y^2) / r^3 - (m == 0) && return sqrt( 7 / 16T(π)) * z * (2z^2 - 3x^2 - 3y^2) / r^3 - (m == 1) && return - sqrt( 21 / 64T(π)) * (x + im*y) * (4z^2 - x^2 - y^2) / r^3 - (m == 2) && return sqrt(105 / 32T(π)) * (x + im*y)^2 * z / r^3 - (m == 3) && return - sqrt( 35 / 64T(π)) * (x + im*y)^3 / r^3 - end - - error("The case l = $l and m = $m is not implemented") -end From 48a33f03d01d9feb43dc5da46bf5f4d9c3330983 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 25 Sep 2025 15:23:21 +0200 Subject: [PATCH 03/50] Correction to testcases.jl --- test/hamiltonian_consistency.jl | 2 +- test/testcases.jl | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index aa38059569..aafaf2676d 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -30,7 +30,7 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, xc = term isa Xc ? "($(first(term.functionals)))" : "" @testset "$(typeof(term))$xc $sspol" begin n_dim = 3 - count(iszero, eachcol(lattice)) - Si = n_dim == 3 ? ElementPsp(14, PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf")) : ElementCoulomb(:Si) + Si = n_dim == 3 ? ElementPsp(14, load_psp(testcase.psp_upf)) : ElementCoulomb(:Si) atoms = [Si, Si] model = Model(lattice, atoms, testcase.positions; terms=[term], spin_polarization, symmetries=true) diff --git a/test/testcases.jl b/test/testcases.jl index b8c558b3f8..840bb0f6e1 100644 --- a/test/testcases.jl +++ b/test/testcases.jl @@ -159,12 +159,7 @@ oxygen = (; psp_upf = pd_lda_family[:O], temperature = 0.02, is_metal = true, -) -nio2 = merge(nio2, - (; atoms=vcat(fill(ElementPsp(nickel.atnum, - load_psp(nickel.psp_gth)), 2), - fill(ElementPsp(oxygen.atnum, - load_psp(oxygen.psp_gth)), 2)))) +) all_testcases = (; silicon, magnesium, aluminium, aluminium_primitive, platinum_hcp, iron_bcc, o2molecule) From 6c77122b50714fc36de539c8117bd5121fbdc5cb Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 25 Sep 2025 16:05:34 +0200 Subject: [PATCH 04/50] Added test for hubbard term --- src/terms/hubbard.jl | 46 +++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 63522b29e7..33a882e513 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -86,8 +86,8 @@ function symmetrize_nhub(n_IJ::Array{Matrix{Complex{T}}}, lattice, symmetry, pos ns = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) for σ in 1:nspins, iatom in 1:natoms, jatom in 1:natoms ns[σ, iatom, jatom] = zeros(Complex{T}, - size(n_IJ[σ, iatom, jatom],1), - size(n_IJ[σ, iatom, jatom],2)) + size(n_IJ[σ, iatom, jatom], 1), + size(n_IJ[σ, iatom, jatom], 2)) end for σ in 1:nspins, iatom in 1:natoms, isym in 1:nsym @@ -181,46 +181,48 @@ function compute_hubbard_nIJ(manifold::OrbitalManifold, projectors = nothing, labels = nothing, positions = basis.model.positions) where {T} for (iatom, atom) in enumerate(basis.model.atoms) - @assert !iszero(size(atom.psp.r2_pswfcs[1], 1)) - "FATAL ERROR: No Atomic projector found within the provided PseudoPotential." + @assert !iszero(size(atom.psp.r2_pswfcs[1], 1)) "FATAL ERROR: No Atomic projector found within the provided PseudoPotential." end filled_occ = filled_occupation(basis.model) nprojs = length(labels) nspins = basis.model.n_spin_components - n_matrix = zeros(Complex{T}, nspins, nprojs, nprojs) + n_matrix = zeros(Complex{T}, nspins, nprojs, nprojs) - for σ in 1:nspins, ik = krange_spin(basis, σ) + for σ in 1:nspins, ik = krange_spin(basis, σ) # We divide by filled_occ to deal with the physical two spin channels separately. - ψk, projk, nk = @views ψ[ik], projectors[ik], occupation[ik]/filled_occ + ψk, projk, nk = @views ψ[ik], projectors[ik], occupation[ik]/filled_occ c = projk' * ψk # <ϕ|ψ> - # The matrix product is done over the bands. - # In QE, basis.kweights[ik]*nk[ibnd] would be wg(ik,ibnd) - n_matrix[σ, :, :] .+= basis.kweights[ik] * c * diagm(nk) * c' + # The matrix product is done over the bands. In QE, basis.kweights[ik]*nk[ibnd] would be wg(ik,ibnd) + n_matrix[σ, :, :] .+= basis.kweights[ik] * c * diagm(nk) * c' end n_matrix = mpi_sum(n_matrix, basis.comm_kpts) # Now I want to reshape it to match the notation used in the papers. - # Reshape into n[I, J, σ][m1, m2] where I, J indicate the atom in the Hubbard manifold, σ is the spin, - # m1 and m2 are magnetic quantum numbers (n, l are fixed) - natoms = max([labels[i].iatom for i in 1:length(labels)]...) + # Reshape into n[I, J, σ][m1, m2] where I, J indicate the atom in the Hubbard manifold, σ is the spin, m1 and m2 are magnetic quantum numbers (n, l are fixed) + manifold_atoms = findall(at -> at.species == Symbol(manifold.species), basis.model.atoms) + natoms = length(manifold_atoms) # Number of atoms of the species in the manifold n_IJ = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for ik in 1:length(basis.kpoints)] # Very low-level, but works - for σ in 1:nspins + for σ in 1:nspins, iatom in eachindex(manifold_atoms) i = 1 while i <= nprojs il = labels[i].l - iatom = labels[i].iatom - j = 1 - while j <= nprojs - jl = labels[j].l - jatom = labels[j].iatom - n_IJ[σ, iatom, jatom] = n_matrix[σ, i:i+2*il, j:j+2*jl] - j += 2*jl + 1 + if !(manifold_atoms[iatom] == labels[i].iatom) + i += 2*il + 1 + continue + end + for jatom in eachindex(manifold_atoms) + j = 1 + while j <= nprojs + jl = labels[j].l + (manifold_atoms[jatom] == labels[j].iatom) && (n_IJ[σ, iatom, jatom] = n_matrix[σ, i:i+2*il, j:j+2*jl]) + j += 2*jl + 1 + end end for (ik, projk) in enumerate(projectors) - p_I[ik][iatom] = projk[:, i:i+2*il] + p_I[ik][iatom] = projk[:, i:i+2*il] end i += 2*il + 1 end From 5540b225871ed5eb50e5d86a70ad7a37f62a74dd Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 25 Sep 2025 16:20:58 +0200 Subject: [PATCH 05/50] Added explicit import of Hubbard in testcases.jl --- test/hamiltonian_consistency.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index aafaf2676d..f85fad0a4e 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -3,6 +3,7 @@ using Test using DFTK using Logging using DFTK: mpi_sum +using DFTK: Hubbard using LinearAlgebra using ..TestCases: silicon using PseudoPotentialData @@ -84,6 +85,7 @@ end @testitem "Hamiltonian consistency" setup=[TestCases, HamConsistency] tags=[:dont_test_mpi] begin using DFTK + using DFTK: Hubbard using LinearAlgebra using .HamConsistency: test_consistency_term From 9e894bf61cfbe0a4b8a4d41f3535da78c1328450 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 25 Sep 2025 16:43:31 +0200 Subject: [PATCH 06/50] Speed up of the hubbard test --- src/DFTK.jl | 1 + test/hamiltonian_consistency.jl | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DFTK.jl b/src/DFTK.jl index 69aa2c5c14..22670744e0 100644 --- a/src/DFTK.jl +++ b/src/DFTK.jl @@ -111,6 +111,7 @@ export AtomicNonlocal export Ewald export PspCorrection export Entropy +export Hubbard export Magnetic export PairwisePotential export Anyonic diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index f85fad0a4e..aafaf2676d 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -3,7 +3,6 @@ using Test using DFTK using Logging using DFTK: mpi_sum -using DFTK: Hubbard using LinearAlgebra using ..TestCases: silicon using PseudoPotentialData @@ -85,7 +84,6 @@ end @testitem "Hamiltonian consistency" setup=[TestCases, HamConsistency] tags=[:dont_test_mpi] begin using DFTK - using DFTK: Hubbard using LinearAlgebra using .HamConsistency: test_consistency_term From 4a1d0e29d8ee7f693bad5a09c59af3a5a65d69fe Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 25 Sep 2025 17:11:39 +0200 Subject: [PATCH 07/50] Hubbard test now functional --- test/hubbard.jl | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/hubbard.jl diff --git a/test/hubbard.jl b/test/hubbard.jl new file mode 100644 index 0000000000..b81bb82486 --- /dev/null +++ b/test/hubbard.jl @@ -0,0 +1,45 @@ +@testitem "Nickel Oxide" setup=[TestCases] begin + using DFTK + using PseudoPotentialData + using Unitful + using UnitfulAtomic + + # Hubbard parameters + U = 10 + manifold = DFTK.OrbitalManifold(;species=:Ni, label="3D") + + a = 7.9 # Bohr + lattice = a * [[ 1.0 0.5 0.5]; + [ 0.5 1.0 0.5]; + [ 0.5 0.5 1.0]] + pseudopotentials = PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf") + Ni = ElementPsp(:Ni, pseudopotentials) + O = ElementPsp(:O, pseudopotentials) + atoms = [Ni, O, Ni, O] + positions = [[0.0, 0.0, 0.0], + [0.25, 0.25, 0.25], + [0.5, 0.5, 0.5], + [0.75, 0.75, 0.75]] + magnetic_moments = [2, 0, -1, 0] + + ψ=nothing + occupation=nothing + model = model_DFT(lattice, atoms, positions; + extra_terms=[DFTK.Hubbard(manifold, U)], + temperature=0.01, functionals=PBE(), + smearing=DFTK.Smearing.Gaussian(), magnetic_moments=magnetic_moments) + basis = PlaneWaveBasis(model; Ecut = 15, kgrid = [2, 2, 2]) + ρ0 = guess_density(basis, magnetic_moments) + scfres = self_consistent_field(basis; tol=1e-10, ρ=ρ0) + + ref = -354.907446880021 + e_total = scfres.energies.total + @test abs(e_total - ref) < 1e-8 + for (term, value) in scfres.energies + if term == "Hubbard" + ref = 0.17629078433258719 + e_hub = value + @test abs(e_hub - ref) < 1e-8 + end + end +end \ No newline at end of file From db7d948af743afd93c94428cdce8205db44c602e Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Fri, 26 Sep 2025 10:50:24 +0200 Subject: [PATCH 08/50] Still errors in the consistency test --- test/hamiltonian_consistency.jl | 66 +++++++++++++++++---------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index aafaf2676d..4d0ea60a56 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -24,7 +24,7 @@ function test_matrix_repr_operator(hamk, ψk; atol=1e-8) end function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, 3], - kshift=[0, 1, 0]/2, lattice=testcase.lattice, Ecut=10, + kshift=[0, 1, 0]/2, lattice=testcase.lattice, Ecut=15, spin_polarization=:none) sspol = spin_polarization != :none ? " ($spin_polarization)" : "" xc = term isa Xc ? "($(first(term.functionals)))" : "" @@ -32,24 +32,27 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, n_dim = 3 - count(iszero, eachcol(lattice)) Si = n_dim == 3 ? ElementPsp(14, load_psp(testcase.psp_upf)) : ElementCoulomb(:Si) atoms = [Si, Si] - model = Model(lattice, atoms, testcase.positions; terms=[term], spin_polarization, + model = Model(lattice, atoms, testcase.positions; terms=[term], #spin_polarization=:collinear, symmetries=true) basis = PlaneWaveBasis(model; Ecut, kgrid=MonkhorstPack(kgrid; kshift)) n_electrons = testcase.n_electrons n_bands = div(n_electrons, 2, RoundUp) filled_occ = DFTK.filled_occupation(model) + @show filled_occ ψ = [Matrix(qr(randn(ComplexF64, length(G_vectors(basis, kpt)), n_bands)).Q) for kpt in basis.kpoints] occupation = [filled_occ * rand(n_bands) for _ = 1:length(basis.kpoints)] occ_scaling = n_electrons / sum(sum(occupation)) occupation = [occ * occ_scaling for occ in occupation] + @show occupation ρ = with_logger(NullLogger()) do compute_density(basis, ψ, occupation) end τ = compute_kinetic_energy_density(basis, ψ, occupation) E0, ham = energy_hamiltonian(basis, ψ, occupation; ρ, τ) + @show E0 @assert length(basis.terms) == 1 @@ -76,6 +79,7 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, diff_predicted = mpi_sum(diff_predicted, basis.comm_kpts) err = abs(diff - diff_predicted) + @show diff diff_predicted err rtol*abs(E0.total) atol @test err < rtol * abs(E0.total) || err < atol end end @@ -87,33 +91,33 @@ end using LinearAlgebra using .HamConsistency: test_consistency_term - test_consistency_term(Kinetic()) - test_consistency_term(AtomicLocal()) - test_consistency_term(AtomicNonlocal()) - test_consistency_term(ExternalFromReal(X -> cos(X[1]))) - test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) - test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) - test_consistency_term(Hartree()) - test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3S"), 1.0)) - test_consistency_term(Ewald()) - test_consistency_term(PspCorrection()) - test_consistency_term(Xc([:lda_xc_teter93])) - test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) - test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_tpss])) - test_consistency_term(Xc([:mgga_x_scan])) - test_consistency_term(Xc([:mgga_c_scan]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_b00])) - test_consistency_term(Xc([:mgga_c_b94]), spin_polarization=:collinear) - - let - a = 6 - pot(x, y, z) = (x - a/2)^2 + (y - a/2)^2 - Apot(x, y, z) = .2 * [y - a/2, -(x - a/2), 0] - Apot(X) = Apot(X...) - test_consistency_term(Magnetic(Apot); kgrid=[1, 1, 1], kshift=[0, 0, 0], - lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) - test_consistency_term(DFTK.Anyonic(2, 3.2); kgrid=[1, 1, 1], kshift=[0, 0, 0], - lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) - end +# test_consistency_term(Kinetic()) +# test_consistency_term(AtomicLocal()) +# test_consistency_term(AtomicNonlocal()) +# test_consistency_term(ExternalFromReal(X -> cos(X[1]))) +# test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) +# test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) +# test_consistency_term(Hartree()) + test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3S"), 10)) +# test_consistency_term(Ewald()) +# test_consistency_term(PspCorrection()) +# test_consistency_term(Xc([:lda_xc_teter93])) +# test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) +# test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) +# test_consistency_term(Xc([:mgga_x_tpss])) +# test_consistency_term(Xc([:mgga_x_scan])) +# test_consistency_term(Xc([:mgga_c_scan]), spin_polarization=:collinear) +# test_consistency_term(Xc([:mgga_x_b00])) +# test_consistency_term(Xc([:mgga_c_b94]), spin_polarization=:collinear) +# +# let +# a = 6 +# pot(x, y, z) = (x - a/2)^2 + (y - a/2)^2 +# Apot(x, y, z) = .2 * [y - a/2, -(x - a/2), 0] +# Apot(X) = Apot(X...) +# test_consistency_term(Magnetic(Apot); kgrid=[1, 1, 1], kshift=[0, 0, 0], +# lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) +# test_consistency_term(DFTK.Anyonic(2, 3.2); kgrid=[1, 1, 1], kshift=[0, 0, 0], +# lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) +# end end From e0a1002a45ec32da1729bc368c806590b1916e6d Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Mon, 29 Sep 2025 12:07:30 +0200 Subject: [PATCH 09/50] Some comments have been resolved --- src/eigen/diag.jl | 2 +- src/postprocess/band_structure.jl | 2 +- src/postprocess/dos.jl | 1 + src/terms/hubbard.jl | 53 ++++--------------------- src/terms/operators.jl | 4 +- test/hamiltonian_consistency.jl | 66 +++++++++++++++---------------- test/hubbard.jl | 2 +- 7 files changed, 45 insertions(+), 85 deletions(-) diff --git a/src/eigen/diag.jl b/src/eigen/diag.jl index 700efd8d5e..116621d55f 100644 --- a/src/eigen/diag.jl +++ b/src/eigen/diag.jl @@ -9,7 +9,7 @@ that really does the work, operating on a single ``k``-Block. function diagonalize_all_kblocks(eigensolver, ham::Hamiltonian, nev_per_kpoint::Int; ψguess=nothing, prec_type=PreconditionerTPA, interpolate_kpoints=true, - tol=1e-6, miniter=1, maxiter=100, n_conv_check=nothing, kwargs...) + tol=1e-6, miniter=1, maxiter=100, n_conv_check=nothing) kpoints = ham.basis.kpoints results = Vector{Any}(undef, length(kpoints)) diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index d28328cca4..d2b66f23b5 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -35,7 +35,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): # Create new basis with new kpoints bs_basis = PlaneWaveBasis(basis, kgrid) - ham = Hamiltonian(bs_basis; ρ, τ, n_hub, occupation, kwargs...) + ham = Hamiltonian(bs_basis; ρ, τ, n_hub, occupation) eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands + n_extra; n_conv_check=n_bands, tol, kwargs...) if !eigres.converged diff --git a/src/postprocess/dos.jl b/src/postprocess/dos.jl index 3f264d49e5..55ce2c6489 100644 --- a/src/postprocess/dos.jl +++ b/src/postprocess/dos.jl @@ -166,6 +166,7 @@ function atomic_orbital_projectors(basis::PlaneWaveBasis{T}; labels = [] for (iatom, atom) in enumerate(basis.model.atoms) psp = atom.psp + @assert !iszero(size(psp.r2_pswfcs[1], 1)) "FATAL ERROR: No Atomic projector found within the provided PseudoPotential." for l in 0:psp.lmax, n in 1:DFTK.count_n_pswfc_radial(psp, l) label = DFTK.pswfc_label(psp, n, l) if !isonmanifold((; iatom, atom.species, label)) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 33a882e513..aca2fe87c9 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -54,21 +54,6 @@ function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, return (; manifold_labels, manifold_projectors) end -function compute_overlap_matrix(basis::PlaneWaveBasis{T}; - manifold = nothing, - positions = basis.model.positions) where {T} - proj = atomic_orbital_projectors(basis; manifold, positions) - projectors = proj.projectors - labels = proj.labels - overlap_matrix = Vector{Matrix{T}}(undef, length(basis.kpoints)) - - for (ik, projk) in enumerate(projectors) - overlap_matrix[ik] = abs2.(projk' * projk) - end - - return (; overlap_matrix, projectors, labels) -end - """ Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ @@ -92,7 +77,7 @@ function symmetrize_nhub(n_IJ::Array{Matrix{Complex{T}}}, lattice, symmetry, pos for σ in 1:nspins, iatom in 1:natoms, isym in 1:nsym for m1 in 1:size(ns[σ, iatom, iatom], 1), m2 in 1:size(ns[σ, iatom, iatom], 2) - sym_atom = find_symmetric(iatom, symmetry, isym, positions) + sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry[isym]) # TODO: Here QE flips spin for time-reversal in collinear systems, should we? for m0 in 1:size(n_IJ[σ, iatom, iatom], 1), m00 in 1:size(n_IJ[σ, iatom, iatom], 2) ns[σ, iatom, iatom][m1, m2] += WigD[m0, m1, isym] * @@ -102,26 +87,6 @@ function symmetrize_nhub(n_IJ::Array{Matrix{Complex{T}}}, lattice, symmetry, pos end end ns .= ns / nsym - - return ns -end - -""" -Find the symmetric atom index for a given atom and symmetry operation -""" -function find_symmetric(iatom::Int64, symmetry::Vector{SymOp{T}}, - isym::Int64, positions) where {T} - sym_atom = iatom - W, w = symmetry[isym].W, symmetry[isym].w - p = positions[iatom] - p2 = W * p + w - for (jatom, pos) in enumerate(positions) - if isapprox(pos, p2, atol=1e-8) - sym_atom = jatom - break - end - end - return sym_atom end """ @@ -142,6 +107,7 @@ function Wigner_sym(l::Int64, L, symmetries::Vector{SymOp{T}}) where {T} A = Matrix{Float64}(undef, 2*l+1, 2*l+1) for n in 1:2*l+1 r = rand(Float64, 3) + r = r / norm(r) r0 = L * W * inv(L) * r b[n] = DFTK.ylm_real(l, m1, r0) for m2 in -l:l @@ -178,12 +144,8 @@ Overviw of outputs: function compute_hubbard_nIJ(manifold::OrbitalManifold, basis::PlaneWaveBasis{T}, ψ, occupation; - projectors = nothing, labels = nothing, + projectors, labels, positions = basis.model.positions) where {T} - for (iatom, atom) in enumerate(basis.model.atoms) - @assert !iszero(size(atom.psp.r2_pswfcs[1], 1)) "FATAL ERROR: No Atomic projector found within the provided PseudoPotential." - end - filled_occ = filled_occupation(basis.model) nprojs = length(labels) nspins = basis.model.n_spin_components @@ -252,18 +214,19 @@ end struct Hubbard manifold :: OrbitalManifold - U :: Float64 + U :: Real end function (hubbard::Hubbard)(basis::AbstractBasis) isempty(hubbard.U) && return TermNoop() projs, labs = atomic_orbital_projectors(basis) labels, projectors = extract_manifold(basis, projs, labs, hubbard.manifold) - TermHubbard(hubbard.manifold, hubbard.U, projectors, labels) + U = austrip(hubbard.U) + TermHubbard(hubbard.manifold, U, projectors, labels) end struct TermHubbard{PT, L} <: Term manifold :: OrbitalManifold - U :: Float64 + U :: Real P :: PT labels :: L end @@ -299,7 +262,7 @@ end E = zero(T) for σ in 1:nspins, iatom in 1:natoms - E += filled_occ * 0.5 * U * real(tr(n[σ, iatom,iatom] * (I - n[σ, iatom,iatom]))) + E += filled_occ * 1/2 * U * real(tr(n[σ, iatom,iatom] * (I - n[σ, iatom,iatom]))) end return (; E, ops, n) end diff --git a/src/terms/operators.jl b/src/terms/operators.jl index 52c336e474..315a7ff130 100644 --- a/src/terms/operators.jl +++ b/src/terms/operators.jl @@ -156,7 +156,7 @@ end # TODO Implement Matrix(op::DivAgrad) @doc raw""" -"Hubbard U" operator ``Hψ = Σᵢ Σₘ₁ₘ₂ U * (1 - 2n[i,i][m1,m2]) * Pᵢₘ₁ * Pᵢₘ₂' * ψ`` +"Hubbard U" operator ``Hψ = Σᵢ Σₘ₁ₘ₂ U/2 * (1 - 2n[i,i][m1,m2]) * Pᵢₘ₁ * Pᵢₘ₂' * ψ`` where ``Pᵢₘ₁`` is the projector for atom i and orbital m₁. (m₁ is usually just the magnetic quantum number, since l is usually fixed) """ @@ -180,7 +180,7 @@ function apply!(Hψ, op::HubbardUOperator, ψ) for m2 = 1:size(n_ii, 2) P_i_m2 = proj_I[iatom][:,m2] δm = (m1 == m2) ? one(eltype(n_ii)) : zero(eltype(n_ii)) - coefficient = 0.5 * op.Us * (δm - 2*n_ii[m1, m2]) + coefficient = 1/2 * op.Us * (δm - 2*n_ii[m1, m2]) projection = P_i_m2' * ψ.fourier Hψ.fourier .+= coefficient * projection * P_i_m1 end diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 4d0ea60a56..24558d991a 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -6,7 +6,7 @@ using DFTK: mpi_sum using LinearAlgebra using ..TestCases: silicon using PseudoPotentialData -testcase = silicon +testcase = silicon function test_matrix_repr_operator(hamk, ψk; atol=1e-8) for operator in hamk.operators @@ -30,7 +30,7 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, xc = term isa Xc ? "($(first(term.functionals)))" : "" @testset "$(typeof(term))$xc $sspol" begin n_dim = 3 - count(iszero, eachcol(lattice)) - Si = n_dim == 3 ? ElementPsp(14, load_psp(testcase.psp_upf)) : ElementCoulomb(:Si) + Si = n_dim == 3 ? ElementPsp(14, PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf")) : ElementCoulomb(:Si) atoms = [Si, Si] model = Model(lattice, atoms, testcase.positions; terms=[term], #spin_polarization=:collinear, symmetries=true) @@ -39,20 +39,17 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, n_electrons = testcase.n_electrons n_bands = div(n_electrons, 2, RoundUp) filled_occ = DFTK.filled_occupation(model) - @show filled_occ ψ = [Matrix(qr(randn(ComplexF64, length(G_vectors(basis, kpt)), n_bands)).Q) for kpt in basis.kpoints] occupation = [filled_occ * rand(n_bands) for _ = 1:length(basis.kpoints)] occ_scaling = n_electrons / sum(sum(occupation)) occupation = [occ * occ_scaling for occ in occupation] - @show occupation ρ = with_logger(NullLogger()) do compute_density(basis, ψ, occupation) end τ = compute_kinetic_energy_density(basis, ψ, occupation) E0, ham = energy_hamiltonian(basis, ψ, occupation; ρ, τ) - @show E0 @assert length(basis.terms) == 1 @@ -79,7 +76,6 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, diff_predicted = mpi_sum(diff_predicted, basis.comm_kpts) err = abs(diff - diff_predicted) - @show diff diff_predicted err rtol*abs(E0.total) atol @test err < rtol * abs(E0.total) || err < atol end end @@ -91,33 +87,33 @@ end using LinearAlgebra using .HamConsistency: test_consistency_term -# test_consistency_term(Kinetic()) -# test_consistency_term(AtomicLocal()) -# test_consistency_term(AtomicNonlocal()) -# test_consistency_term(ExternalFromReal(X -> cos(X[1]))) -# test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) -# test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) -# test_consistency_term(Hartree()) - test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3S"), 10)) -# test_consistency_term(Ewald()) -# test_consistency_term(PspCorrection()) -# test_consistency_term(Xc([:lda_xc_teter93])) -# test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) -# test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) -# test_consistency_term(Xc([:mgga_x_tpss])) -# test_consistency_term(Xc([:mgga_x_scan])) -# test_consistency_term(Xc([:mgga_c_scan]), spin_polarization=:collinear) -# test_consistency_term(Xc([:mgga_x_b00])) -# test_consistency_term(Xc([:mgga_c_b94]), spin_polarization=:collinear) -# -# let -# a = 6 -# pot(x, y, z) = (x - a/2)^2 + (y - a/2)^2 -# Apot(x, y, z) = .2 * [y - a/2, -(x - a/2), 0] -# Apot(X) = Apot(X...) -# test_consistency_term(Magnetic(Apot); kgrid=[1, 1, 1], kshift=[0, 0, 0], -# lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) -# test_consistency_term(DFTK.Anyonic(2, 3.2); kgrid=[1, 1, 1], kshift=[0, 0, 0], -# lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) -# end + test_consistency_term(Kinetic()) + test_consistency_term(AtomicLocal()) + test_consistency_term(AtomicNonlocal()) + test_consistency_term(ExternalFromReal(X -> cos(X[1]))) + test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) + test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) + test_consistency_term(Hartree()) + test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3P"), 10)) + test_consistency_term(Ewald()) + test_consistency_term(PspCorrection()) + test_consistency_term(Xc([:lda_xc_teter93])) + test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) + test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) + test_consistency_term(Xc([:mgga_x_tpss])) + test_consistency_term(Xc([:mgga_x_scan])) + test_consistency_term(Xc([:mgga_c_scan]), spin_polarization=:collinear) + test_consistency_term(Xc([:mgga_x_b00])) + test_consistency_term(Xc([:mgga_c_b94]), spin_polarization=:collinear) + + let + a = 6 + pot(x, y, z) = (x - a/2)^2 + (y - a/2)^2 + Apot(x, y, z) = .2 * [y - a/2, -(x - a/2), 0] + Apot(X) = Apot(X...) + test_consistency_term(Magnetic(Apot); kgrid=[1, 1, 1], kshift=[0, 0, 0], + lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) + test_consistency_term(DFTK.Anyonic(2, 3.2); kgrid=[1, 1, 1], kshift=[0, 0, 0], + lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) + end end diff --git a/test/hubbard.jl b/test/hubbard.jl index b81bb82486..735f84bf1a 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -1,4 +1,4 @@ -@testitem "Nickel Oxide" setup=[TestCases] begin +@testitem "Test Hubbard U term in Nickel Oxide" setup=[TestCases] begin using DFTK using PseudoPotentialData using Unitful From 7045a6d0dba44d288fa77a3458efcddd96a06667 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Mon, 29 Sep 2025 12:07:30 +0200 Subject: [PATCH 10/50] Some comments have been resolved --- ext/DFTKPlotsExt.jl | 2 +- src/eigen/diag.jl | 2 +- src/postprocess/band_structure.jl | 2 +- src/postprocess/dos.jl | 1 + src/scf/self_consistent_field.jl | 7 +-- src/terms/hubbard.jl | 94 ++++++++++--------------------- src/terms/operators.jl | 6 +- test/hamiltonian_consistency.jl | 64 ++++++++++----------- test/hubbard.jl | 9 ++- 9 files changed, 77 insertions(+), 110 deletions(-) diff --git a/ext/DFTKPlotsExt.jl b/ext/DFTKPlotsExt.jl index fad3ba9b5c..9ebd911f2b 100644 --- a/ext/DFTKPlotsExt.jl +++ b/ext/DFTKPlotsExt.jl @@ -162,7 +162,7 @@ function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom, label=nothi p = Plots.plot(p; kwargs...) spinlabels = spin_components(basis.model) pdos = DFTK.sum_pdos(compute_pdos(εs, basis, ψ, eigenvalues; - positions, temperature, smearing), + positions, temperature=0.01, smearing), [DFTK.OrbitalManifold(;iatom, label)]) for σ = 1:n_spin plot_label = n_spin > 1 ? "$(species) $(orb_name) $(spinlabels[σ]) spin" : "$(species) $(orb_name)" diff --git a/src/eigen/diag.jl b/src/eigen/diag.jl index 700efd8d5e..116621d55f 100644 --- a/src/eigen/diag.jl +++ b/src/eigen/diag.jl @@ -9,7 +9,7 @@ that really does the work, operating on a single ``k``-Block. function diagonalize_all_kblocks(eigensolver, ham::Hamiltonian, nev_per_kpoint::Int; ψguess=nothing, prec_type=PreconditionerTPA, interpolate_kpoints=true, - tol=1e-6, miniter=1, maxiter=100, n_conv_check=nothing, kwargs...) + tol=1e-6, miniter=1, maxiter=100, n_conv_check=nothing) kpoints = ham.basis.kpoints results = Vector{Any}(undef, length(kpoints)) diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index d28328cca4..d2b66f23b5 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -35,7 +35,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): # Create new basis with new kpoints bs_basis = PlaneWaveBasis(basis, kgrid) - ham = Hamiltonian(bs_basis; ρ, τ, n_hub, occupation, kwargs...) + ham = Hamiltonian(bs_basis; ρ, τ, n_hub, occupation) eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands + n_extra; n_conv_check=n_bands, tol, kwargs...) if !eigres.converged diff --git a/src/postprocess/dos.jl b/src/postprocess/dos.jl index 3f264d49e5..55ce2c6489 100644 --- a/src/postprocess/dos.jl +++ b/src/postprocess/dos.jl @@ -166,6 +166,7 @@ function atomic_orbital_projectors(basis::PlaneWaveBasis{T}; labels = [] for (iatom, atom) in enumerate(basis.model.atoms) psp = atom.psp + @assert !iszero(size(psp.r2_pswfcs[1], 1)) "FATAL ERROR: No Atomic projector found within the provided PseudoPotential." for l in 0:psp.lmax, n in 1:DFTK.count_n_pswfc_radial(psp, l) label = DFTK.pswfc_label(psp, n, l) if !isonmanifold((; iatom, atom.species, label)) diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index be1f647c32..1993175e29 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -180,11 +180,8 @@ Overview of parameters: τ = compute_kinetic_energy_density(basis, ψ, occupation) end for (iterm, term) in enumerate(basis.terms) - if typeof(term)==TermHubbard - n_hub = Vector{Array{Matrix{ComplexF64}}}(undef, 0) - for (iman, manifold) in enumerate(term.manifold) - push!(n_hub, compute_hubbard_nIJ(manifold, basis, ψ, occupation).n_IJ) - end + if typeof(term)==DFTK.TermHubbard{Vector{Matrix{ComplexF64}}, Vector{Any}} + n_hub = compute_hubbard_nIJ(term.manifold, basis, ψ, occupation; projectors=term.P, labels=term.labels).n_IJ end end diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 33a882e513..81325bc05c 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -54,21 +54,6 @@ function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, return (; manifold_labels, manifold_projectors) end -function compute_overlap_matrix(basis::PlaneWaveBasis{T}; - manifold = nothing, - positions = basis.model.positions) where {T} - proj = atomic_orbital_projectors(basis; manifold, positions) - projectors = proj.projectors - labels = proj.labels - overlap_matrix = Vector{Matrix{T}}(undef, length(basis.kpoints)) - - for (ik, projk) in enumerate(projectors) - overlap_matrix[ik] = abs2.(projk' * projk) - end - - return (; overlap_matrix, projectors, labels) -end - """ Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ @@ -92,7 +77,7 @@ function symmetrize_nhub(n_IJ::Array{Matrix{Complex{T}}}, lattice, symmetry, pos for σ in 1:nspins, iatom in 1:natoms, isym in 1:nsym for m1 in 1:size(ns[σ, iatom, iatom], 1), m2 in 1:size(ns[σ, iatom, iatom], 2) - sym_atom = find_symmetric(iatom, symmetry, isym, positions) + sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry[isym]) # TODO: Here QE flips spin for time-reversal in collinear systems, should we? for m0 in 1:size(n_IJ[σ, iatom, iatom], 1), m00 in 1:size(n_IJ[σ, iatom, iatom], 2) ns[σ, iatom, iatom][m1, m2] += WigD[m0, m1, isym] * @@ -102,26 +87,6 @@ function symmetrize_nhub(n_IJ::Array{Matrix{Complex{T}}}, lattice, symmetry, pos end end ns .= ns / nsym - - return ns -end - -""" -Find the symmetric atom index for a given atom and symmetry operation -""" -function find_symmetric(iatom::Int64, symmetry::Vector{SymOp{T}}, - isym::Int64, positions) where {T} - sym_atom = iatom - W, w = symmetry[isym].W, symmetry[isym].w - p = positions[iatom] - p2 = W * p + w - for (jatom, pos) in enumerate(positions) - if isapprox(pos, p2, atol=1e-8) - sym_atom = jatom - break - end - end - return sym_atom end """ @@ -142,6 +107,7 @@ function Wigner_sym(l::Int64, L, symmetries::Vector{SymOp{T}}) where {T} A = Matrix{Float64}(undef, 2*l+1, 2*l+1) for n in 1:2*l+1 r = rand(Float64, 3) + r = r / norm(r) r0 = L * W * inv(L) * r b[n] = DFTK.ylm_real(l, m1, r0) for m2 in -l:l @@ -178,12 +144,8 @@ Overviw of outputs: function compute_hubbard_nIJ(manifold::OrbitalManifold, basis::PlaneWaveBasis{T}, ψ, occupation; - projectors = nothing, labels = nothing, + projectors, labels, positions = basis.model.positions) where {T} - for (iatom, atom) in enumerate(basis.model.atoms) - @assert !iszero(size(atom.psp.r2_pswfcs[1], 1)) "FATAL ERROR: No Atomic projector found within the provided PseudoPotential." - end - filled_occ = filled_occupation(basis.model) nprojs = length(labels) nspins = basis.model.n_spin_components @@ -228,23 +190,29 @@ function compute_hubbard_nIJ(manifold::OrbitalManifold, end end - n_IJ = symmetrize_nhub(n_IJ, basis.model.lattice, basis.symmetries, basis.model.positions) + n_IJ = symmetrize_nhub(n_IJ, basis.model.lattice, basis.symmetries, basis.model.positions[manifold_atoms]) return (; n_IJ=n_IJ, manifold_labels=labels, p_I=p_I) end -function reshape_hubbard_proj(projectors::Vector{Matrix{Complex{T}}}, labels) where {T} - natoms = max([labels[i].iatom for i in 1:length(labels)]...) +function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} + manifold_atoms = findall(at -> at.species == Symbol(manifold.species), basis.model.atoms) + natoms = length(manifold_atoms) + nprojs = length(labels) p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for i in 1:length(projectors)] - i = 1 - while i <= size(projectors, 2) - il = labels[i].l - iatom = labels[i].iatom - for (ik, projk) in enumerate(projectors) - p_I[ik][iatom] = Matrix{Complex{T}}(undef, size(projk,1), 2*il + 1) - p_I[ik][iatom] = copy(projk[:, i:i+2*il]) + for iatom in eachindex(manifold_atoms) + i = 1 + while i <= nprojs + il = labels[i].l + if !(manifold_atoms[iatom] == labels[i].iatom) + i += 2*il + 1 + continue + end + for (ik, projk) in enumerate(projectors) + p_I[ik][iatom] = projk[:, i:i+2*il] + end + i += 2*il + 1 end - i += 2*il + 1 end return p_I @@ -252,18 +220,19 @@ end struct Hubbard manifold :: OrbitalManifold - U :: Float64 + U end function (hubbard::Hubbard)(basis::AbstractBasis) isempty(hubbard.U) && return TermNoop() projs, labs = atomic_orbital_projectors(basis) labels, projectors = extract_manifold(basis, projs, labs, hubbard.manifold) - TermHubbard(hubbard.manifold, hubbard.U, projectors, labels) + U = austrip(hubbard.U) + TermHubbard(hubbard.manifold, U, projectors, labels) end struct TermHubbard{PT, L} <: Term manifold :: OrbitalManifold - U :: Float64 + U P :: PT labels :: L end @@ -277,20 +246,17 @@ end if isnothing(n_hub) return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end - n = n_hub ψ = ψ_hub - proj = reshape_hubbard_proj(term.P, term.labels) + proj = reshape_hubbard_proj(basis, term.P, term.labels, term.manifold) else Hubbard = compute_hubbard_nIJ(term.manifold, basis, ψ, occupation; projectors=term.P, labels) - n = Hubbard.n_IJ - n_hub = n + n_hub = Hubbard.n_IJ proj = Hubbard.p_I end - to_unit = ustrip(auconvert(u"eV", 1.0)) - U = term.U / to_unit # U is usally expressed in eV in the literature, but we work in au - ops = [HubbardUOperator(basis, kpt, U, n, proj[ik]) for (ik,kpt) in enumerate(basis.kpoints)] + #@show proj[1][1] + ops = [HubbardUOperator(basis, kpt, term.U, n_hub, proj[ik]) for (ik,kpt) in enumerate(basis.kpoints)] filled_occ = filled_occupation(basis.model) types = findall(at -> at.species == Symbol(term.manifold.species), basis.model.atoms) @@ -299,7 +265,7 @@ end E = zero(T) for σ in 1:nspins, iatom in 1:natoms - E += filled_occ * 0.5 * U * real(tr(n[σ, iatom,iatom] * (I - n[σ, iatom,iatom]))) + E += filled_occ * 1/2 * term.U * real(tr(n_hub[σ, iatom,iatom] * (I - n_hub[σ, iatom,iatom]))) end - return (; E, ops, n) + return (; E, ops, n_hub) end diff --git a/src/terms/operators.jl b/src/terms/operators.jl index 52c336e474..456bc69425 100644 --- a/src/terms/operators.jl +++ b/src/terms/operators.jl @@ -156,14 +156,14 @@ end # TODO Implement Matrix(op::DivAgrad) @doc raw""" -"Hubbard U" operator ``Hψ = Σᵢ Σₘ₁ₘ₂ U * (1 - 2n[i,i][m1,m2]) * Pᵢₘ₁ * Pᵢₘ₂' * ψ`` +"Hubbard U" operator ``Hψ = Σᵢ Σₘ₁ₘ₂ U/2 * (1 - 2n[i,i][m1,m2]) * Pᵢₘ₁ * Pᵢₘ₂' * ψ`` where ``Pᵢₘ₁`` is the projector for atom i and orbital m₁. (m₁ is usually just the magnetic quantum number, since l is usually fixed) """ struct HubbardUOperator{T <: Real} <: RealFourierOperator basis :: PlaneWaveBasis{T} kpoint :: Kpoint{T} - Us :: Real # Hubbard U parameter + Us # Hubbard U parameter n_IJs :: Array{Matrix{Complex{T}}} proj_Is :: Vector{Matrix{Complex{T}}} # It is the projector for the given kpoint only end @@ -180,7 +180,7 @@ function apply!(Hψ, op::HubbardUOperator, ψ) for m2 = 1:size(n_ii, 2) P_i_m2 = proj_I[iatom][:,m2] δm = (m1 == m2) ? one(eltype(n_ii)) : zero(eltype(n_ii)) - coefficient = 0.5 * op.Us * (δm - 2*n_ii[m1, m2]) + coefficient = 1/2 * op.Us * (δm - 2*n_ii[m1, m2]) projection = P_i_m2' * ψ.fourier Hψ.fourier .+= coefficient * projection * P_i_m1 end diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 4d0ea60a56..de2b71846b 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -6,7 +6,7 @@ using DFTK: mpi_sum using LinearAlgebra using ..TestCases: silicon using PseudoPotentialData -testcase = silicon +testcase = silicon function test_matrix_repr_operator(hamk, ψk; atol=1e-8) for operator in hamk.operators @@ -39,20 +39,17 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, n_electrons = testcase.n_electrons n_bands = div(n_electrons, 2, RoundUp) filled_occ = DFTK.filled_occupation(model) - @show filled_occ ψ = [Matrix(qr(randn(ComplexF64, length(G_vectors(basis, kpt)), n_bands)).Q) for kpt in basis.kpoints] occupation = [filled_occ * rand(n_bands) for _ = 1:length(basis.kpoints)] occ_scaling = n_electrons / sum(sum(occupation)) occupation = [occ * occ_scaling for occ in occupation] - @show occupation ρ = with_logger(NullLogger()) do compute_density(basis, ψ, occupation) end τ = compute_kinetic_energy_density(basis, ψ, occupation) E0, ham = energy_hamiltonian(basis, ψ, occupation; ρ, τ) - @show E0 @assert length(basis.terms) == 1 @@ -79,7 +76,6 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, diff_predicted = mpi_sum(diff_predicted, basis.comm_kpts) err = abs(diff - diff_predicted) - @show diff diff_predicted err rtol*abs(E0.total) atol @test err < rtol * abs(E0.total) || err < atol end end @@ -91,33 +87,33 @@ end using LinearAlgebra using .HamConsistency: test_consistency_term -# test_consistency_term(Kinetic()) -# test_consistency_term(AtomicLocal()) -# test_consistency_term(AtomicNonlocal()) -# test_consistency_term(ExternalFromReal(X -> cos(X[1]))) -# test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) -# test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) -# test_consistency_term(Hartree()) - test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3S"), 10)) -# test_consistency_term(Ewald()) -# test_consistency_term(PspCorrection()) -# test_consistency_term(Xc([:lda_xc_teter93])) -# test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) -# test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) -# test_consistency_term(Xc([:mgga_x_tpss])) -# test_consistency_term(Xc([:mgga_x_scan])) -# test_consistency_term(Xc([:mgga_c_scan]), spin_polarization=:collinear) -# test_consistency_term(Xc([:mgga_x_b00])) -# test_consistency_term(Xc([:mgga_c_b94]), spin_polarization=:collinear) -# -# let -# a = 6 -# pot(x, y, z) = (x - a/2)^2 + (y - a/2)^2 -# Apot(x, y, z) = .2 * [y - a/2, -(x - a/2), 0] -# Apot(X) = Apot(X...) -# test_consistency_term(Magnetic(Apot); kgrid=[1, 1, 1], kshift=[0, 0, 0], -# lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) -# test_consistency_term(DFTK.Anyonic(2, 3.2); kgrid=[1, 1, 1], kshift=[0, 0, 0], -# lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) -# end + test_consistency_term(Kinetic()) + test_consistency_term(AtomicLocal()) + test_consistency_term(AtomicNonlocal()) + test_consistency_term(ExternalFromReal(X -> cos(X[1]))) + test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) + test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) + test_consistency_term(Hartree()) + test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3P"), 10)) + test_consistency_term(Ewald()) + test_consistency_term(PspCorrection()) + test_consistency_term(Xc([:lda_xc_teter93])) + test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) + test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) + test_consistency_term(Xc([:mgga_x_tpss])) + test_consistency_term(Xc([:mgga_x_scan])) + test_consistency_term(Xc([:mgga_c_scan]), spin_polarization=:collinear) + test_consistency_term(Xc([:mgga_x_b00])) + test_consistency_term(Xc([:mgga_c_b94]), spin_polarization=:collinear) + + let + a = 6 + pot(x, y, z) = (x - a/2)^2 + (y - a/2)^2 + Apot(x, y, z) = .2 * [y - a/2, -(x - a/2), 0] + Apot(X) = Apot(X...) + test_consistency_term(Magnetic(Apot); kgrid=[1, 1, 1], kshift=[0, 0, 0], + lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) + test_consistency_term(DFTK.Anyonic(2, 3.2); kgrid=[1, 1, 1], kshift=[0, 0, 0], + lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) + end end diff --git a/test/hubbard.jl b/test/hubbard.jl index b81bb82486..d39d416447 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -1,4 +1,4 @@ -@testitem "Nickel Oxide" setup=[TestCases] begin +@testitem "Test Hubbard U term in Nickel Oxide" setup=[TestCases] begin using DFTK using PseudoPotentialData using Unitful @@ -42,4 +42,11 @@ @test abs(e_hub - ref) < 1e-8 end end + + # Test symmetry consistency + n_hub = scfres.n_hub + basis_nosym = unfold_bz(basis) + ρ0 = guess_density(basis_nosym, magnetic_moments) + scfres = self_consistent_field(basis_nosym; tol=1e-10, ρ=ρ0) + @test norm(n_hub .- scfres.n_hub) < 1e-8 end \ No newline at end of file From fab6fb5f6f7ddff51e05e29e2fa516b2ee48e878 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Mon, 29 Sep 2025 17:06:10 +0200 Subject: [PATCH 11/50] Comments have been resolved - New handling of measure unit for U value; - Hubbard matrix has been renamed everywhere as "nhubbard"; - Function "compute_hubbard_nIJ" has been renamed as "compute_nhubbard"; - New documentation for "compute_nhubbard" function; - New documentation for "Hubbard" struct; - New test on Wigner matrices added in tests/hubbard.jl; - Refactoring inside apply! for HubbardUOperator; - Several lines have been shortened for compactness; - Unutilized functions have been removed from terms/hubbard.jl; - Function reshape_hubbard_proj has been updated. --- ext/DFTKPlotsExt.jl | 2 +- src/postprocess/band_structure.jl | 8 ++-- src/scf/self_consistent_field.jl | 20 ++++---- src/terms/hubbard.jl | 77 ++++++++++++++++++++----------- src/terms/operators.jl | 4 +- test/hamiltonian_consistency.jl | 2 +- test/hubbard.jl | 44 +++++++++++++----- 7 files changed, 100 insertions(+), 57 deletions(-) diff --git a/ext/DFTKPlotsExt.jl b/ext/DFTKPlotsExt.jl index 9ebd911f2b..fad3ba9b5c 100644 --- a/ext/DFTKPlotsExt.jl +++ b/ext/DFTKPlotsExt.jl @@ -162,7 +162,7 @@ function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom, label=nothi p = Plots.plot(p; kwargs...) spinlabels = spin_components(basis.model) pdos = DFTK.sum_pdos(compute_pdos(εs, basis, ψ, eigenvalues; - positions, temperature=0.01, smearing), + positions, temperature, smearing), [DFTK.OrbitalManifold(;iatom, label)]) for σ = 1:n_spin plot_label = n_spin > 1 ? "$(species) $(orb_name) $(spinlabels[σ]) spin" : "$(species) $(orb_name)" diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index d2b66f23b5..50ffd7d48e 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -15,7 +15,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): kgrid::Union{AbstractKgrid,AbstractKgridGenerator}; n_bands=default_n_bands_bandstructure(basis.model), n_extra=3, ρ=nothing, τ=nothing, εF=nothing, - occupation=nothing, n_hub=nothing, + occupation=nothing, nhubbard=nothing, eigensolver=lobpcg_hyper, tol=1e-3, kwargs...) # kcoords are the kpoint coordinates in fractional coordinates if isnothing(ρ) @@ -35,7 +35,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): # Create new basis with new kpoints bs_basis = PlaneWaveBasis(basis, kgrid) - ham = Hamiltonian(bs_basis; ρ, τ, n_hub, occupation) + ham = Hamiltonian(bs_basis; ρ, τ, nhubbard, occupation) eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands + n_extra; n_conv_check=n_bands, tol, kwargs...) if !eigres.converged @@ -67,9 +67,9 @@ function compute_bands(scfres::NamedTuple, kgrid::Union{AbstractKgrid,AbstractKgridGenerator}; n_bands=default_n_bands_bandstructure(scfres), kwargs...) τ = haskey(scfres, :τ) ? scfres.τ : nothing - n_hub = haskey(scfres, :n_hub) ? scfres.n_hub : nothing + nhubbard = haskey(scfres, :nhubbard) ? scfres.nhubbard : nothing occupation = scfres.occupation - compute_bands(scfres.basis, kgrid; scfres.ρ, τ, n_hub, occupation, scfres.εF, n_bands, kwargs...) + compute_bands(scfres.basis, kgrid; scfres.ρ, τ, nhubbard, occupation, scfres.εF, n_bands, kwargs...) end """ diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index 1993175e29..daf9cca79f 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -131,7 +131,7 @@ Overview of parameters: basis::PlaneWaveBasis{T}; ρ=guess_density(basis), τ=any(needs_τ, basis.terms) ? zero(ρ) : nothing, - n_hub=nothing, + nhubbard=nothing, ψ=nothing, tol=1e-6, is_converged=ScfConvergenceDensity(tol), @@ -158,12 +158,12 @@ Overview of parameters: # We do density mixing in the real representation # TODO support other mixing types function fixpoint_map(ρin, info) - (; ψ, occupation, eigenvalues, εF, n_iter, converged, timedout, τ, n_hub) = info + (; ψ, occupation, eigenvalues, εF, n_iter, converged, timedout, τ, nhubbard) = info n_iter += 1 # Note that ρin is not the density of ψ, and the eigenvalues # are not the self-consistent ones, which makes this energy non-variational - energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρin, τ, n_hub, eigenvalues, εF) + energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρin, τ, nhubbard, eigenvalues, εF) # Diagonalize `ham` to get the new state nextstate = next_density(ham, nbandsalg, fermialg; eigensolver, ψ, eigenvalues, @@ -181,19 +181,19 @@ Overview of parameters: end for (iterm, term) in enumerate(basis.terms) if typeof(term)==DFTK.TermHubbard{Vector{Matrix{ComplexF64}}, Vector{Any}} - n_hub = compute_hubbard_nIJ(term.manifold, basis, ψ, occupation; projectors=term.P, labels=term.labels).n_IJ + nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, labels=term.labels).nhubbard end end # Update info with results gathered so far info_next = (; ham, basis, converged, stage=:iterate, algorithm="SCF", - ρin, τ, n_hub, α=damping, n_iter, nbandsalg.occupation_threshold, + ρin, τ, nhubbard, α=damping, n_iter, nbandsalg.occupation_threshold, runtime_ns=time_ns() - start_ns, nextstate..., diagonalization=[nextstate.diagonalization]) # Compute the energy of the new state if compute_consistent_energies - (; energies) = energy(basis, ψ, occupation; ρ=ρout, τ, n_hub, eigenvalues, εF) + (; energies) = energy(basis, ψ, occupation; ρ=ρout, τ, nhubbard, eigenvalues, εF) end history_Etot = vcat(info.history_Etot, energies.total) history_Δρ = vcat(info.history_Δρ, norm(Δρ) * sqrt(basis.dvol)) @@ -215,7 +215,7 @@ Overview of parameters: ρnext, info_next end - info_init = (; ρin=ρ, τ, n_hub, ψ, occupation=nothing, eigenvalues=nothing, εF=nothing, + info_init = (; ρin=ρ, τ, nhubbard, ψ, occupation=nothing, eigenvalues=nothing, εF=nothing, n_iter=0, n_matvec=0, timedout=false, converged=false, history_Etot=T[], history_Δρ=T[]) @@ -225,13 +225,13 @@ Overview of parameters: # We do not use the return value of solver but rather the one that got updated by fixpoint_map # ψ is consistent with ρout, so we return that. We also perform a last energy computation # to return a correct variational energy - (; ρin, ρout, τ, n_hub, ψ, occupation, eigenvalues, εF, converged) = info - energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρout, τ, n_hub, eigenvalues, εF) + (; ρin, ρout, τ, nhubbard, ψ, occupation, eigenvalues, εF, converged) = info + energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρout, τ, nhubbard, eigenvalues, εF) # Callback is run one last time with final state to allow callback to clean up scfres = (; ham, basis, energies, converged, nbandsalg.occupation_threshold, ρ=ρout, τ, α=damping, eigenvalues, occupation, εF, info.n_bands_converge, - info.n_iter, info.n_matvec, ψ, n_hub, info.diagonalization, stage=:finalize, + info.n_iter, info.n_matvec, ψ, nhubbard, info.diagonalization, stage=:finalize, info.history_Δρ, info.history_Etot, info.timedout, mixing, runtime_ns=time_ns() - start_ns, algorithm="SCF") callback(scfres) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 81325bc05c..4ae82da53b 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -1,4 +1,5 @@ using LinearAlgebra +using Random """ Structure for manifold choice and projectors extraction. @@ -30,7 +31,8 @@ function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, manifold_labels = [] manifold_projectors = Vector{Matrix{Complex{T}}}(undef, length(basis.kpoints)) for (iproj, orb) in enumerate(labels) - if Symbol(manifold.species) == Symbol(orb.species) && lowercase(manifold.label) == lowercase(orb.label) + if Symbol(manifold.species) == Symbol(orb.species) && + lowercase(manifold.label) == lowercase(orb.label) # If the label matches the manifold, we add it to the labels # This is useful for extracting specific orbitals from the basis # e.g., (:Si, "3S") will match all 3S orbitals of Si atoms @@ -57,31 +59,31 @@ end """ Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ -function symmetrize_nhub(n_IJ::Array{Matrix{Complex{T}}}, lattice, symmetry, positions) where {T} +function symmetrize_nhub(nhubbard::Array{Matrix{Complex{T}}}, lattice, symmetry, positions) where {T} # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) # WARNING: To implement +V this will need to be changed! - nspins = size(n_IJ, 1) - natoms = size(n_IJ, 2) + nspins = size(nhubbard, 1) + natoms = size(nhubbard, 2) nsym = length(symmetry) - l = Int64((size(n_IJ[1, 1, 1], 1)-1)/2) + l = Int64((size(nhubbard[1, 1, 1], 1)-1)/2) WigD = Wigner_sym(l, lattice, symmetry) - # Initialize the n_IJ matrix + # Initialize the nhubbard matrix ns = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) for σ in 1:nspins, iatom in 1:natoms, jatom in 1:natoms ns[σ, iatom, jatom] = zeros(Complex{T}, - size(n_IJ[σ, iatom, jatom], 1), - size(n_IJ[σ, iatom, jatom], 2)) + size(nhubbard[σ, iatom, jatom], 1), + size(nhubbard[σ, iatom, jatom], 2)) end for σ in 1:nspins, iatom in 1:natoms, isym in 1:nsym for m1 in 1:size(ns[σ, iatom, iatom], 1), m2 in 1:size(ns[σ, iatom, iatom], 2) sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry[isym]) # TODO: Here QE flips spin for time-reversal in collinear systems, should we? - for m0 in 1:size(n_IJ[σ, iatom, iatom], 1), m00 in 1:size(n_IJ[σ, iatom, iatom], 2) + for m0 in 1:size(nhubbard[σ, iatom, iatom], 1), m00 in 1:size(nhubbard[σ, iatom, iatom], 2) ns[σ, iatom, iatom][m1, m2] += WigD[m0, m1, isym] * - n_IJ[σ, sym_atom, sym_atom][m0, m00] * + nhubbard[σ, sym_atom, sym_atom][m0, m00] * WigD[m00, m2, isym] end end @@ -92,7 +94,7 @@ end """ This function returns the Wigner matrix for a given l and symmetry operation solving a randomized linear system. - The lattice L is needed to convert reduced symmetries to Cartesian space. + The lattice L is needed to convert reduced symmetries to Cartesian space. """ function Wigner_sym(l::Int64, L, symmetries::Vector{SymOp{T}}) where {T} nsym = length(symmetries) @@ -100,6 +102,7 @@ function Wigner_sym(l::Int64, L, symmetries::Vector{SymOp{T}}) where {T} if l == 0 return D .= 1 end + Random.seed!(1234) for (isym, symmetry) in enumerate(symmetries) W = symmetry.W for m1 in -l:l @@ -114,6 +117,7 @@ function Wigner_sym(l::Int64, L, symmetries::Vector{SymOp{T}}) where {T} A[n,m2+l+1] = DFTK.ylm_real(l, m2, r) end end + @assert cond(A) > 1/2 "The Wigner matrix computaton is badly conditioned." D[m1+l+1,:,isym] = A\b end end @@ -122,26 +126,33 @@ function Wigner_sym(l::Int64, L, symmetries::Vector{SymOp{T}}) where {T} end """ - compute_hubbard_nIJ(manifold, basis, ψ, occupation; [projectors, labels, positions]) + compute_nhubbard(manifold, basis, ψ, occupation; [projectors, labels, positions]) -Computes a matrix nᴵᴶ of size (nspins, natoms, natoms), where each entry nᴵᴶ[iatom, jatom] +Computes a matrix nhubbard of size (nspins, natoms, natoms), where each entry nhubbard[iatom, jatom] contains the submatrix of the occupation matrix corresponding to the projectors of atom iatom and atom jatom, with dimensions determined by the number of projectors for each atom. The atoms and orbitals are defined by the manifold tuple. + nhubbard[σ, iatom, jatom][m1, m2] = Σₖ₍ₛₚᵢₙ₎Σₙ weights[ik, ibnd] * ψₙₖ' * Pᵢₘ₁ * Pᵢₘ₂' * ψₙₖ + + where n or ibnd is the band index, ``weights[ik ibnd] = kweights[ik] * occupation[ik, ibnd]`` + and ``Pᵢₘ₁`` is the pseudoatomic orbital projector for atom i and orbital m₁ + (usually just the magnetic quantum number, since l is usually fixed). + For details on the projectors see `atomic_orbital_projectors`. + Overview of inputs: - `manifold`: OrbitalManifold with the atomic orbital type to define the Hubbard manifold. - `occupation`: Occupation matrix for the bands. - `positions`: Positions of the atoms in the unit cell. Default is model.positions. Overviw of outputs: -- `n_IJ`: 3-tensor of matrices. Outer indices select spin, iatom and jatom, +- `nhubbard`: 3-tensor of matrices. Outer indices select spin, iatom and jatom, inner indices select m1 and m2 in the manifold. - `manifold_labels`: Labels for all manifold orbitals, corresponding to different columns of p_I. - `p_I`: Projectors for the manifold. Those are orthonormalized against all orbitals, also against those outside of the manifold. """ -function compute_hubbard_nIJ(manifold::OrbitalManifold, +function compute_nhubbard(manifold::OrbitalManifold, basis::PlaneWaveBasis{T}, ψ, occupation; projectors, labels, @@ -161,10 +172,11 @@ function compute_hubbard_nIJ(manifold::OrbitalManifold, n_matrix = mpi_sum(n_matrix, basis.comm_kpts) # Now I want to reshape it to match the notation used in the papers. - # Reshape into n[I, J, σ][m1, m2] where I, J indicate the atom in the Hubbard manifold, σ is the spin, m1 and m2 are magnetic quantum numbers (n, l are fixed) + # Reshape into n[I, J, σ][m1, m2] where I, J indicate the atom in the Hubbard manifold, σ is the spin, + # m1 and m2 are magnetic quantum numbers (n, l are fixed) manifold_atoms = findall(at -> at.species == Symbol(manifold.species), basis.model.atoms) natoms = length(manifold_atoms) # Number of atoms of the species in the manifold - n_IJ = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) + nhubbard = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for ik in 1:length(basis.kpoints)] # Very low-level, but works for σ in 1:nspins, iatom in eachindex(manifold_atoms) @@ -179,7 +191,8 @@ function compute_hubbard_nIJ(manifold::OrbitalManifold, j = 1 while j <= nprojs jl = labels[j].l - (manifold_atoms[jatom] == labels[j].iatom) && (n_IJ[σ, iatom, jatom] = n_matrix[σ, i:i+2*il, j:j+2*jl]) + (manifold_atoms[jatom] == labels[j].iatom) && + (nhubbard[σ, iatom, jatom] = n_matrix[σ, i:i+2*il, j:j+2*jl]) j += 2*jl + 1 end end @@ -190,9 +203,10 @@ function compute_hubbard_nIJ(manifold::OrbitalManifold, end end - n_IJ = symmetrize_nhub(n_IJ, basis.model.lattice, basis.symmetries, basis.model.positions[manifold_atoms]) + nhubbard = symmetrize_nhub(nhubbard, basis.model.lattice, + basis.symmetries, basis.model.positions[manifold_atoms]) - return (; n_IJ=n_IJ, manifold_labels=labels, p_I=p_I) + return (; nhubbard, manifold_labels=labels, p_I) end function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} @@ -218,6 +232,12 @@ function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, lab return p_I end +@doc raw""" +Hubbard energy: +```math +1/2 Σ_{σI} U * Tr[nhubbard[σ,i,i] * (1 - nhubbard[σ,i,i])] +``` +""" struct Hubbard manifold :: OrbitalManifold U @@ -239,24 +259,24 @@ end @timing "ene_ops: hubbard" function ene_ops(term::TermHubbard, basis::PlaneWaveBasis{T}, - ψ, occupation; n_hub=nothing, ψ_hub=nothing, + ψ, occupation; nhubbard=nothing, ψ_hub=nothing, labels=term.labels, kwargs...) where {T} if isnothing(ψ) - if isnothing(n_hub) + if isnothing(nhubbard) return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end ψ = ψ_hub proj = reshape_hubbard_proj(basis, term.P, term.labels, term.manifold) else - Hubbard = compute_hubbard_nIJ(term.manifold, basis, ψ, occupation; projectors=term.P, + Hubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, labels) - n_hub = Hubbard.n_IJ + nhubbard = Hubbard.nhubbard proj = Hubbard.p_I end - #@show proj[1][1] - ops = [HubbardUOperator(basis, kpt, term.U, n_hub, proj[ik]) for (ik,kpt) in enumerate(basis.kpoints)] + ops = [HubbardUOperator(basis, kpt, term.U, nhubbard, proj[ik]) + for (ik,kpt) in enumerate(basis.kpoints)] filled_occ = filled_occupation(basis.model) types = findall(at -> at.species == Symbol(term.manifold.species), basis.model.atoms) @@ -265,7 +285,8 @@ end E = zero(T) for σ in 1:nspins, iatom in 1:natoms - E += filled_occ * 1/2 * term.U * real(tr(n_hub[σ, iatom,iatom] * (I - n_hub[σ, iatom,iatom]))) + E += filled_occ * 1/2 * term.U * + real(tr(nhubbard[σ, iatom, iatom] * (I - nhubbard[σ, iatom, iatom]))) end - return (; E, ops, n_hub) + return (; E, ops, nhubbard) end diff --git a/src/terms/operators.jl b/src/terms/operators.jl index 456bc69425..a571422124 100644 --- a/src/terms/operators.jl +++ b/src/terms/operators.jl @@ -156,7 +156,7 @@ end # TODO Implement Matrix(op::DivAgrad) @doc raw""" -"Hubbard U" operator ``Hψ = Σᵢ Σₘ₁ₘ₂ U/2 * (1 - 2n[i,i][m1,m2]) * Pᵢₘ₁ * Pᵢₘ₂' * ψ`` +"Hubbard U" operator ``V_σ ψ = Σᵢ Σₘ₁ₘ₂ U/2 * (1 - 2nhubbard[σ, i,i][m1,m2]) * Pᵢₘ₁ * Pᵢₘ₂' * ψ`` where ``Pᵢₘ₁`` is the projector for atom i and orbital m₁. (m₁ is usually just the magnetic quantum number, since l is usually fixed) """ @@ -182,7 +182,7 @@ function apply!(Hψ, op::HubbardUOperator, ψ) δm = (m1 == m2) ? one(eltype(n_ii)) : zero(eltype(n_ii)) coefficient = 1/2 * op.Us * (δm - 2*n_ii[m1, m2]) projection = P_i_m2' * ψ.fourier - Hψ.fourier .+= coefficient * projection * P_i_m1 + Hψ.fourier .+= P_i_m1 * coefficient * projection end end end diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index de2b71846b..c52ffb249f 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -32,7 +32,7 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, n_dim = 3 - count(iszero, eachcol(lattice)) Si = n_dim == 3 ? ElementPsp(14, load_psp(testcase.psp_upf)) : ElementCoulomb(:Si) atoms = [Si, Si] - model = Model(lattice, atoms, testcase.positions; terms=[term], #spin_polarization=:collinear, + model = Model(lattice, atoms, testcase.positions; terms=[term], spin_polarization, symmetries=true) basis = PlaneWaveBasis(model; Ecut, kgrid=MonkhorstPack(kgrid; kshift)) diff --git a/test/hubbard.jl b/test/hubbard.jl index d39d416447..e307469181 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -3,9 +3,10 @@ using PseudoPotentialData using Unitful using UnitfulAtomic + using LinearAlgebra # Hubbard parameters - U = 10 + U = 10u"eV" manifold = DFTK.OrbitalManifold(;species=:Ni, label="3D") a = 7.9 # Bohr @@ -22,8 +23,6 @@ [0.75, 0.75, 0.75]] magnetic_moments = [2, 0, -1, 0] - ψ=nothing - occupation=nothing model = model_DFT(lattice, atoms, positions; extra_terms=[DFTK.Hubbard(manifold, U)], temperature=0.01, functionals=PBE(), @@ -37,16 +36,39 @@ @test abs(e_total - ref) < 1e-8 for (term, value) in scfres.energies if term == "Hubbard" - ref = 0.17629078433258719 + ref_hub = 0.17629078433258719 e_hub = value - @test abs(e_hub - ref) < 1e-8 + @test abs(e_hub - ref_hub) < 1e-8 end end - + # Test symmetry consistency - n_hub = scfres.n_hub - basis_nosym = unfold_bz(basis) + n_hub = scfres.nhubbard + basis_nosym = DFTK.unfold_bz(basis) ρ0 = guess_density(basis_nosym, magnetic_moments) - scfres = self_consistent_field(basis_nosym; tol=1e-10, ρ=ρ0) - @test norm(n_hub .- scfres.n_hub) < 1e-8 -end \ No newline at end of file + scfres_nosym = self_consistent_field(basis_nosym; tol=1e-10, ρ=ρ0) + @test norm(n_hub .- scfres_nosym.nhubbard) < 1e-8 + +end + +@testitem "Test Wigner matrices on Silicon symmetries" setup=[TestCases] begin + using DFTK + using PseudoPotentialData + using LinearAlgebra + + lattice = [[0 1 1.]; + [1 0 1.]; + [1 1 0.]] + positions = [ones(3)/8, -ones(3)/8] + pseudopotentials = PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf") + Si = ElementPsp(:Si, pseudopotentials) + atoms = [Si, Si] + model = model_DFT(lattice, atoms, positions; functionals=PBE()) + basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2]) + + D = DFTK.Wigner_sym(1, lattice, basis.symmetries) + D5 = [-1 0 0; 0 -1 0; 0 0 1] + @test norm(D[:,:,1] - I) < 1e-8 + @test norm(D[:,:,25] + I) < 1e-8 + @test norm(D[:,:,5] - D5) < 1e-8 +end \ No newline at end of file From 5aefec236cd095779898736229f7bab037f6c61d Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Tue, 30 Sep 2025 11:54:26 +0200 Subject: [PATCH 12/50] Added nhubbard field to scfres in forwarddiff_rules. --- src/scf/self_consistent_field.jl | 1 + src/workarounds/forwarddiff_rules.jl | 1 + 2 files changed, 2 insertions(+) diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index daf9cca79f..c579c8b2b0 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -190,6 +190,7 @@ Overview of parameters: ρin, τ, nhubbard, α=damping, n_iter, nbandsalg.occupation_threshold, runtime_ns=time_ns() - start_ns, nextstate..., diagonalization=[nextstate.diagonalization]) + @show nhubbard # Compute the energy of the new state if compute_consistent_energies diff --git a/src/workarounds/forwarddiff_rules.jl b/src/workarounds/forwarddiff_rules.jl index 26f946db28..41987d2d3c 100644 --- a/src/workarounds/forwarddiff_rules.jl +++ b/src/workarounds/forwarddiff_rules.jl @@ -270,6 +270,7 @@ function self_consistent_field(basis_dual::PlaneWaveBasis{T}; # This has to be changed whenever the scfres structure changes (; ham, basis=basis_dual, energies, ρ, eigenvalues, occupation, εF, ψ, scfres.τ, # TODO make τ also differentiable for meta-GGA DFPT + scfres.nhubbard, # non-differentiable metadata: response=getfield.(δresults, :info_gmres), scfres.converged, scfres.occupation_threshold, scfres.α, scfres.n_iter, From 0d69edd089959831bd13c20cca9135dff1e4f115 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Tue, 30 Sep 2025 14:03:50 +0200 Subject: [PATCH 13/50] Update to Wigner_sym, now wigner_d_matrix and relative test --- src/common/spherical_harmonics.jl | 37 +++++++++++++++++++ src/postprocess/band_structure.jl | 5 ++- src/postprocess/dos.jl | 2 +- src/scf/self_consistent_field.jl | 5 +-- src/terms/hubbard.jl | 61 ++++++++----------------------- test/hubbard.jl | 57 ++++++++++++++++++----------- 6 files changed, 93 insertions(+), 74 deletions(-) diff --git a/src/common/spherical_harmonics.jl b/src/common/spherical_harmonics.jl index ec46b871eb..ac68fdd3a7 100644 --- a/src/common/spherical_harmonics.jl +++ b/src/common/spherical_harmonics.jl @@ -47,3 +47,40 @@ function ylm_real(l::Integer, m::Integer, rvec::AbstractVector{T}) where {T} error("The case l = $l and m = $m is not implemented") end + +""" + This function returns the Wigner D matrix for real spherical harmonics, + for a given l and symmetry operation, solving a randomized linear system. + Such matrix gives the decomposition of a spherical harmonic after application + of a symmetry operation back into the basis of spherical harmonics. + + Yₗₘ₁(R̂r) = Σₘ₂ D(l,R̂)ₘ₁ₘ₂ * Yₗₘ₂(r) + + The lattice is needed to convert reduced symmetries to Cartesian space. +""" +function wigner_d_matrix(l::Integer, Wcart::AbstractMatrix{T}) where {T} + D = Matrix{T}(undef, 2*l+1, 2*l+1) + if l == 0 + return D .= 1 + end + rng = Xoshiro(1234) + neq = 4*(2*l+1) + for m1 in -l:l + b = Vector{T}(undef, neq) + A = Matrix{T}(undef, neq, 2*l+1) + for n in 1:neq + r = rand(rng, T, 3) + r = r / norm(r) + r0 = Wcart * r + b[n] = DFTK.ylm_real(l, m1, r0) + for m2 in -l:l + A[n,m2+l+1] = DFTK.ylm_real(l, m2, r) + end + end + κ = cond(A) + @assert κ < 10.0 "The Wigner matrix computation is badly conditioned. κ(A)=$(κ)" + D[m1+l+1,:] = A\b + end + + return D +end \ No newline at end of file diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index 50ffd7d48e..72006cf094 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -68,8 +68,9 @@ function compute_bands(scfres::NamedTuple, n_bands=default_n_bands_bandstructure(scfres), kwargs...) τ = haskey(scfres, :τ) ? scfres.τ : nothing nhubbard = haskey(scfres, :nhubbard) ? scfres.nhubbard : nothing - occupation = scfres.occupation - compute_bands(scfres.basis, kgrid; scfres.ρ, τ, nhubbard, occupation, scfres.εF, n_bands, kwargs...) + compute_bands(scfres.basis, kgrid; + scfres.ρ, τ, nhubbard, scfres.occupation, + scfres.εF, n_bands, kwargs...) end """ diff --git a/src/postprocess/dos.jl b/src/postprocess/dos.jl index 55ce2c6489..540333017a 100644 --- a/src/postprocess/dos.jl +++ b/src/postprocess/dos.jl @@ -166,7 +166,7 @@ function atomic_orbital_projectors(basis::PlaneWaveBasis{T}; labels = [] for (iatom, atom) in enumerate(basis.model.atoms) psp = atom.psp - @assert !iszero(size(psp.r2_pswfcs[1], 1)) "FATAL ERROR: No Atomic projector found within the provided PseudoPotential." + count_n_pswfc(psp) for l in 0:psp.lmax, n in 1:DFTK.count_n_pswfc_radial(psp, l) label = DFTK.pswfc_label(psp, n, l) if !isonmanifold((; iatom, atom.species, label)) diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index c579c8b2b0..f9acc5739b 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -179,8 +179,8 @@ Overview of parameters: if any(needs_τ, basis.terms) τ = compute_kinetic_energy_density(basis, ψ, occupation) end - for (iterm, term) in enumerate(basis.terms) - if typeof(term)==DFTK.TermHubbard{Vector{Matrix{ComplexF64}}, Vector{Any}} + for term in basis.terms + if isa(term, DFTK.TermHubbard) nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, labels=term.labels).nhubbard end end @@ -190,7 +190,6 @@ Overview of parameters: ρin, τ, nhubbard, α=damping, n_iter, nbandsalg.occupation_threshold, runtime_ns=time_ns() - start_ns, nextstate..., diagonalization=[nextstate.diagonalization]) - @show nhubbard # Compute the energy of the new state if compute_consistent_energies diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 4ae82da53b..80ca7f0533 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -59,15 +59,14 @@ end """ Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ -function symmetrize_nhub(nhubbard::Array{Matrix{Complex{T}}}, lattice, symmetry, positions) where {T} +function symmetrize_nhub(nhubbard::Array{Matrix{Complex{T}}}, lattice, symmetries, positions) where {T} # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) # WARNING: To implement +V this will need to be changed! nspins = size(nhubbard, 1) natoms = size(nhubbard, 2) - nsym = length(symmetry) + nsym = length(symmetries) l = Int64((size(nhubbard[1, 1, 1], 1)-1)/2) - WigD = Wigner_sym(l, lattice, symmetry) # Initialize the nhubbard matrix ns = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) @@ -77,52 +76,22 @@ function symmetrize_nhub(nhubbard::Array{Matrix{Complex{T}}}, lattice, symmetry, size(nhubbard[σ, iatom, jatom], 2)) end - for σ in 1:nspins, iatom in 1:natoms, isym in 1:nsym - for m1 in 1:size(ns[σ, iatom, iatom], 1), m2 in 1:size(ns[σ, iatom, iatom], 2) - sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry[isym]) - # TODO: Here QE flips spin for time-reversal in collinear systems, should we? - for m0 in 1:size(nhubbard[σ, iatom, iatom], 1), m00 in 1:size(nhubbard[σ, iatom, iatom], 2) - ns[σ, iatom, iatom][m1, m2] += WigD[m0, m1, isym] * - nhubbard[σ, sym_atom, sym_atom][m0, m00] * - WigD[m00, m2, isym] - end - end - end - ns .= ns / nsym -end - -""" - This function returns the Wigner matrix for a given l and symmetry operation - solving a randomized linear system. - The lattice L is needed to convert reduced symmetries to Cartesian space. -""" -function Wigner_sym(l::Int64, L, symmetries::Vector{SymOp{T}}) where {T} - nsym = length(symmetries) - D = Array{Float64}(undef, 2*l+1, 2*l+1, nsym) - if l == 0 - return D .= 1 - end - Random.seed!(1234) - for (isym, symmetry) in enumerate(symmetries) - W = symmetry.W - for m1 in -l:l - b = Vector{Float64}(undef, 2*l+1) - A = Matrix{Float64}(undef, 2*l+1, 2*l+1) - for n in 1:2*l+1 - r = rand(Float64, 3) - r = r / norm(r) - r0 = L * W * inv(L) * r - b[n] = DFTK.ylm_real(l, m1, r0) - for m2 in -l:l - A[n,m2+l+1] = DFTK.ylm_real(l, m2, r) + for symmetry in symmetries + Wcart = lattice * symmetry.W * inv(lattice) + WigD = wigner_d_matrix(l, Wcart) + for σ in 1:nspins, iatom in 1:natoms + sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry) + for m1 in 1:size(ns[σ, iatom, iatom], 1), m2 in 1:size(ns[σ, iatom, iatom], 2) + # TODO: Here QE flips spin for time-reversal in collinear systems, should we? + for m0 in 1:size(nhubbard[σ, iatom, iatom], 1), m00 in 1:size(nhubbard[σ, iatom, iatom], 2) + ns[σ, iatom, iatom][m1, m2] += WigD[m0, m1] * + nhubbard[σ, sym_atom, sym_atom][m0, m00] * + WigD[m00, m2] end end - @assert cond(A) > 1/2 "The Wigner matrix computaton is badly conditioned." - D[m1+l+1,:,isym] = A\b end end - - return D + ns .= ns / nsym end """ @@ -243,7 +212,7 @@ struct Hubbard U end function (hubbard::Hubbard)(basis::AbstractBasis) - isempty(hubbard.U) && return TermNoop() + iszero(hubbard.U) && return TermNoop() projs, labs = atomic_orbital_projectors(basis) labels, projectors = extract_manifold(basis, projs, labs, hubbard.manifold) U = austrip(hubbard.U) diff --git a/test/hubbard.jl b/test/hubbard.jl index e307469181..f845042ed5 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -1,3 +1,38 @@ +@testitem "Test Wigner matrices on Silicon symmetries" setup=[TestCases] begin + using DFTK + using PseudoPotentialData + using LinearAlgebra + + # Identity + Id = Float64[1 0 0; 0 1 0; 0 0 1] + D = DFTK.wigner_d_matrix(1, Id) + @test norm(D - I) < 1e-8 + D = DFTK.wigner_d_matrix(2, Id) + @test norm(D - I) < 1e-8 + # This reverts all p orbitals, sends all d orbitals in themselves + Inv = -Id + D = DFTK.wigner_d_matrix(1, Inv) + @test norm(D + I) < 1e-8 + D = DFTK.wigner_d_matrix(2, Inv) + @test norm(D - I) < 1e-8 + # This keeps pz, dz2, dx2-y2 and dxy unchanged, changes sign to all others + A3 = Float64[1 0 0; 0 -1 0; 0 0 -1] + D3p = Float64[-1 0 0; 0 -1 0; 0 0 1] + D3d = Float64[-1 0 0 0 0; 0 1 0 0 0; 0 0 1 0 0; 0 0 0 -1 0; 0 0 0 0 1] + D = DFTK.wigner_d_matrix(1, A3) + @test norm(D - D3p) < 1e-8 + D = DFTK.wigner_d_matrix(2, A3) + @test norm(D - D3d) < 1e-8 + # This sends: px <-> py, dxz <-> dyz, dx2-y2 -> -(dx2-y2) and keeps the other fixed + A3 = Float64[0 1 0; 1 0 0; 0 0 1] + D3p = Float64[0 0 1; 0 1 0; 1 0 0] + D3d = Float64[1 0 0 0 0; 0 0 0 1 0; 0 0 1 0 0; 0 1 0 0 0; 0 0 0 0 -1] + D = DFTK.wigner_d_matrix(1, A3) + @test norm(D - D3p) < 1e-8 + D = DFTK.wigner_d_matrix(2, A3) + @test norm(D - D3d) < 1e-8 +end + @testitem "Test Hubbard U term in Nickel Oxide" setup=[TestCases] begin using DFTK using PseudoPotentialData @@ -50,25 +85,3 @@ @test norm(n_hub .- scfres_nosym.nhubbard) < 1e-8 end - -@testitem "Test Wigner matrices on Silicon symmetries" setup=[TestCases] begin - using DFTK - using PseudoPotentialData - using LinearAlgebra - - lattice = [[0 1 1.]; - [1 0 1.]; - [1 1 0.]] - positions = [ones(3)/8, -ones(3)/8] - pseudopotentials = PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf") - Si = ElementPsp(:Si, pseudopotentials) - atoms = [Si, Si] - model = model_DFT(lattice, atoms, positions; functionals=PBE()) - basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2]) - - D = DFTK.Wigner_sym(1, lattice, basis.symmetries) - D5 = [-1 0 0; 0 -1 0; 0 0 1] - @test norm(D[:,:,1] - I) < 1e-8 - @test norm(D[:,:,25] + I) < 1e-8 - @test norm(D[:,:,5] - D5) < 1e-8 -end \ No newline at end of file From d0cf01ad7e0a80ea327ad00acda362d6747b3328 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Tue, 30 Sep 2025 16:21:13 +0200 Subject: [PATCH 14/50] Change in testing - Kwarg use_nlcc=false has been added for all meta_gga tests; - Kwarg spin_polarization=:none added to anyonic test, to avoid failure from @assert length(basis.kpoints) == 1; - Tolerance for condition number in wigner_d_matrix increased to 100, neq decreased to 2*l+2. --- src/common/spherical_harmonics.jl | 4 ++-- src/scf/self_consistent_field.jl | 3 ++- src/terms/hubbard.jl | 15 +++++++++------ test/hamiltonian_consistency.jl | 17 ++++++++--------- test/hubbard.jl | 2 +- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/common/spherical_harmonics.jl b/src/common/spherical_harmonics.jl index ac68fdd3a7..c2359ba84a 100644 --- a/src/common/spherical_harmonics.jl +++ b/src/common/spherical_harmonics.jl @@ -64,7 +64,7 @@ function wigner_d_matrix(l::Integer, Wcart::AbstractMatrix{T}) where {T} return D .= 1 end rng = Xoshiro(1234) - neq = 4*(2*l+1) + neq = (2*l+2) # This value should work for p and d orbitals, but can be increased if needed for m1 in -l:l b = Vector{T}(undef, neq) A = Matrix{T}(undef, neq, 2*l+1) @@ -78,7 +78,7 @@ function wigner_d_matrix(l::Integer, Wcart::AbstractMatrix{T}) where {T} end end κ = cond(A) - @assert κ < 10.0 "The Wigner matrix computation is badly conditioned. κ(A)=$(κ)" + @assert κ < 100.0 "The Wigner matrix computation is badly conditioned. κ(A)=$(κ)" D[m1+l+1,:] = A\b end diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index f9acc5739b..0a79cd7a48 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -181,7 +181,8 @@ Overview of parameters: end for term in basis.terms if isa(term, DFTK.TermHubbard) - nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, labels=term.labels).nhubbard + nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; + projectors=term.P, labels=term.labels).nhubbard end end diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 80ca7f0533..580e5d3a3f 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -59,7 +59,8 @@ end """ Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ -function symmetrize_nhub(nhubbard::Array{Matrix{Complex{T}}}, lattice, symmetries, positions) where {T} +function symmetrize_nhub(nhubbard::Array{Matrix{Complex{T}}}, + lattice, symmetries, positions) where {T} # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) # WARNING: To implement +V this will need to be changed! @@ -83,7 +84,7 @@ function symmetrize_nhub(nhubbard::Array{Matrix{Complex{T}}}, lattice, symmetrie sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry) for m1 in 1:size(ns[σ, iatom, iatom], 1), m2 in 1:size(ns[σ, iatom, iatom], 2) # TODO: Here QE flips spin for time-reversal in collinear systems, should we? - for m0 in 1:size(nhubbard[σ, iatom, iatom], 1), m00 in 1:size(nhubbard[σ, iatom, iatom], 2) + for m0 in 1:size(nhubbard[σ,iatom,iatom],1), m00 in 1:size(nhubbard[σ,iatom,iatom],2) ns[σ, iatom, iatom][m1, m2] += WigD[m0, m1] * nhubbard[σ, sym_atom, sym_atom][m0, m00] * WigD[m00, m2] @@ -135,14 +136,15 @@ function compute_nhubbard(manifold::OrbitalManifold, # We divide by filled_occ to deal with the physical two spin channels separately. ψk, projk, nk = @views ψ[ik], projectors[ik], occupation[ik]/filled_occ c = projk' * ψk # <ϕ|ψ> - # The matrix product is done over the bands. In QE, basis.kweights[ik]*nk[ibnd] would be wg(ik,ibnd) + # The matrix product is done over the bands. + # In QE, basis.kweights[ik]*nk[ibnd] would be wg(ik,ibnd) n_matrix[σ, :, :] .+= basis.kweights[ik] * c * diagm(nk) * c' end n_matrix = mpi_sum(n_matrix, basis.comm_kpts) # Now I want to reshape it to match the notation used in the papers. - # Reshape into n[I, J, σ][m1, m2] where I, J indicate the atom in the Hubbard manifold, σ is the spin, - # m1 and m2 are magnetic quantum numbers (n, l are fixed) + # Reshape into n[I, J, σ][m1, m2] where I, J indicate the atom in the Hubbard manifold, + # σ is the spin, m1 and m2 are magnetic quantum numbers (n, l are fixed) manifold_atoms = findall(at -> at.species == Symbol(manifold.species), basis.model.atoms) natoms = length(manifold_atoms) # Number of atoms of the species in the manifold nhubbard = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) @@ -178,7 +180,8 @@ function compute_nhubbard(manifold::OrbitalManifold, return (; nhubbard, manifold_labels=labels, p_I) end -function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} +function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, + labels, manifold) where {T} manifold_atoms = findall(at -> at.species == Symbol(manifold.species), basis.model.atoms) natoms = length(manifold_atoms) nprojs = length(labels) diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index c52ffb249f..ba1734ba7e 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -5,8 +5,7 @@ using Logging using DFTK: mpi_sum using LinearAlgebra using ..TestCases: silicon -using PseudoPotentialData -testcase = silicon +testcase = silicon function test_matrix_repr_operator(hamk, ψk; atol=1e-8) for operator in hamk.operators @@ -94,17 +93,17 @@ end test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) test_consistency_term(Hartree()) - test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3P"), 10)) + test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3P"), 0.01), ) test_consistency_term(Ewald()) test_consistency_term(PspCorrection()) test_consistency_term(Xc([:lda_xc_teter93])) test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_tpss])) - test_consistency_term(Xc([:mgga_x_scan])) - test_consistency_term(Xc([:mgga_c_scan]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_b00])) - test_consistency_term(Xc([:mgga_c_b94]), spin_polarization=:collinear) + test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) + test_consistency_term(Xc([:mgga_x_scan]; use_nlcc=false)) + test_consistency_term(Xc([:mgga_c_scan]; use_nlcc=false), spin_polarization=:collinear) + test_consistency_term(Xc([:mgga_x_b00]; use_nlcc=false)) + test_consistency_term(Xc([:mgga_c_b94]; use_nlcc=false), spin_polarization=:collinear) let a = 6 @@ -114,6 +113,6 @@ end test_consistency_term(Magnetic(Apot); kgrid=[1, 1, 1], kshift=[0, 0, 0], lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) test_consistency_term(DFTK.Anyonic(2, 3.2); kgrid=[1, 1, 1], kshift=[0, 0, 0], - lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) + lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20, spin_polarization=:none) end end diff --git a/test/hubbard.jl b/test/hubbard.jl index f845042ed5..4c32b6432e 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -1,4 +1,4 @@ -@testitem "Test Wigner matrices on Silicon symmetries" setup=[TestCases] begin +@testitem "Test Wigner matrices" setup=[TestCases] begin using DFTK using PseudoPotentialData using LinearAlgebra From 16a3f6f92aad9ef76b86beb0d299d71f847632ce Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 2 Oct 2025 12:01:26 +0200 Subject: [PATCH 15/50] New example in examples/hubbard.jl --- docs/make.jl | 157 ++++++++++++++++---------------- examples/hubbard.jl | 60 ++++++++++++ ext/DFTKPlotsExt.jl | 7 +- test/hamiltonian_consistency.jl | 8 +- 4 files changed, 147 insertions(+), 85 deletions(-) create mode 100644 examples/hubbard.jl diff --git a/docs/make.jl b/docs/make.jl index 3e708ac06f..af31330401 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -14,87 +14,88 @@ # Structure of the docs. List files as *.jl or *.md here. All files # ending in *.jl will be processed to *.md with Literate. PAGES = [ - "Home" => "index.md", - "features.md", - "Getting started" => [ - "guide/installation.md", - "guide/tutorial.jl", - ], - "Background" => [ - # Theoretical background - "guide/introductory_resources.md", - "guide/periodic_problems.jl", - "guide/discretisation.jl", - "guide/atomic_chains.jl", - "guide/density_functional_theory.md", - "guide/self_consistent_field.jl", - "school2022.md", - ], +# "Home" => "index.md", +# "features.md", +# "Getting started" => [ +# "guide/installation.md", +# "guide/tutorial.jl", +# ], +# "Background" => [ +# # Theoretical background +# "guide/introductory_resources.md", +# "guide/periodic_problems.jl", +# "guide/discretisation.jl", +# "guide/atomic_chains.jl", +# "guide/density_functional_theory.md", +# "guide/self_consistent_field.jl", +# "school2022.md", +# ], "Basic DFT calculations" => [ # Ground-state DFT calculations, standard problems and modelling techniques # Some basic show cases; may feature integration of DFTK with other packages. - "examples/metallic_systems.jl", - "examples/collinear_magnetism.jl", - "examples/convergence_study.jl", - "examples/pseudopotentials.jl", - "examples/supercells.jl", - "examples/gaas_surface.jl", - "examples/graphene.jl", - "examples/geometry_optimization.jl", - "examples/energy_cutoff_smearing.jl", - ], - "Response and properties" => [ - "examples/polarizability.jl", - "examples/forwarddiff.jl", - "examples/phonons.jl", - ], - "Ecosystem integration" => [ - # This concerns the discussion of interfaces, IO and integration - # options we have - "ecosystem/atomsbase.jl", - "ecosystem/atomscalculators.jl", - "ecosystem/input_output.jl", - "ecosystem/atomistic_simulation_environment.md", - "ecosystem/wannier.jl", - ], - "Tips and tricks" => [ - # Resolving convergence issues, what solver to use, improving performance or - # reliability of calculations. - "tricks/achieving_convergence.md", - "tricks/parallelization.md", - "tricks/gpu.jl", - "tricks/scf_checkpoints.jl", - "tricks/compute_clusters.md", - ], - "Solvers" => [ - "examples/custom_solvers.jl", - "examples/scf_callbacks.jl", - "examples/compare_solvers.jl", - "examples/analysing_scf_convergence.jl", - ], - "Nonstandard models" => [ - "examples/gross_pitaevskii.jl", - "examples/gross_pitaevskii_2D.jl", - "examples/custom_potential.jl", - "examples/cohen_bergstresser.jl", - "examples/anyons.jl", - ], - "Error control" => [ - "examples/arbitrary_floattype.jl", - "examples/error_estimates_forces.jl", - ], - "Developer resources" => [ - "developer/setup.md", - "developer/testsystem.md", - "developer/conventions.md", - "developer/style_guide.md", - "developer/data_structures.md", - "developer/useful_formulas.md", - "developer/symmetries.md", - "developer/gpu_computations.md", +# "examples/metallic_systems.jl", +# "examples/collinear_magnetism.jl", +# "examples/convergence_study.jl", +# "examples/pseudopotentials.jl", +# "examples/supercells.jl", + "examples/hubbard.jl", +# "examples/gaas_surface.jl", +# "examples/graphene.jl", +# "examples/geometry_optimization.jl", +# "examples/energy_cutoff_smearing.jl", ], - "api.md", - "publications.md", +# "Response and properties" => [ +# "examples/polarizability.jl", +# "examples/forwarddiff.jl", +# "examples/phonons.jl", +# ], +# "Ecosystem integration" => [ +# # This concerns the discussion of interfaces, IO and integration +# # options we have +# "ecosystem/atomsbase.jl", +# "ecosystem/atomscalculators.jl", +# "ecosystem/input_output.jl", +# "ecosystem/atomistic_simulation_environment.md", +# "ecosystem/wannier.jl", +# ], +# "Tips and tricks" => [ +# # Resolving convergence issues, what solver to use, improving performance or +# # reliability of calculations. +# "tricks/achieving_convergence.md", +# "tricks/parallelization.md", +# "tricks/gpu.jl", +# "tricks/scf_checkpoints.jl", +# "tricks/compute_clusters.md", +# ], +# "Solvers" => [ +# "examples/custom_solvers.jl", +# "examples/scf_callbacks.jl", +# "examples/compare_solvers.jl", +# "examples/analysing_scf_convergence.jl", +# ], +# "Nonstandard models" => [ +# "examples/gross_pitaevskii.jl", +# "examples/gross_pitaevskii_2D.jl", +# "examples/custom_potential.jl", +# "examples/cohen_bergstresser.jl", +# "examples/anyons.jl", +# ], +# "Error control" => [ +# "examples/arbitrary_floattype.jl", +# "examples/error_estimates_forces.jl", +# ], +# "Developer resources" => [ +# "developer/setup.md", +# "developer/testsystem.md", +# "developer/conventions.md", +# "developer/style_guide.md", +# "developer/data_structures.md", +# "developer/useful_formulas.md", +# "developer/symmetries.md", +# "developer/gpu_computations.md", +# ], +# "api.md", +# "publications.md", ] # Files from the /examples folder that need to be copied over to the docs @@ -104,7 +105,7 @@ EXAMPLE_ASSETS = [] # Specify e.g. as "examples/Fe_afm.pwi" # # Configuration and setup # -DEBUG = false # Set to true to disable some checks and cleanup +DEBUG = true # Set to true to disable some checks and cleanup import LibGit2 import Pkg diff --git a/examples/hubbard.jl b/examples/hubbard.jl new file mode 100644 index 0000000000..903ce1ceb0 --- /dev/null +++ b/examples/hubbard.jl @@ -0,0 +1,60 @@ +# # Hubbard correction (DFT+U) +# In this example, we'll plot the DOS and projected DOS of Nickel Oxide +# with and without the Hubbard term correction. + +using DFTK +using Printf +using PseudoPotentialData +using Unitful +using UnitfulAtomic +using Plots + +# Define the geometry and pseudopotential +a = 7.9 # Nickel Oxide lattice constant in Bohr +lattice = a * [[ 1.0 0.5 0.5]; + [ 0.5 1.0 0.5]; + [ 0.5 0.5 1.0]] +pseudopotentials = PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf") +Ni, O = ElementPsp(:Ni, pseudopotentials), ElementPsp(:O, pseudopotentials) +atoms = [Ni, O, Ni, O] +positions = [zeros(3), ones(3) / 4, ones(3) / 2, ones(3) * 3 / 4] +magnetic_moments = [2, 0, 2, 0] + +# First, we run an SCF and NSCF without the Hubbard term +model = model_DFT(lattice, atoms, positions; temperature=5e-3, + functionals = PBE(), magnetic_moments=magnetic_moments) +basis = PlaneWaveBasis(model; Ecut = 15, kgrid = [2, 2, 2] ) +scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) +bands = compute_bands(scfres, MonkhorstPack(2, 2, 2); ρ=scfres.ρ) + +εF = bands.εF +width = 5.0u"eV" +εrange = (εF - austrip(width), εF + austrip(width)) +band_gap = bands.eigenvalues[1][25] - bands.eigenvalues[1][24] + +# Then we plot the DOS and the PDOS for the relevant 3D (pseudo)atomic projector +p = plot_dos(bands; εrange, temperature=2e-3, colors=[:red, :red]) +p = plot_pdos(bands; p, temperature=2e-3, iatom=1, label="3D", colors=[:yellow, :orange], εrange) + +# To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant +U = 10u"eV" +manifold = DFTK.OrbitalManifold(;species=:Ni, label="3D") + +# Run SCF +model = model_DFT(lattice, atoms, positions; extra_terms=[DFTK.Hubbard(manifold, U)], + functionals = PBE(), temperature=5e-3, magnetic_moments=magnetic_moments) +basis = PlaneWaveBasis(model; Ecut = 15, kgrid = [2, 2, 2] ) +scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) + +# Run NSCF +bands_hub = compute_bands(scfres, MonkhorstPack(2, 2, 2); ρ=scfres.ρ) + +εF = bands_hub.εF +εrange = (εF - austrip(width), εF + austrip(width)) +band_gap = bands_hub.eigenvalues[1][26] - bands_hub.eigenvalues[1][25] + +# With the electron localization introduced by the Hubbard term, the band gap has now opened, +# reflecting the experimental insulating behaviour of Nickel Oxide. +p = plot_dos(bands_hub; p, colors=[:blue, :blue], temperature=2e-3, εrange) +plot_pdos(bands_hub; p, temperature=2e-3, iatom=1, label="3D", colors=[:green, :purple], εrange) + diff --git a/ext/DFTKPlotsExt.jl b/ext/DFTKPlotsExt.jl index fad3ba9b5c..0899ee02f9 100644 --- a/ext/DFTKPlotsExt.jl +++ b/ext/DFTKPlotsExt.jl @@ -75,7 +75,8 @@ end function plot_dos(basis, eigenvalues; εF=nothing, unit=u"hartree", temperature=basis.model.temperature, - smearing=basis.model.smearing, + smearing=basis.model.smearing, + colors = [:blue, :red], p=nothing, εrange=default_band_εrange(eigenvalues; εF), n_points=1000, kwargs...) # TODO Should also split this up into one stage doing the DOS computation # 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", # Constant to convert from AU to the desired unit to_unit = ustrip(auconvert(unit, 1.0)) - p = Plots.plot(; kwargs...) + isnothing(p) && (p = Plots.plot(; kwargs...)) + p = Plots.plot(p; kwargs...) spinlabels = spin_components(basis.model) - colors = [:blue, :red] Dεs = compute_dos.(εs, Ref(basis), Ref(eigenvalues); smearing, temperature) for σ = 1:n_spin D = [Dσ[σ] for Dσ in Dεs] diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index ba1734ba7e..664f2b3476 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -99,10 +99,10 @@ end test_consistency_term(Xc([:lda_xc_teter93])) test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) - test_consistency_term(Xc([:mgga_x_scan]; use_nlcc=false)) + test_consistency_term(Xc([:mgga_x_tpss])) + test_consistency_term(Xc([:mgga_x_scan])) test_consistency_term(Xc([:mgga_c_scan]; use_nlcc=false), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_b00]; use_nlcc=false)) + test_consistency_term(Xc([:mgga_x_b00])) test_consistency_term(Xc([:mgga_c_b94]; use_nlcc=false), spin_polarization=:collinear) let @@ -113,6 +113,6 @@ end test_consistency_term(Magnetic(Apot); kgrid=[1, 1, 1], kshift=[0, 0, 0], lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) test_consistency_term(DFTK.Anyonic(2, 3.2); kgrid=[1, 1, 1], kshift=[0, 0, 0], - lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20, spin_polarization=:none) + lattice=[a 0 0; 0 a 0; 0 0 0], Ecut=20) end end From c9d8328338ef89766967ebb2efef09f9adb52722 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Fri, 3 Oct 2025 12:02:04 +0200 Subject: [PATCH 16/50] use_nlcc=false for all mgga tests --- test/hamiltonian_consistency.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 664f2b3476..9c551aefc5 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -99,10 +99,10 @@ end test_consistency_term(Xc([:lda_xc_teter93])) test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_tpss])) - test_consistency_term(Xc([:mgga_x_scan])) + test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) + test_consistency_term(Xc([:mgga_x_scan]; use_nlcc=false)) test_consistency_term(Xc([:mgga_c_scan]; use_nlcc=false), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_b00])) + test_consistency_term(Xc([:mgga_x_b00]; use_nlcc=false)) test_consistency_term(Xc([:mgga_c_b94]; use_nlcc=false), spin_polarization=:collinear) let From f90519777016fd3785d7b082f26687fac5a990e1 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Tue, 7 Oct 2025 11:23:07 +0200 Subject: [PATCH 17/50] Most comments have been addressed --- docs/make.jl | 156 +++++++++++++++--------------- examples/hubbard.jl | 26 +++-- ext/DFTKPlotsExt.jl | 6 +- src/common/spherical_harmonics.jl | 45 ++++----- src/postprocess/dos.jl | 2 +- src/terms/hubbard.jl | 58 +++++------ src/terms/operators.jl | 4 +- 7 files changed, 137 insertions(+), 160 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index af31330401..576ff0e04c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -14,88 +14,88 @@ # Structure of the docs. List files as *.jl or *.md here. All files # ending in *.jl will be processed to *.md with Literate. PAGES = [ -# "Home" => "index.md", -# "features.md", -# "Getting started" => [ -# "guide/installation.md", -# "guide/tutorial.jl", -# ], -# "Background" => [ -# # Theoretical background -# "guide/introductory_resources.md", -# "guide/periodic_problems.jl", -# "guide/discretisation.jl", -# "guide/atomic_chains.jl", -# "guide/density_functional_theory.md", -# "guide/self_consistent_field.jl", -# "school2022.md", -# ], + "Home" => "index.md", + "features.md", + "Getting started" => [ + "guide/installation.md", + "guide/tutorial.jl", + ], + "Background" => [ + # Theoretical background + "guide/introductory_resources.md", + "guide/periodic_problems.jl", + "guide/discretisation.jl", + "guide/atomic_chains.jl", + "guide/density_functional_theory.md", + "guide/self_consistent_field.jl", + "school2022.md", + ], "Basic DFT calculations" => [ # Ground-state DFT calculations, standard problems and modelling techniques # Some basic show cases; may feature integration of DFTK with other packages. -# "examples/metallic_systems.jl", -# "examples/collinear_magnetism.jl", -# "examples/convergence_study.jl", -# "examples/pseudopotentials.jl", -# "examples/supercells.jl", + "examples/metallic_systems.jl", + "examples/collinear_magnetism.jl", + "examples/convergence_study.jl", + "examples/pseudopotentials.jl", + "examples/supercells.jl", "examples/hubbard.jl", -# "examples/gaas_surface.jl", -# "examples/graphene.jl", -# "examples/geometry_optimization.jl", -# "examples/energy_cutoff_smearing.jl", + "examples/gaas_surface.jl", + "examples/graphene.jl", + "examples/geometry_optimization.jl", + "examples/energy_cutoff_smearing.jl", + ], + "Response and properties" => [ + "examples/polarizability.jl", + "examples/forwarddiff.jl", + "examples/phonons.jl", + ], + "Ecosystem integration" => [ + # This concerns the discussion of interfaces, IO and integration + # options we have + "ecosystem/atomsbase.jl", + "ecosystem/atomscalculators.jl", + "ecosystem/input_output.jl", + "ecosystem/atomistic_simulation_environment.md", + "ecosystem/wannier.jl", + ], + "Tips and tricks" => [ + # Resolving convergence issues, what solver to use, improving performance or + # reliability of calculations. + "tricks/achieving_convergence.md", + "tricks/parallelization.md", + "tricks/gpu.jl", + "tricks/scf_checkpoints.jl", + "tricks/compute_clusters.md", + ], + "Solvers" => [ + "examples/custom_solvers.jl", + "examples/scf_callbacks.jl", + "examples/compare_solvers.jl", + "examples/analysing_scf_convergence.jl", + ], + "Nonstandard models" => [ + "examples/gross_pitaevskii.jl", + "examples/gross_pitaevskii_2D.jl", + "examples/custom_potential.jl", + "examples/cohen_bergstresser.jl", + "examples/anyons.jl", + ], + "Error control" => [ + "examples/arbitrary_floattype.jl", + "examples/error_estimates_forces.jl", + ], + "Developer resources" => [ + "developer/setup.md", + "developer/testsystem.md", + "developer/conventions.md", + "developer/style_guide.md", + "developer/data_structures.md", + "developer/useful_formulas.md", + "developer/symmetries.md", + "developer/gpu_computations.md", ], -# "Response and properties" => [ -# "examples/polarizability.jl", -# "examples/forwarddiff.jl", -# "examples/phonons.jl", -# ], -# "Ecosystem integration" => [ -# # This concerns the discussion of interfaces, IO and integration -# # options we have -# "ecosystem/atomsbase.jl", -# "ecosystem/atomscalculators.jl", -# "ecosystem/input_output.jl", -# "ecosystem/atomistic_simulation_environment.md", -# "ecosystem/wannier.jl", -# ], -# "Tips and tricks" => [ -# # Resolving convergence issues, what solver to use, improving performance or -# # reliability of calculations. -# "tricks/achieving_convergence.md", -# "tricks/parallelization.md", -# "tricks/gpu.jl", -# "tricks/scf_checkpoints.jl", -# "tricks/compute_clusters.md", -# ], -# "Solvers" => [ -# "examples/custom_solvers.jl", -# "examples/scf_callbacks.jl", -# "examples/compare_solvers.jl", -# "examples/analysing_scf_convergence.jl", -# ], -# "Nonstandard models" => [ -# "examples/gross_pitaevskii.jl", -# "examples/gross_pitaevskii_2D.jl", -# "examples/custom_potential.jl", -# "examples/cohen_bergstresser.jl", -# "examples/anyons.jl", -# ], -# "Error control" => [ -# "examples/arbitrary_floattype.jl", -# "examples/error_estimates_forces.jl", -# ], -# "Developer resources" => [ -# "developer/setup.md", -# "developer/testsystem.md", -# "developer/conventions.md", -# "developer/style_guide.md", -# "developer/data_structures.md", -# "developer/useful_formulas.md", -# "developer/symmetries.md", -# "developer/gpu_computations.md", -# ], -# "api.md", -# "publications.md", + "api.md", + "publications.md", ] # Files from the /examples folder that need to be copied over to the docs @@ -105,7 +105,7 @@ EXAMPLE_ASSETS = [] # Specify e.g. as "examples/Fe_afm.pwi" # # Configuration and setup # -DEBUG = true # Set to true to disable some checks and cleanup +DEBUG = false # Set to true to disable some checks and cleanup import LibGit2 import Pkg diff --git a/examples/hubbard.jl b/examples/hubbard.jl index 903ce1ceb0..228904f139 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -18,23 +18,22 @@ pseudopotentials = PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf") Ni, O = ElementPsp(:Ni, pseudopotentials), ElementPsp(:O, pseudopotentials) atoms = [Ni, O, Ni, O] positions = [zeros(3), ones(3) / 4, ones(3) / 2, ones(3) * 3 / 4] -magnetic_moments = [2, 0, 2, 0] +magnetic_moments = [2, 0, -1, 0] # First, we run an SCF and NSCF without the Hubbard term model = model_DFT(lattice, atoms, positions; temperature=5e-3, functionals = PBE(), magnetic_moments=magnetic_moments) -basis = PlaneWaveBasis(model; Ecut = 15, kgrid = [2, 2, 2] ) +basis = PlaneWaveBasis(model; Ecut = 32, kgrid = [2, 2, 2] ) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) -bands = compute_bands(scfres, MonkhorstPack(2, 2, 2); ρ=scfres.ρ) +bands = compute_bands(scfres, MonkhorstPack(2, 2, 2); ρ=scfres.ρ); +band_gap = bands.eigenvalues[1][25] - bands.eigenvalues[1][24] +# Then we plot the DOS and the PDOS for the relevant 3D (pseudo)atomic projector εF = bands.εF width = 5.0u"eV" εrange = (εF - austrip(width), εF + austrip(width)) -band_gap = bands.eigenvalues[1][25] - bands.eigenvalues[1][24] - -# Then we plot the DOS and the PDOS for the relevant 3D (pseudo)atomic projector -p = plot_dos(bands; εrange, temperature=2e-3, colors=[:red, :red]) -p = plot_pdos(bands; p, temperature=2e-3, iatom=1, label="3D", colors=[:yellow, :orange], εrange) +p = plot_dos(bands; εrange, colors=[:red, :red]) +plot_pdos(bands; p, temperature=2e-3, iatom=1, label="3D", colors=[:yellow, :orange], εrange) # To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant U = 10u"eV" @@ -43,18 +42,17 @@ manifold = DFTK.OrbitalManifold(;species=:Ni, label="3D") # Run SCF model = model_DFT(lattice, atoms, positions; extra_terms=[DFTK.Hubbard(manifold, U)], functionals = PBE(), temperature=5e-3, magnetic_moments=magnetic_moments) -basis = PlaneWaveBasis(model; Ecut = 15, kgrid = [2, 2, 2] ) +basis = PlaneWaveBasis(model; Ecut = 32, kgrid = [2, 2, 2] ) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) # Run NSCF bands_hub = compute_bands(scfres, MonkhorstPack(2, 2, 2); ρ=scfres.ρ) - -εF = bands_hub.εF -εrange = (εF - austrip(width), εF + austrip(width)) band_gap = bands_hub.eigenvalues[1][26] - bands_hub.eigenvalues[1][25] # With the electron localization introduced by the Hubbard term, the band gap has now opened, # reflecting the experimental insulating behaviour of Nickel Oxide. -p = plot_dos(bands_hub; p, colors=[:blue, :blue], temperature=2e-3, εrange) -plot_pdos(bands_hub; p, temperature=2e-3, iatom=1, label="3D", colors=[:green, :purple], εrange) +εF = bands_hub.εF +εrange = (εF - austrip(width), εF + austrip(width)) +p = plot_dos(bands_hub; colors=[:blue, :blue], εrange) +plot_pdos(bands_hub; p, iatom=1, label="3D", colors=[:green, :purple], εrange) diff --git a/ext/DFTKPlotsExt.jl b/ext/DFTKPlotsExt.jl index 0899ee02f9..b2435367e7 100644 --- a/ext/DFTKPlotsExt.jl +++ b/ext/DFTKPlotsExt.jl @@ -76,7 +76,7 @@ end function plot_dos(basis, eigenvalues; εF=nothing, unit=u"hartree", temperature=basis.model.temperature, smearing=basis.model.smearing, - colors = [:blue, :red], p=nothing, + colors=[:blue, :red], p=nothing, εrange=default_band_εrange(eigenvalues; εF), n_points=1000, kwargs...) # TODO Should also split this up into one stage doing the DOS computation # and one stage doing the DOS plotting (like now for the bands.) @@ -88,7 +88,7 @@ function plot_dos(basis, eigenvalues; εF=nothing, unit=u"hartree", # Constant to convert from AU to the desired unit to_unit = ustrip(auconvert(unit, 1.0)) - isnothing(p) && (p = Plots.plot(; kwargs...)) + isnothing(p) && (p = Plots.plot()) p = Plots.plot(p; kwargs...) spinlabels = spin_components(basis.model) Dεs = compute_dos.(εs, Ref(basis), Ref(eigenvalues); smearing, temperature) @@ -159,7 +159,7 @@ function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom, label=nothi orb_name = isnothing(label) ? "all orbitals" : label # Plot pdos - isnothing(p) && (p = Plots.plot(; kwargs...)) + isnothing(p) && (p = Plots.plot()) p = Plots.plot(p; kwargs...) spinlabels = spin_components(basis.model) pdos = DFTK.sum_pdos(compute_pdos(εs, basis, ψ, eigenvalues; diff --git a/src/common/spherical_harmonics.jl b/src/common/spherical_harmonics.jl index c2359ba84a..51f47bd8d4 100644 --- a/src/common/spherical_harmonics.jl +++ b/src/common/spherical_harmonics.jl @@ -50,37 +50,30 @@ end """ This function returns the Wigner D matrix for real spherical harmonics, - for a given l and symmetry operation, solving a randomized linear system. - Such matrix gives the decomposition of a spherical harmonic after application - of a symmetry operation back into the basis of spherical harmonics. + for a given l and orthogonal matrix, solving a randomized linear system. + Such D matrix gives the decomposition of a spherical harmonic after application + of an orthogonal matrix back into the basis of spherical harmonics. - Yₗₘ₁(R̂r) = Σₘ₂ D(l,R̂)ₘ₁ₘ₂ * Yₗₘ₂(r) - - The lattice is needed to convert reduced symmetries to Cartesian space. + Yₗₘ₁(Wr) = Σₘ₂ D(l,R̂)ₘ₁ₘ₂ * Yₗₘ₂(r) """ function wigner_d_matrix(l::Integer, Wcart::AbstractMatrix{T}) where {T} - D = Matrix{T}(undef, 2*l+1, 2*l+1) - if l == 0 - return D .= 1 + if l == 0 # In this case no computation is needed + return hcat(one(T)) end rng = Xoshiro(1234) - neq = (2*l+2) # This value should work for p and d orbitals, but can be increased if needed - for m1 in -l:l - b = Vector{T}(undef, neq) - A = Matrix{T}(undef, neq, 2*l+1) - for n in 1:neq - r = rand(rng, T, 3) - r = r / norm(r) - r0 = Wcart * r - b[n] = DFTK.ylm_real(l, m1, r0) - for m2 in -l:l - A[n,m2+l+1] = DFTK.ylm_real(l, m2, r) - end + neq = (2*l+2) # We need at least 2*l+1 equations, we add one for numerical stability + B = Matrix{T}(undef, 2*l+1, neq) + A = Matrix{T}(undef, 2*l+1, neq) + for n in 1:neq + r = rand(rng, T, 3) + r = r / norm(r) + r0 = Wcart * r + for m in -l:l + B[m+l+1, n] = DFTK.ylm_real(l, m, r0) + A[m+l+1, n] = DFTK.ylm_real(l, m, r) end - κ = cond(A) - @assert κ < 100.0 "The Wigner matrix computation is badly conditioned. κ(A)=$(κ)" - D[m1+l+1,:] = A\b end - - return D + κ = cond(A) + @assert κ < 100.0 "The Wigner matrix computation is badly conditioned. κ(A)=$(κ)" + B / A end \ No newline at end of file diff --git a/src/postprocess/dos.jl b/src/postprocess/dos.jl index 540333017a..ff9ec78f30 100644 --- a/src/postprocess/dos.jl +++ b/src/postprocess/dos.jl @@ -166,7 +166,7 @@ function atomic_orbital_projectors(basis::PlaneWaveBasis{T}; labels = [] for (iatom, atom) in enumerate(basis.model.atoms) psp = atom.psp - count_n_pswfc(psp) + count_n_pswfc(psp) # We need this to check if we have any atomic orbital projector for l in 0:psp.lmax, n in 1:DFTK.count_n_pswfc_radial(psp, l) label = DFTK.pswfc_label(psp, n, l) if !isonmanifold((; iatom, atom.species, label)) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 580e5d3a3f..31e9183280 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -28,17 +28,8 @@ end function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, manifold::OrbitalManifold) where {T} - manifold_labels = [] + manifold_labels = filter(manifold, labels) manifold_projectors = Vector{Matrix{Complex{T}}}(undef, length(basis.kpoints)) - for (iproj, orb) in enumerate(labels) - if Symbol(manifold.species) == Symbol(orb.species) && - lowercase(manifold.label) == lowercase(orb.label) - # If the label matches the manifold, we add it to the labels - # This is useful for extracting specific orbitals from the basis - # e.g., (:Si, "3S") will match all 3S orbitals of Si atoms - push!(manifold_labels, (; orb.iatom, orb.species, orb.n, orb.l, orb.m, orb.label)) - end - end for (ik, projk) in enumerate(projectors) manifold_projectors[ik] = zeros(Complex{T}, size(projectors[ik], 1), length(manifold_labels)) for (iproj, orb) in enumerate(manifold_labels) @@ -59,8 +50,8 @@ end """ Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ -function symmetrize_nhub(nhubbard::Array{Matrix{Complex{T}}}, - lattice, symmetries, positions) where {T} +function symmetrize_nhubbard(nhubbard::Array{Matrix{Complex{T}}}, + model, symmetries, positions) where {T} # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) # WARNING: To implement +V this will need to be changed! @@ -72,24 +63,19 @@ function symmetrize_nhub(nhubbard::Array{Matrix{Complex{T}}}, # Initialize the nhubbard matrix ns = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) for σ in 1:nspins, iatom in 1:natoms, jatom in 1:natoms + ldim = size(nhubbard[σ, iatom, jatom], 1) # TODO: discuss this. the program doesn't work as inteded without the species field in OrbitalManifold ns[σ, iatom, jatom] = zeros(Complex{T}, - size(nhubbard[σ, iatom, jatom], 1), - size(nhubbard[σ, iatom, jatom], 2)) + ldim, + ldim) end for symmetry in symmetries - Wcart = lattice * symmetry.W * inv(lattice) + Wcart = model.lattice * symmetry.W * model.inv_lattice WigD = wigner_d_matrix(l, Wcart) for σ in 1:nspins, iatom in 1:natoms sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry) - for m1 in 1:size(ns[σ, iatom, iatom], 1), m2 in 1:size(ns[σ, iatom, iatom], 2) - # TODO: Here QE flips spin for time-reversal in collinear systems, should we? - for m0 in 1:size(nhubbard[σ,iatom,iatom],1), m00 in 1:size(nhubbard[σ,iatom,iatom],2) - ns[σ, iatom, iatom][m1, m2] += WigD[m0, m1] * - nhubbard[σ, sym_atom, sym_atom][m0, m00] * - WigD[m00, m2] - end - end + # TODO: Here QE flips spin for time-reversal in collinear systems, should we? + ns[σ, iatom, iatom] .+= WigD' * nhubbard[σ, sym_atom, sym_atom] * WigD end end ns .= ns / nsym @@ -131,14 +117,14 @@ function compute_nhubbard(manifold::OrbitalManifold, nprojs = length(labels) nspins = basis.model.n_spin_components n_matrix = zeros(Complex{T}, nspins, nprojs, nprojs) + @show manifold + @show size(projectors[1], 2) for σ in 1:nspins, ik = krange_spin(basis, σ) # We divide by filled_occ to deal with the physical two spin channels separately. - ψk, projk, nk = @views ψ[ik], projectors[ik], occupation[ik]/filled_occ - c = projk' * ψk # <ϕ|ψ> - # The matrix product is done over the bands. - # In QE, basis.kweights[ik]*nk[ibnd] would be wg(ik,ibnd) - n_matrix[σ, :, :] .+= basis.kweights[ik] * c * diagm(nk) * c' + c = projectors[ik]'ψ[ik] # <ϕ|ψ> + # Sums over the bands + n_matrix[σ, :, :] .+= basis.kweights[ik] * c * diagm(occupation[ik]/filled_occ) * c' end n_matrix = mpi_sum(n_matrix, basis.comm_kpts) @@ -154,7 +140,7 @@ function compute_nhubbard(manifold::OrbitalManifold, i = 1 while i <= nprojs il = labels[i].l - if !(manifold_atoms[iatom] == labels[i].iatom) + if manifold_atoms[iatom] != labels[i].iatom i += 2*il + 1 continue end @@ -162,8 +148,9 @@ function compute_nhubbard(manifold::OrbitalManifold, j = 1 while j <= nprojs jl = labels[j].l - (manifold_atoms[jatom] == labels[j].iatom) && - (nhubbard[σ, iatom, jatom] = n_matrix[σ, i:i+2*il, j:j+2*jl]) + if manifold_atoms[jatom] == labels[j].iatom + nhubbard[σ, iatom, jatom] = n_matrix[σ, i:i+2*il, j:j+2*jl] + end j += 2*jl + 1 end end @@ -174,7 +161,7 @@ function compute_nhubbard(manifold::OrbitalManifold, end end - nhubbard = symmetrize_nhub(nhubbard, basis.model.lattice, + nhubbard = symmetrize_nhubbard(nhubbard, basis.model, basis.symmetries, basis.model.positions[manifold_atoms]) return (; nhubbard, manifold_labels=labels, p_I) @@ -216,8 +203,8 @@ struct Hubbard end function (hubbard::Hubbard)(basis::AbstractBasis) iszero(hubbard.U) && return TermNoop() - projs, labs = atomic_orbital_projectors(basis) - labels, projectors = extract_manifold(basis, projs, labs, hubbard.manifold) + projs, labels = atomic_orbital_projectors(basis) + labels, projectors = extract_manifold(basis, projs, labels, hubbard.manifold) U = austrip(hubbard.U) TermHubbard(hubbard.manifold, U, projectors, labels) end @@ -231,14 +218,13 @@ end @timing "ene_ops: hubbard" function ene_ops(term::TermHubbard, basis::PlaneWaveBasis{T}, - ψ, occupation; nhubbard=nothing, ψ_hub=nothing, + ψ, occupation; nhubbard=nothing, labels=term.labels, kwargs...) where {T} if isnothing(ψ) if isnothing(nhubbard) return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end - ψ = ψ_hub proj = reshape_hubbard_proj(basis, term.P, term.labels, term.manifold) else Hubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, diff --git a/src/terms/operators.jl b/src/terms/operators.jl index a571422124..6b003e77bc 100644 --- a/src/terms/operators.jl +++ b/src/terms/operators.jl @@ -176,9 +176,9 @@ function apply!(Hψ, op::HubbardUOperator, ψ) n_ii = n_IJ[σ, iatom, iatom] iszero(n_ii) && continue for m1 = 1:size(n_ii, 1) - P_i_m1 = proj_I[iatom][:,m1] + P_i_m1 = @view proj_I[iatom][:,m1] for m2 = 1:size(n_ii, 2) - P_i_m2 = proj_I[iatom][:,m2] + P_i_m2 = @view proj_I[iatom][:,m2] δm = (m1 == m2) ? one(eltype(n_ii)) : zero(eltype(n_ii)) coefficient = 1/2 * op.Us * (δm - 2*n_ii[m1, m2]) projection = P_i_m2' * ψ.fourier From 7ea2342c9db1f1ed8a5c73c5e1cbd70522f5f377 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Tue, 7 Oct 2025 14:03:58 +0200 Subject: [PATCH 18/50] Several issues solved --- ext/DFTKPlotsExt.jl | 2 +- src/terms/hubbard.jl | 33 +++++++++++++-------------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/ext/DFTKPlotsExt.jl b/ext/DFTKPlotsExt.jl index b2435367e7..76128a9788 100644 --- a/ext/DFTKPlotsExt.jl +++ b/ext/DFTKPlotsExt.jl @@ -75,7 +75,7 @@ end function plot_dos(basis, eigenvalues; εF=nothing, unit=u"hartree", temperature=basis.model.temperature, - smearing=basis.model.smearing, + smearing=basis.model.smearing, colors=[:blue, :red], p=nothing, εrange=default_band_εrange(eigenvalues; εF), n_points=1000, kwargs...) # TODO Should also split this up into one stage doing the DOS computation diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 31e9183280..fa84cc28aa 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -29,18 +29,16 @@ end function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, manifold::OrbitalManifold) where {T} manifold_labels = filter(manifold, labels) + isempty(manifold_labels) && @warn "Projector for $(manifold) not found." manifold_projectors = Vector{Matrix{Complex{T}}}(undef, length(basis.kpoints)) for (ik, projk) in enumerate(projectors) manifold_projectors[ik] = zeros(Complex{T}, size(projectors[ik], 1), length(manifold_labels)) - for (iproj, orb) in enumerate(manifold_labels) + iproj = 0 + for (proj_index, orb) in enumerate(labels) # Find the index of the projector that matches the manifold label - proj_index = findfirst(p -> p.iatom == orb.iatom && p.species == orb.species && - p.n == orb.n && p.l == orb.l && p.m == orb.m, labels) - if proj_index !== nothing + if manifold(orb) + iproj += 1 manifold_projectors[ik][:, iproj] = projk[:, proj_index] - else - @warn "Projector for $(orb.species) with n=$(orb.n), l=$(orb.l), m=$(orb.m) - not found in projectors." end end end @@ -59,16 +57,13 @@ function symmetrize_nhubbard(nhubbard::Array{Matrix{Complex{T}}}, natoms = size(nhubbard, 2) nsym = length(symmetries) l = Int64((size(nhubbard[1, 1, 1], 1)-1)/2) + ldim = 2*l+1 # Initialize the nhubbard matrix ns = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) for σ in 1:nspins, iatom in 1:natoms, jatom in 1:natoms - ldim = size(nhubbard[σ, iatom, jatom], 1) # TODO: discuss this. the program doesn't work as inteded without the species field in OrbitalManifold - ns[σ, iatom, jatom] = zeros(Complex{T}, - ldim, - ldim) + ns[σ, iatom, jatom] = zeros(Complex{T}, ldim, ldim) end - for symmetry in symmetries Wcart = model.lattice * symmetry.W * model.inv_lattice WigD = wigner_d_matrix(l, Wcart) @@ -117,9 +112,6 @@ function compute_nhubbard(manifold::OrbitalManifold, nprojs = length(labels) nspins = basis.model.n_spin_components n_matrix = zeros(Complex{T}, nspins, nprojs, nprojs) - @show manifold - @show size(projectors[1], 2) - for σ in 1:nspins, ik = krange_spin(basis, σ) # We divide by filled_occ to deal with the physical two spin channels separately. c = projectors[ik]'ψ[ik] # <ϕ|ψ> @@ -131,7 +123,7 @@ function compute_nhubbard(manifold::OrbitalManifold, # Now I want to reshape it to match the notation used in the papers. # Reshape into n[I, J, σ][m1, m2] where I, J indicate the atom in the Hubbard manifold, # σ is the spin, m1 and m2 are magnetic quantum numbers (n, l are fixed) - manifold_atoms = findall(at -> at.species == Symbol(manifold.species), basis.model.atoms) + manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) natoms = length(manifold_atoms) # Number of atoms of the species in the manifold nhubbard = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for ik in 1:length(basis.kpoints)] @@ -169,7 +161,7 @@ end function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} - manifold_atoms = findall(at -> at.species == Symbol(manifold.species), basis.model.atoms) + manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) natoms = length(manifold_atoms) nprojs = length(labels) p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for i in 1:length(projectors)] @@ -202,6 +194,9 @@ struct Hubbard U end function (hubbard::Hubbard)(basis::AbstractBasis) + if isnothing(hubbard.manifold.label) || isnothing(hubbard.manifold.species) + error("TermHubbard needs both a species and a label inside OrbitalManifold") + end iszero(hubbard.U) && return TermNoop() projs, labels = atomic_orbital_projectors(basis) labels, projectors = extract_manifold(basis, projs, labels, hubbard.manifold) @@ -235,12 +230,10 @@ end ops = [HubbardUOperator(basis, kpt, term.U, nhubbard, proj[ik]) for (ik,kpt) in enumerate(basis.kpoints)] - filled_occ = filled_occupation(basis.model) - types = findall(at -> at.species == Symbol(term.manifold.species), basis.model.atoms) + types = findall(at -> at.species==term.manifold.species, basis.model.atoms) natoms = length(types) nspins = basis.model.n_spin_components - E = zero(T) for σ in 1:nspins, iatom in 1:natoms E += filled_occ * 1/2 * term.U * From 42b5f852da280fad05eb3802e69f9d5d7c0abc0e Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 9 Oct 2025 16:04:13 +0200 Subject: [PATCH 19/50] Semplification in compute_nhubbard --- src/terms/hubbard.jl | 79 +++++++++++---------------------- src/terms/operators.jl | 14 +++--- test/hamiltonian_consistency.jl | 2 +- test/testcases.jl | 28 ------------ 4 files changed, 33 insertions(+), 90 deletions(-) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index fa84cc28aa..310e81cc2c 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -69,7 +69,6 @@ function symmetrize_nhubbard(nhubbard::Array{Matrix{Complex{T}}}, WigD = wigner_d_matrix(l, Wcart) for σ in 1:nspins, iatom in 1:natoms sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry) - # TODO: Here QE flips spin for time-reversal in collinear systems, should we? ns[σ, iatom, iatom] .+= WigD' * nhubbard[σ, sym_atom, sym_atom] * WigD end end @@ -104,59 +103,32 @@ Overviw of outputs: also against those outside of the manifold. """ function compute_nhubbard(manifold::OrbitalManifold, - basis::PlaneWaveBasis{T}, - ψ, occupation; - projectors, labels, - positions = basis.model.positions) where {T} + basis::PlaneWaveBasis{T}, + ψ, occupation; + projectors, labels, + positions = basis.model.positions) where {T} filled_occ = filled_occupation(basis.model) - nprojs = length(labels) nspins = basis.model.n_spin_components - n_matrix = zeros(Complex{T}, nspins, nprojs, nprojs) - for σ in 1:nspins, ik = krange_spin(basis, σ) - # We divide by filled_occ to deal with the physical two spin channels separately. - c = projectors[ik]'ψ[ik] # <ϕ|ψ> - # Sums over the bands - n_matrix[σ, :, :] .+= basis.kweights[ik] * c * diagm(occupation[ik]/filled_occ) * c' - end - n_matrix = mpi_sum(n_matrix, basis.comm_kpts) - # Now I want to reshape it to match the notation used in the papers. - # Reshape into n[I, J, σ][m1, m2] where I, J indicate the atom in the Hubbard manifold, - # σ is the spin, m1 and m2 are magnetic quantum numbers (n, l are fixed) manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) natoms = length(manifold_atoms) # Number of atoms of the species in the manifold + l = labels[1].l nhubbard = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) - p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for ik in 1:length(basis.kpoints)] - # Very low-level, but works - for σ in 1:nspins, iatom in eachindex(manifold_atoms) - i = 1 - while i <= nprojs - il = labels[i].l - if manifold_atoms[iatom] != labels[i].iatom - i += 2*il + 1 - continue - end - for jatom in eachindex(manifold_atoms) - j = 1 - while j <= nprojs - jl = labels[j].l - if manifold_atoms[jatom] == labels[j].iatom - nhubbard[σ, iatom, jatom] = n_matrix[σ, i:i+2*il, j:j+2*jl] - end - j += 2*jl + 1 - end - end - for (ik, projk) in enumerate(projectors) - p_I[ik][iatom] = projk[:, i:i+2*il] - end - i += 2*il + 1 + for σ in 1:nspins, iatom in 1:natoms, jatom in 1:natoms + nhubbard[σ, iatom, jatom] = zeros(Complex{T}, 2*l+1, 2*l+1) + for ik = krange_spin(basis, σ) + # We divide by filled_occ to deal with the physical two spin channels separately. + j_projection = ψ[ik]' * projectors[ik][jatom] # <ψ|ϕJ> + i_projection = projectors[ik][iatom]' * ψ[ik] # <ϕI|ψ> + # Sums over the bands + nhubbard[σ, iatom, jatom] .+= basis.kweights[ik] * i_projection * + diagm(occupation[ik]/filled_occ) * j_projection end end - nhubbard = symmetrize_nhubbard(nhubbard, basis.model, basis.symmetries, basis.model.positions[manifold_atoms]) - return (; nhubbard, manifold_labels=labels, p_I) + return (; nhubbard, manifold_labels=labels) end function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, @@ -164,19 +136,18 @@ function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) natoms = length(manifold_atoms) nprojs = length(labels) + l = labels[1].l + @assert all(label -> label.l==l, labels) + @assert length(labels) == natoms * (2*l+1) p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for i in 1:length(projectors)] - for iatom in eachindex(manifold_atoms) - i = 1 - while i <= nprojs - il = labels[i].l - if !(manifold_atoms[iatom] == labels[i].iatom) - i += 2*il + 1 + for (iatom, idx) in enumerate(manifold_atoms) + for i in 1:2*l+1:nprojs + if iatom != labels[i].iatom continue end for (ik, projk) in enumerate(projectors) - p_I[ik][iatom] = projk[:, i:i+2*il] + p_I[ik][idx] = projk[:, i:i+2*l] end - i += 2*il + 1 end end @@ -197,9 +168,9 @@ function (hubbard::Hubbard)(basis::AbstractBasis) if isnothing(hubbard.manifold.label) || isnothing(hubbard.manifold.species) error("TermHubbard needs both a species and a label inside OrbitalManifold") end - iszero(hubbard.U) && return TermNoop() projs, labels = atomic_orbital_projectors(basis) labels, projectors = extract_manifold(basis, projs, labels, hubbard.manifold) + projectors = reshape_hubbard_proj(basis, projectors, labels, hubbard.manifold) U = austrip(hubbard.U) TermHubbard(hubbard.manifold, U, projectors, labels) end @@ -220,12 +191,12 @@ end if isnothing(nhubbard) return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end - proj = reshape_hubbard_proj(basis, term.P, term.labels, term.manifold) + proj = term.P else Hubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, labels) nhubbard = Hubbard.nhubbard - proj = Hubbard.p_I + proj = term.P end ops = [HubbardUOperator(basis, kpt, term.U, nhubbard, proj[ik]) diff --git a/src/terms/operators.jl b/src/terms/operators.jl index 6b003e77bc..fc6b94d3e9 100644 --- a/src/terms/operators.jl +++ b/src/terms/operators.jl @@ -171,18 +171,18 @@ function apply!(Hψ, op::HubbardUOperator, ψ) σ = op.kpoint.spin n_IJ = op.n_IJs proj_I = op.proj_Is - natoms = size(op.n_IJs, 2) + natoms = size(n_IJ, 2) for iatom in 1:natoms n_ii = n_IJ[σ, iatom, iatom] iszero(n_ii) && continue - for m1 = 1:size(n_ii, 1) - P_i_m1 = @view proj_I[iatom][:,m1] - for m2 = 1:size(n_ii, 2) - P_i_m2 = @view proj_I[iatom][:,m2] + for m2 = 1:size(n_ii, 2) + P_i_m2 = @view proj_I[iatom][:,m2] + projection = P_i_m2' * ψ.fourier + for m1 = 1:size(n_ii, 1) + P_i_m1 = @view proj_I[iatom][:,m1] δm = (m1 == m2) ? one(eltype(n_ii)) : zero(eltype(n_ii)) coefficient = 1/2 * op.Us * (δm - 2*n_ii[m1, m2]) - projection = P_i_m2' * ψ.fourier - Hψ.fourier .+= P_i_m1 * coefficient * projection + mul!(Hψ.fourier, P_i_m1, projection, coefficient, 1) end end end diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 9c551aefc5..ea4d6c5cb7 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -93,7 +93,7 @@ end test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) test_consistency_term(Hartree()) - test_consistency_term(Hubbard(DFTK.OrbitalManifold(;species=:Si, label="3P"), 0.01), ) + test_consistency_term(Hubbard(DFTK.OrbitalManifold(; species=:Si, label="3P"), 0.01)) test_consistency_term(Ewald()) test_consistency_term(PspCorrection()) test_consistency_term(Xc([:lda_xc_teter93])) diff --git a/test/testcases.jl b/test/testcases.jl index 840bb0f6e1..c178c290ac 100644 --- a/test/testcases.jl +++ b/test/testcases.jl @@ -10,7 +10,6 @@ gth_lda_semi = PseudoFamily("cp2k.nc.sr.lda.v0_1.semicore.gth") pd_lda_family = PseudoFamily("dojo.nc.sr.lda.v0_4_1.standard.upf") silicon = (; - symbol = :Si, lattice = [0.0 5.131570667152971 5.131570667152971; 5.131570667152971 0.0 5.131570667152971; 5.131570667152971 5.131570667152971 0.0], @@ -32,7 +31,6 @@ silicon = merge(silicon, (; atoms=fill(ElementPsp(silicon.atnum, load_psp(silicon.psp_gth)), 2))) magnesium = (; - symbol = :Mg, lattice = [-3.0179389205999998 -3.0179389205999998 0.0000000000000000; -5.2272235447000002 5.2272235447000002 0.0000000000000000; 0.0000000000000000 0.0000000000000000 -9.7736219469000005], @@ -57,7 +55,6 @@ magnesium = merge(magnesium, aluminium = (; - symbol = :Al, lattice = Matrix(Diagonal([4 * 7.6324708938577865, 7.6324708938577865, 7.6324708938577865])), atnum = 13, @@ -74,7 +71,6 @@ aluminium = merge(aluminium, aluminium_primitive = (; - symbol = :Al, lattice = [5.39697192863632 2.69848596431816 2.69848596431816; 0.00000000000000 4.67391479368660 1.55797159787754; 0.00000000000000 0.00000000000000 4.40660912710674], @@ -93,7 +89,6 @@ aluminium_primitive = merge(aluminium_primitive, platinum_hcp = (; - symbol = :Pt, lattice = [10.00000000000000 0.00000000000000 0.00000000000000; 5.00000000000000 8.66025403784439 0.00000000000000; 0.00000000000000 0.00000000000000 16.3300000000000], @@ -111,7 +106,6 @@ platinum_hcp = merge(platinum_hcp, load_psp(platinum_hcp.psp_gth)), 2))) iron_bcc = (; - symbol = :Fe, lattice = 2.71176 .* [[-1 1 1]; [1 -1 1]; [1 1 -1]], atnum = 26, mass = 55.8452u"u", @@ -139,28 +133,6 @@ o2molecule = merge(o2molecule, (; atoms=fill(ElementPsp(o2molecule.atnum, load_psp(o2molecule.psp_gth)), 2))) -nickel = (; - symbol = :Ni, - atnum = 28, - mass = 58.6934u"u", - n_electrons = 10, - psp_gth = gth_lda_semi[:Ni], - psp_upf = pd_lda_family[:Ni], - temperature = 0.02, - is_metal = true, -) - -oxygen = (; - symbol = :O, - atnum = 8, - mass = 15.999u"u", - n_electrons = 6, - psp_gth = gth_lda_semi[:O], - psp_upf = pd_lda_family[:O], - temperature = 0.02, - is_metal = true, -) - all_testcases = (; silicon, magnesium, aluminium, aluminium_primitive, platinum_hcp, iron_bcc, o2molecule) end From 5a95c9d1b67ac3fc545f9433e0db99d2cbb46937 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 9 Oct 2025 16:21:12 +0200 Subject: [PATCH 20/50] Simplification to the apply!(HubbardUOperator) --- docs/make.jl | 2 +- src/terms/hubbard.jl | 34 ++++++++++++++-------------- src/terms/operators.jl | 30 +++++++++--------------- src/workarounds/forwarddiff_rules.jl | 2 +- test/hamiltonian_consistency.jl | 2 +- test/hubbard.jl | 20 ++++++++-------- 6 files changed, 40 insertions(+), 50 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 576ff0e04c..34be906147 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -38,11 +38,11 @@ PAGES = [ "examples/convergence_study.jl", "examples/pseudopotentials.jl", "examples/supercells.jl", - "examples/hubbard.jl", "examples/gaas_surface.jl", "examples/graphene.jl", "examples/geometry_optimization.jl", "examples/energy_cutoff_smearing.jl", + "examples/hubbard.jl", ], "Response and properties" => [ "examples/polarizability.jl", diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 310e81cc2c..34c0da4757 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -108,24 +108,25 @@ function compute_nhubbard(manifold::OrbitalManifold, projectors, labels, positions = basis.model.positions) where {T} filled_occ = filled_occupation(basis.model) + nprojs = length(labels) nspins = basis.model.n_spin_components manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) natoms = length(manifold_atoms) # Number of atoms of the species in the manifold l = labels[1].l nhubbard = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) - for σ in 1:nspins, iatom in 1:natoms, jatom in 1:natoms - nhubbard[σ, iatom, jatom] = zeros(Complex{T}, 2*l+1, 2*l+1) - for ik = krange_spin(basis, σ) + for σ in 1:nspins, (idx, iatom) in enumerate(manifold_atoms), (jdx, jatom) in enumerate(manifold_atoms) + nhubbard[σ, idx, jdx] = zeros(Complex{T}, 2*l+1, 2*l+1) + for ik = krange_spin(basis, σ) # We divide by filled_occ to deal with the physical two spin channels separately. - j_projection = ψ[ik]' * projectors[ik][jatom] # <ψ|ϕJ> - i_projection = projectors[ik][iatom]' * ψ[ik] # <ϕI|ψ> + j_projection = ψ[ik]' * projectors[ik][jdx] # <ψ|ϕJ> + i_projection = projectors[ik][idx]' * ψ[ik] # <ϕI|ψ> # Sums over the bands - nhubbard[σ, iatom, jatom] .+= basis.kweights[ik] * i_projection * + nhubbard[σ, idx, jdx] .+= basis.kweights[ik] * i_projection * diagm(occupation[ik]/filled_occ) * j_projection end end - nhubbard = symmetrize_nhubbard(nhubbard, basis.model, + nhubbard = symmetrize_nhubbard(nhubbard, basis.model, basis.symmetries, basis.model.positions[manifold_atoms]) return (; nhubbard, manifold_labels=labels) @@ -140,13 +141,14 @@ function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, @assert all(label -> label.l==l, labels) @assert length(labels) == natoms * (2*l+1) p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for i in 1:length(projectors)] - for (iatom, idx) in enumerate(manifold_atoms) + for (idx, iatom) in enumerate(manifold_atoms) for i in 1:2*l+1:nprojs if iatom != labels[i].iatom continue - end - for (ik, projk) in enumerate(projectors) - p_I[ik][idx] = projk[:, i:i+2*l] + else + for (ik, projk) in enumerate(projectors) + p_I[ik][idx] = projk[:, i:i+2*l] + end end end end @@ -191,13 +193,11 @@ end if isnothing(nhubbard) return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end - proj = term.P else - Hubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, - labels) - nhubbard = Hubbard.nhubbard - proj = term.P + nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, + labels).nhubbard end + proj = term.P ops = [HubbardUOperator(basis, kpt, term.U, nhubbard, proj[ik]) for (ik,kpt) in enumerate(basis.kpoints)] @@ -210,5 +210,5 @@ end E += filled_occ * 1/2 * term.U * real(tr(nhubbard[σ, iatom, iatom] * (I - nhubbard[σ, iatom, iatom]))) end - return (; E, ops, nhubbard) + return (; E, ops) end diff --git a/src/terms/operators.jl b/src/terms/operators.jl index fc6b94d3e9..d7e499589f 100644 --- a/src/terms/operators.jl +++ b/src/terms/operators.jl @@ -161,30 +161,22 @@ where ``Pᵢₘ₁`` is the projector for atom i and orbital m₁. (m₁ is usually just the magnetic quantum number, since l is usually fixed) """ struct HubbardUOperator{T <: Real} <: RealFourierOperator - basis :: PlaneWaveBasis{T} - kpoint :: Kpoint{T} - Us # Hubbard U parameter - n_IJs :: Array{Matrix{Complex{T}}} - proj_Is :: Vector{Matrix{Complex{T}}} # It is the projector for the given kpoint only + basis :: PlaneWaveBasis{T} + kpoint :: Kpoint{T} + U # Hubbard U parameter + nhubbard :: Array{Matrix{Complex{T}}} + proj :: Vector{Matrix{Complex{T}}} # It is the projector for the given kpoint only end function apply!(Hψ, op::HubbardUOperator, ψ) σ = op.kpoint.spin - n_IJ = op.n_IJs - proj_I = op.proj_Is - natoms = size(n_IJ, 2) + nhubbard = op.nhubbard + projectors = op.proj + natoms = size(nhubbard, 2) for iatom in 1:natoms - n_ii = n_IJ[σ, iatom, iatom] + n_ii = nhubbard[σ, iatom, iatom] iszero(n_ii) && continue - for m2 = 1:size(n_ii, 2) - P_i_m2 = @view proj_I[iatom][:,m2] - projection = P_i_m2' * ψ.fourier - for m1 = 1:size(n_ii, 1) - P_i_m1 = @view proj_I[iatom][:,m1] - δm = (m1 == m2) ? one(eltype(n_ii)) : zero(eltype(n_ii)) - coefficient = 1/2 * op.Us * (δm - 2*n_ii[m1, m2]) - mul!(Hψ.fourier, P_i_m1, projection, coefficient, 1) - end - end + coefficient = 1/2 * op.U * (I - 2*n_ii) + mul!(Hψ.fourier, projectors[iatom], coefficient * (projectors[iatom]' * ψ.fourier), 1, 1) end end diff --git a/src/workarounds/forwarddiff_rules.jl b/src/workarounds/forwarddiff_rules.jl index 41987d2d3c..da4c67cc31 100644 --- a/src/workarounds/forwarddiff_rules.jl +++ b/src/workarounds/forwarddiff_rules.jl @@ -269,7 +269,7 @@ function self_consistent_field(basis_dual::PlaneWaveBasis{T}; # This has to be changed whenever the scfres structure changes (; ham, basis=basis_dual, energies, ρ, eigenvalues, occupation, εF, ψ, - scfres.τ, # TODO make τ also differentiable for meta-GGA DFPT + scfres.τ, # TODO make τ and nhubbard also differentiable for meta-GGA DFPT scfres.nhubbard, # non-differentiable metadata: response=getfield.(δresults, :info_gmres), diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index ea4d6c5cb7..2eac28f8de 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -99,7 +99,7 @@ end test_consistency_term(Xc([:lda_xc_teter93])) test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) + test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) # TODO: fix consistency for meta-GGA with NLCC test_consistency_term(Xc([:mgga_x_scan]; use_nlcc=false)) test_consistency_term(Xc([:mgga_c_scan]; use_nlcc=false), spin_polarization=:collinear) test_consistency_term(Xc([:mgga_x_b00]; use_nlcc=false)) diff --git a/test/hubbard.jl b/test/hubbard.jl index 4c32b6432e..ebf23fa860 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -6,31 +6,31 @@ # Identity Id = Float64[1 0 0; 0 1 0; 0 0 1] D = DFTK.wigner_d_matrix(1, Id) - @test norm(D - I) < 1e-8 + @test D ≈ I D = DFTK.wigner_d_matrix(2, Id) - @test norm(D - I) < 1e-8 + @test D ≈ I # This reverts all p orbitals, sends all d orbitals in themselves Inv = -Id D = DFTK.wigner_d_matrix(1, Inv) - @test norm(D + I) < 1e-8 + @test D ≈ -I D = DFTK.wigner_d_matrix(2, Inv) - @test norm(D - I) < 1e-8 + @test D ≈ I # This keeps pz, dz2, dx2-y2 and dxy unchanged, changes sign to all others A3 = Float64[1 0 0; 0 -1 0; 0 0 -1] D3p = Float64[-1 0 0; 0 -1 0; 0 0 1] D3d = Float64[-1 0 0 0 0; 0 1 0 0 0; 0 0 1 0 0; 0 0 0 -1 0; 0 0 0 0 1] D = DFTK.wigner_d_matrix(1, A3) - @test norm(D - D3p) < 1e-8 + @test D ≈ D3p D = DFTK.wigner_d_matrix(2, A3) - @test norm(D - D3d) < 1e-8 + @test D ≈ D3d # This sends: px <-> py, dxz <-> dyz, dx2-y2 -> -(dx2-y2) and keeps the other fixed A3 = Float64[0 1 0; 1 0 0; 0 0 1] D3p = Float64[0 0 1; 0 1 0; 1 0 0] D3d = Float64[1 0 0 0 0; 0 0 0 1 0; 0 0 1 0 0; 0 1 0 0 0; 0 0 0 0 -1] D = DFTK.wigner_d_matrix(1, A3) - @test norm(D - D3p) < 1e-8 + @test D ≈ D3p D = DFTK.wigner_d_matrix(2, A3) - @test norm(D - D3d) < 1e-8 + @test D ≈ D3d end @testitem "Test Hubbard U term in Nickel Oxide" setup=[TestCases] begin @@ -79,9 +79,7 @@ end # Test symmetry consistency n_hub = scfres.nhubbard - basis_nosym = DFTK.unfold_bz(basis) - ρ0 = guess_density(basis_nosym, magnetic_moments) - scfres_nosym = self_consistent_field(basis_nosym; tol=1e-10, ρ=ρ0) + scfres_nosym = DFTK.unfold_bz(scfres) @test norm(n_hub .- scfres_nosym.nhubbard) < 1e-8 end From 914bbe69a2a71f1c5467fef533c896cf2eec8a2e Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 9 Oct 2025 23:50:53 +0200 Subject: [PATCH 21/50] Simplification to the apply!(HubbardUOperator) --- src/terms/hubbard.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 34c0da4757..3d757f5734 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -194,7 +194,7 @@ end return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end else - nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, + nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, labels).nhubbard end proj = term.P From b31874d235c9ee18c5ef47f7e0976390d50f69a5 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Fri, 10 Oct 2025 00:15:06 +0200 Subject: [PATCH 22/50] New comments addressed --- examples/hubbard.jl | 14 +++---- src/common/spherical_harmonics.jl | 4 +- test/hubbard.jl | 65 +++++++++++++++++-------------- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/examples/hubbard.jl b/examples/hubbard.jl index 228904f139..f18ba0432e 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -3,7 +3,6 @@ # with and without the Hubbard term correction. using DFTK -using Printf using PseudoPotentialData using Unitful using UnitfulAtomic @@ -15,17 +14,18 @@ lattice = a * [[ 1.0 0.5 0.5]; [ 0.5 1.0 0.5]; [ 0.5 0.5 1.0]] pseudopotentials = PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf") -Ni, O = ElementPsp(:Ni, pseudopotentials), ElementPsp(:O, pseudopotentials) +Ni = ElementPsp(:Ni, pseudopotentials) +O = ElementPsp(:O, pseudopotentials) atoms = [Ni, O, Ni, O] positions = [zeros(3), ones(3) / 4, ones(3) / 2, ones(3) * 3 / 4] magnetic_moments = [2, 0, -1, 0] -# First, we run an SCF and NSCF without the Hubbard term +# First, we run an SCF and band computation without the Hubbard term model = model_DFT(lattice, atoms, positions; temperature=5e-3, - functionals = PBE(), magnetic_moments=magnetic_moments) -basis = PlaneWaveBasis(model; Ecut = 32, kgrid = [2, 2, 2] ) + functionals=PBE(), magnetic_moments=magnetic_moments) +basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2] ) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) -bands = compute_bands(scfres, MonkhorstPack(2, 2, 2); ρ=scfres.ρ); +bands = compute_bands(scfres, MonkhorstPack(2, 2, 2)); band_gap = bands.eigenvalues[1][25] - bands.eigenvalues[1][24] # Then we plot the DOS and the PDOS for the relevant 3D (pseudo)atomic projector @@ -45,7 +45,7 @@ model = model_DFT(lattice, atoms, positions; extra_terms=[DFTK.Hubbard(manifold, basis = PlaneWaveBasis(model; Ecut = 32, kgrid = [2, 2, 2] ) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) -# Run NSCF +# Run band computation bands_hub = compute_bands(scfres, MonkhorstPack(2, 2, 2); ρ=scfres.ρ) band_gap = bands_hub.eigenvalues[1][26] - bands_hub.eigenvalues[1][25] diff --git a/src/common/spherical_harmonics.jl b/src/common/spherical_harmonics.jl index 51f47bd8d4..82291d5550 100644 --- a/src/common/spherical_harmonics.jl +++ b/src/common/spherical_harmonics.jl @@ -58,10 +58,10 @@ end """ function wigner_d_matrix(l::Integer, Wcart::AbstractMatrix{T}) where {T} if l == 0 # In this case no computation is needed - return hcat(one(T)) + return [one(T);;] end rng = Xoshiro(1234) - neq = (2*l+2) # We need at least 2*l+1 equations, we add one for numerical stability + neq = 2*l+2 # We need at least 2*l+1 equations, we add one for numerical stability B = Matrix{T}(undef, 2*l+1, neq) A = Matrix{T}(undef, 2*l+1, neq) for n in 1:neq diff --git a/test/hubbard.jl b/test/hubbard.jl index ebf23fa860..6822ba8440 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -3,34 +3,42 @@ using PseudoPotentialData using LinearAlgebra - # Identity - Id = Float64[1 0 0; 0 1 0; 0 0 1] - D = DFTK.wigner_d_matrix(1, Id) - @test D ≈ I - D = DFTK.wigner_d_matrix(2, Id) - @test D ≈ I - # This reverts all p orbitals, sends all d orbitals in themselves - Inv = -Id - D = DFTK.wigner_d_matrix(1, Inv) - @test D ≈ -I - D = DFTK.wigner_d_matrix(2, Inv) - @test D ≈ I - # This keeps pz, dz2, dx2-y2 and dxy unchanged, changes sign to all others - A3 = Float64[1 0 0; 0 -1 0; 0 0 -1] - D3p = Float64[-1 0 0; 0 -1 0; 0 0 1] - D3d = Float64[-1 0 0 0 0; 0 1 0 0 0; 0 0 1 0 0; 0 0 0 -1 0; 0 0 0 0 1] - D = DFTK.wigner_d_matrix(1, A3) - @test D ≈ D3p - D = DFTK.wigner_d_matrix(2, A3) - @test D ≈ D3d - # This sends: px <-> py, dxz <-> dyz, dx2-y2 -> -(dx2-y2) and keeps the other fixed - A3 = Float64[0 1 0; 1 0 0; 0 0 1] - D3p = Float64[0 0 1; 0 1 0; 1 0 0] - D3d = Float64[1 0 0 0 0; 0 0 0 1 0; 0 0 1 0 0; 0 1 0 0 0; 0 0 0 0 -1] - D = DFTK.wigner_d_matrix(1, A3) - @test D ≈ D3p - D = DFTK.wigner_d_matrix(2, A3) - @test D ≈ D3d + @testset "Wigner Identity" begin + # Identity + Id = [1.0 0 0; 0 1 0; 0 0 1] + D = DFTK.wigner_d_matrix(1, Id) + @test D ≈ I + D = DFTK.wigner_d_matrix(2, Id) + @test D ≈ I + end + @testset "Wigner Inversion" begin + # This reverts all p orbitals, sends all d orbitals in themselves + Inv = -[1.0 0 0; 0 1 0; 0 0 1] + D = DFTK.wigner_d_matrix(1, Inv) + @test D ≈ -I + D = DFTK.wigner_d_matrix(2, Inv) + @test D ≈ I + end + @testset "Wigner invert x and y" begin + # This keeps pz, dz2, dx2-y2 and dxy unchanged, changes sign to all others + A3 = [1.0 0 0; 0 -1 0; 0 0 -1] + D3p = [-1.0 0 0; 0 -1 0; 0 0 1] + D3d = [-1.0 0 0 0 0; 0 1 0 0 0; 0 0 1 0 0; 0 0 0 -1 0; 0 0 0 0 1] + D = DFTK.wigner_d_matrix(1, A3) + @test D ≈ D3p + D = DFTK.wigner_d_matrix(2, A3) + @test D ≈ D3d + end + @testset "Wigner swap x and y" begin + # This sends: px <-> py, dxz <-> dyz, dx2-y2 -> -(dx2-y2) and keeps the other fixed + A3 = [0.0 1 0; 1 0 0; 0 0 1] + D3p = [0.0 0 1; 0 1 0; 1 0 0] + D3d = [1.0 0 0 0 0; 0 0 0 1 0; 0 0 1 0 0; 0 1 0 0 0; 0 0 0 0 -1] + D = DFTK.wigner_d_matrix(1, A3) + @test D ≈ D3p + D = DFTK.wigner_d_matrix(2, A3) + @test D ≈ D3d + end end @testitem "Test Hubbard U term in Nickel Oxide" setup=[TestCases] begin @@ -81,5 +89,4 @@ end n_hub = scfres.nhubbard scfres_nosym = DFTK.unfold_bz(scfres) @test norm(n_hub .- scfres_nosym.nhubbard) < 1e-8 - end From d4cb53cf7dd374c22039f31c6316f01aa52a5ccf Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Fri, 10 Oct 2025 12:00:30 +0200 Subject: [PATCH 23/50] New comments addressed --- src/postprocess/dos.jl | 2 +- test/hubbard.jl | 29 ++++++++++++++--------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/postprocess/dos.jl b/src/postprocess/dos.jl index ff9ec78f30..8e250abd08 100644 --- a/src/postprocess/dos.jl +++ b/src/postprocess/dos.jl @@ -166,7 +166,7 @@ function atomic_orbital_projectors(basis::PlaneWaveBasis{T}; labels = [] for (iatom, atom) in enumerate(basis.model.atoms) psp = atom.psp - count_n_pswfc(psp) # We need this to check if we have any atomic orbital projector + @assert count_n_pswfc(psp) > 0 # We need this to check if we have any atomic orbital projector for l in 0:psp.lmax, n in 1:DFTK.count_n_pswfc_radial(psp, l) label = DFTK.pswfc_label(psp, n, l) if !isonmanifold((; iatom, atom.species, label)) diff --git a/test/hubbard.jl b/test/hubbard.jl index 6822ba8440..60b2146a73 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -74,19 +74,18 @@ end ρ0 = guess_density(basis, magnetic_moments) scfres = self_consistent_field(basis; tol=1e-10, ρ=ρ0) - ref = -354.907446880021 - e_total = scfres.energies.total - @test abs(e_total - ref) < 1e-8 - for (term, value) in scfres.energies - if term == "Hubbard" - ref_hub = 0.17629078433258719 - e_hub = value - @test abs(e_hub - ref_hub) < 1e-8 - end - end - - # Test symmetry consistency - n_hub = scfres.nhubbard - scfres_nosym = DFTK.unfold_bz(scfres) - @test norm(n_hub .- scfres_nosym.nhubbard) < 1e-8 + @testset "Test Energy results" begin + # The reference values are obtained with first released version + # of the Hubbard code and are in good agreement with Quantum Espresso + ref = -354.907446880021 + e_total = scfres.energies.total + @test abs(e_total - ref) < 1e-8 + ref_hub = 0.17629078433258719 + @test abs(scfres.energies.Hubbard - ref_hub) < 1e-8 + end + @testset "Test symmetry consistency" begin + n_hub = scfres.nhubbard + scfres_nosym = DFTK.unfold_bz(scfres) + @test norm(n_hub .- scfres_nosym.nhubbard) < 1e-8 + end end From 1fad1c8f7e1a3d5fc1ecfd6a5e0212ee885e120c Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Fri, 10 Oct 2025 15:56:39 +0200 Subject: [PATCH 24/50] Issue solved with mpi --- src/terms/hubbard.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 3d757f5734..9e29fb26a7 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -125,6 +125,7 @@ function compute_nhubbard(manifold::OrbitalManifold, nhubbard[σ, idx, jdx] .+= basis.kweights[ik] * i_projection * diagm(occupation[ik]/filled_occ) * j_projection end + nhubbard[σ, idx, jdx] = mpi_sum(nhubbard[σ, idx, jdx], basis.comm_kpts) end nhubbard = symmetrize_nhubbard(nhubbard, basis.model, basis.symmetries, basis.model.positions[manifold_atoms]) From efc83f571fa530ab21de8788b4531d659451003d Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Tue, 21 Oct 2025 18:50:05 +0200 Subject: [PATCH 25/50] ntermediate version - HubbardOperator removed, NonlocalOperator used in its stead - TermHubbard.P now is stored in the non reshaped form - nhubbard gets back reshaped each time to be passed to the NonlocalOperator - Termhubbard checks now moved to Hubbard constructor - Hubbard struct does not support anymore the iatom field in the OrbitalManifold (this version still does not assume this, kept for record) - symmetrize_nhubbard has been moved to symmetry.jl - hubbard.jl example has been updated, now the band gap has no harcoded part - hubbard.jl test is still under revision due to problems with BZ unfolding, current version fails in local testing setup (corrections are commented) --- examples/hubbard.jl | 16 ++-- src/symmetry.jl | 28 ++++++ src/terms/hubbard.jl | 147 +++++++++++++------------------- src/terms/operators.jl | 25 ------ test/hamiltonian_consistency.jl | 2 +- test/hubbard.jl | 9 +- 6 files changed, 103 insertions(+), 124 deletions(-) diff --git a/examples/hubbard.jl b/examples/hubbard.jl index f18ba0432e..8a06e71bd9 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -25,15 +25,16 @@ model = model_DFT(lattice, atoms, positions; temperature=5e-3, functionals=PBE(), magnetic_moments=magnetic_moments) basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2] ) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) -bands = compute_bands(scfres, MonkhorstPack(2, 2, 2)); -band_gap = bands.eigenvalues[1][25] - bands.eigenvalues[1][24] +bands = compute_bands(scfres, MonkhorstPack(4, 4, 4)) +lowest_unocc_band = findfirst(ε -> ε-bands.εF > 0, bands.eigenvalues[1]) +band_gap = bands.eigenvalues[1][lowest_unocc_band] - bands.eigenvalues[1][lowest_unocc_band-1] # Then we plot the DOS and the PDOS for the relevant 3D (pseudo)atomic projector εF = bands.εF width = 5.0u"eV" εrange = (εF - austrip(width), εF + austrip(width)) p = plot_dos(bands; εrange, colors=[:red, :red]) -plot_pdos(bands; p, temperature=2e-3, iatom=1, label="3D", colors=[:yellow, :orange], εrange) +plot_pdos(bands; p, iatom=1, label="3D", colors=[:yellow, :orange], εrange) # To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant U = 10u"eV" @@ -41,18 +42,19 @@ manifold = DFTK.OrbitalManifold(;species=:Ni, label="3D") # Run SCF model = model_DFT(lattice, atoms, positions; extra_terms=[DFTK.Hubbard(manifold, U)], - functionals = PBE(), temperature=5e-3, magnetic_moments=magnetic_moments) + functionals=PBE(), temperature=5e-3, magnetic_moments) basis = PlaneWaveBasis(model; Ecut = 32, kgrid = [2, 2, 2] ) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) # Run band computation -bands_hub = compute_bands(scfres, MonkhorstPack(2, 2, 2); ρ=scfres.ρ) -band_gap = bands_hub.eigenvalues[1][26] - bands_hub.eigenvalues[1][25] +bands_hub = compute_bands(scfres, MonkhorstPack(4, 4, 4)) +lowest_unocc_band = findfirst(ε -> ε-bands_hub.εF > 0, bands_hub.eigenvalues[1]) +band_gap = bands_hub.eigenvalues[1][lowest_unocc_band] - bands_hub.eigenvalues[1][lowest_unocc_band-1] # With the electron localization introduced by the Hubbard term, the band gap has now opened, # reflecting the experimental insulating behaviour of Nickel Oxide. εF = bands_hub.εF εrange = (εF - austrip(width), εF + austrip(width)) -p = plot_dos(bands_hub; colors=[:blue, :blue], εrange) +p = plot_dos(bands_hub; p, colors=[:blue, :blue], εrange) plot_pdos(bands_hub; p, iatom=1, label="3D", colors=[:green, :purple], εrange) diff --git a/src/symmetry.jl b/src/symmetry.jl index 2e1d16cb22..2d53b4ce64 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -422,6 +422,34 @@ function symmetrize_forces(basis::PlaneWaveBasis, forces) symmetrize_forces(basis.model, forces; basis.symmetries) end +""" +Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. +""" +function symmetrize_nhubbard(nhubbard::Array{Matrix{Complex{T}}}, + model, symmetries, positions) where {T} + # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) + # WARNING: To implement +V this will need to be changed! + + nspins = size(nhubbard, 1) + natoms = size(nhubbard, 2) + nsym = length(symmetries) + # We extract the l value from the manifold size per atom (2l+1) + l = Int((size(nhubbard[1, 1, 1], 1)-1)/2) + ldim = 2*l+1 + + # Initialize the nhubbard matrix + ns = [zeros(Complex{T}, ldim, ldim) for _ in 1:nspins, _ in 1:natoms, _ in 1:natoms] + for symmetry in symmetries + Wcart = model.lattice * symmetry.W * model.inv_lattice + WigD = wigner_d_matrix(l, Wcart) + for σ in 1:nspins, iatom in 1:natoms + sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry) + ns[σ, iatom, iatom] .+= WigD' * nhubbard[σ, sym_atom, sym_atom] * WigD + end + end + ns .= ns ./ nsym +end + """" Convert a `basis` into one that doesn't use BZ symmetry. This is mainly useful for debug purposes (e.g. in cases we don't want to diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 9e29fb26a7..1f8e415be3 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -18,11 +18,10 @@ Can be called with an orbital NamedTuple and returns a boolean species ::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing} = nothing label ::Union{String, Nothing} = nothing end -function (s::OrbitalManifold)(orb) - iatom_match = isnothing(s.iatom) || (s.iatom == orb.iatom) - # See JuliaMolSim/AtomsBase.jl#139 why both species equalities are needed - species_match = isnothing(s.species) || (s.species == orb.species) || (orb.species == s.species) - label_match = isnothing(s.label) || (s.label == orb.label) +function (s::OrbitalManifold)(orbital) + iatom_match = isnothing(s.iatom) || (s.iatom == orbital.iatom) + species_match = isnothing(s.species) || (s.species == orbital.species) + label_match = isnothing(s.label) || (s.label == orbital.label) iatom_match && species_match && label_match end @@ -30,49 +29,11 @@ function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, manifold::OrbitalManifold) where {T} manifold_labels = filter(manifold, labels) isempty(manifold_labels) && @warn "Projector for $(manifold) not found." - manifold_projectors = Vector{Matrix{Complex{T}}}(undef, length(basis.kpoints)) - for (ik, projk) in enumerate(projectors) - manifold_projectors[ik] = zeros(Complex{T}, size(projectors[ik], 1), length(manifold_labels)) - iproj = 0 - for (proj_index, orb) in enumerate(labels) - # Find the index of the projector that matches the manifold label - if manifold(orb) - iproj += 1 - manifold_projectors[ik][:, iproj] = projk[:, proj_index] - end - end - end - return (; manifold_labels, manifold_projectors) -end - -""" -Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. -""" -function symmetrize_nhubbard(nhubbard::Array{Matrix{Complex{T}}}, - model, symmetries, positions) where {T} - # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) - # WARNING: To implement +V this will need to be changed! - - nspins = size(nhubbard, 1) - natoms = size(nhubbard, 2) - nsym = length(symmetries) - l = Int64((size(nhubbard[1, 1, 1], 1)-1)/2) - ldim = 2*l+1 - - # Initialize the nhubbard matrix - ns = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) - for σ in 1:nspins, iatom in 1:natoms, jatom in 1:natoms - ns[σ, iatom, jatom] = zeros(Complex{T}, ldim, ldim) - end - for symmetry in symmetries - Wcart = model.lattice * symmetry.W * model.inv_lattice - WigD = wigner_d_matrix(l, Wcart) - for σ in 1:nspins, iatom in 1:natoms - sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry) - ns[σ, iatom, iatom] .+= WigD' * nhubbard[σ, sym_atom, sym_atom] * WigD - end + proj_indices = findall(orb->manifold(orb)==true, labels) + manifold_projectors = map(enumerate(projectors)) do (ik, projk) + projk[:, proj_indices] end - ns .= ns / nsym + (; manifold_labels, manifold_projectors) end """ @@ -108,45 +69,48 @@ function compute_nhubbard(manifold::OrbitalManifold, projectors, labels, positions = basis.model.positions) where {T} filled_occ = filled_occupation(basis.model) - nprojs = length(labels) nspins = basis.model.n_spin_components - manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) + manifold_atoms = isnothing(manifold.iatom) ? findall(at -> at.species==manifold.species, + basis.model.atoms) : Int[manifold.iatom] natoms = length(manifold_atoms) # Number of atoms of the species in the manifold l = labels[1].l + projectors = reshape_hubbard_proj(basis, projectors, labels, manifold) nhubbard = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) - for σ in 1:nspins, (idx, iatom) in enumerate(manifold_atoms), (jdx, jatom) in enumerate(manifold_atoms) - nhubbard[σ, idx, jdx] = zeros(Complex{T}, 2*l+1, 2*l+1) - for ik = krange_spin(basis, σ) - # We divide by filled_occ to deal with the physical two spin channels separately. - j_projection = ψ[ik]' * projectors[ik][jdx] # <ψ|ϕJ> - i_projection = projectors[ik][idx]' * ψ[ik] # <ϕI|ψ> - # Sums over the bands - nhubbard[σ, idx, jdx] .+= basis.kweights[ik] * i_projection * - diagm(occupation[ik]/filled_occ) * j_projection + for σ in 1:nspins + for idx in 1:length(manifold_atoms), jdx in 1:length(manifold_atoms) + nhubbard[σ, idx, jdx] = zeros(Complex{T}, 2*l+1, 2*l+1) + for ik = krange_spin(basis, σ) + j_projection = ψ[ik]' * projectors[ik][jdx] # <ψ|ϕJ> + i_projection = projectors[ik][idx]' * ψ[ik] # <ϕI|ψ> + # Sums over the bands, dividing by filled_occ to deal + # with the physical two spin channels separately + nhubbard[σ, idx, jdx] .+= basis.kweights[ik] * (i_projection * + (diagm(occupation[ik]/filled_occ) * j_projection)) + end + nhubbard[σ, idx, jdx] = mpi_sum(nhubbard[σ, idx, jdx], basis.comm_kpts) end - nhubbard[σ, idx, jdx] = mpi_sum(nhubbard[σ, idx, jdx], basis.comm_kpts) end nhubbard = symmetrize_nhubbard(nhubbard, basis.model, - basis.symmetries, basis.model.positions[manifold_atoms]) + basis.symmetries, basis.model.positions[manifold_atoms]) - return (; nhubbard, manifold_labels=labels) + (; nhubbard, manifold_labels=labels) end function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} - manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) + manifold_atoms = isnothing(manifold.iatom) ? findall(at -> at.species==manifold.species, + basis.model.atoms) : Int[manifold.iatom] natoms = length(manifold_atoms) nprojs = length(labels) l = labels[1].l @assert all(label -> label.l==l, labels) - @assert length(labels) == natoms * (2*l+1) + @assert length(labels) == natoms * (2*l+1) "Labels length error: + $(length(labels)) != $(natoms)*$(2*l+1)" p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for i in 1:length(projectors)] for (idx, iatom) in enumerate(manifold_atoms) for i in 1:2*l+1:nprojs - if iatom != labels[i].iatom - continue - else + if iatom == labels[i].iatom for (ik, projk) in enumerate(projectors) p_I[ik][idx] = projk[:, i:i+2*l] end @@ -164,25 +128,29 @@ Hubbard energy: ``` """ struct Hubbard - manifold :: OrbitalManifold - U + manifold::OrbitalManifold + U::Float64 + function Hubbard(manifold::OrbitalManifold, u) + if isnothing(manifold.label) || isnothing(manifold.species) + error("Hubbard term needs both a species and a label inside OrbitalManifold") + elseif !isnothing(manifold.iatom) + error("Hubbard term does not support iatom specification inside OrbitalManifold") + end + u = austrip(u) + new(manifold, u) + end end function (hubbard::Hubbard)(basis::AbstractBasis) - if isnothing(hubbard.manifold.label) || isnothing(hubbard.manifold.species) - error("TermHubbard needs both a species and a label inside OrbitalManifold") - end projs, labels = atomic_orbital_projectors(basis) - labels, projectors = extract_manifold(basis, projs, labels, hubbard.manifold) - projectors = reshape_hubbard_proj(basis, projectors, labels, hubbard.manifold) - U = austrip(hubbard.U) - TermHubbard(hubbard.manifold, U, projectors, labels) + labels, projectors_matrix = extract_manifold(basis, projs, labels, hubbard.manifold) + TermHubbard(hubbard.manifold, hubbard.U, projectors_matrix, labels) end struct TermHubbard{PT, L} <: Term - manifold :: OrbitalManifold - U - P :: PT - labels :: L + manifold:: OrbitalManifold + U::Float64 + P:: PT + labels:: L end @timing "ene_ops: hubbard" function ene_ops(term::TermHubbard, @@ -190,26 +158,27 @@ end ψ, occupation; nhubbard=nothing, labels=term.labels, kwargs...) where {T} - if isnothing(ψ) - if isnothing(nhubbard) - return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) - end - else - nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; projectors=term.P, - labels).nhubbard + if isnothing(nhubbard) + return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end proj = term.P - ops = [HubbardUOperator(basis, kpt, term.U, nhubbard, proj[ik]) - for (ik,kpt) in enumerate(basis.kpoints)] filled_occ = filled_occupation(basis.model) types = findall(at -> at.species==term.manifold.species, basis.model.atoms) natoms = length(types) nspins = basis.model.n_spin_components + nproj_atom = size(nhubbard[1,1,1], 1) # This is the number of projectors per atom, namely 2l+1 + # For the ops we have to reshape nhubbard to match the NonlocalOperator structure, using a block diagonal form + nhub = [zeros(Complex{T}, nproj_atom*natoms, nproj_atom*natoms) + for _ in 1:nspins] E = zero(T) for σ in 1:nspins, iatom in 1:natoms - E += filled_occ * 1/2 * term.U * + nhub[σ][1+nproj_atom*(iatom-1):nproj_atom*iatom, 1+nproj_atom*(iatom-1):nproj_atom*iatom] = + nhubbard[σ, iatom, iatom] + E += filled_occ * 1/2 * term.U * real(tr(nhubbard[σ, iatom, iatom] * (I - nhubbard[σ, iatom, iatom]))) end + ops = [NonlocalOperator(basis, kpt, proj[ik], 1/2 * term.U * (I - 2*nhub[kpt.spin])) + for (ik,kpt) in enumerate(basis.kpoints)] return (; E, ops) end diff --git a/src/terms/operators.jl b/src/terms/operators.jl index 8a990c6b52..c2bfed8aff 100644 --- a/src/terms/operators.jl +++ b/src/terms/operators.jl @@ -155,31 +155,6 @@ function apply!(Hψ, op::DivAgradOperator, ψ; end # TODO Implement Matrix(op::DivAgrad) -@doc raw""" -"Hubbard U" operator ``V_σ ψ = Σᵢ Σₘ₁ₘ₂ U/2 * (1 - 2nhubbard[σ, i,i][m1,m2]) * Pᵢₘ₁ * Pᵢₘ₂' * ψ`` -where ``Pᵢₘ₁`` is the projector for atom i and orbital m₁. -(m₁ is usually just the magnetic quantum number, since l is usually fixed) -""" -struct HubbardUOperator{T <: Real} <: RealFourierOperator - basis :: PlaneWaveBasis{T} - kpoint :: Kpoint{T} - U # Hubbard U parameter - nhubbard :: Array{Matrix{Complex{T}}} - proj :: Vector{Matrix{Complex{T}}} # It is the projector for the given kpoint only -end -function apply!(Hψ, op::HubbardUOperator, ψ) - σ = op.kpoint.spin - nhubbard = op.nhubbard - projectors = op.proj - natoms = size(nhubbard, 2) - for iatom in 1:natoms - n_ii = nhubbard[σ, iatom, iatom] - iszero(n_ii) && continue - coefficient = 1/2 * op.U * (I - 2*n_ii) - mul!(Hψ.fourier, projectors[iatom], coefficient * (projectors[iatom]' * ψ.fourier), 1, 1) - end -end - # Optimize RFOs by combining terms that can be combined function optimize_operators(ops) ops = [op for op in ops if !(op isa NoopOperator)] diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 2eac28f8de..43513d2319 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -15,7 +15,7 @@ function test_matrix_repr_operator(hamk, ψk; atol=1e-8) catch e allowed_missing_operators = Union{DFTK.DivAgradOperator, DFTK.MagneticFieldOperator, - DFTK.HubbardUOperator} + DFTK.HubbardOperator} @assert operator isa allowed_missing_operators @info "Matrix of operator $(nameof(typeof(operator))) not yet supported" maxlog=1 end diff --git a/test/hubbard.jl b/test/hubbard.jl index 60b2146a73..3f898fa591 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -41,7 +41,9 @@ end end -@testitem "Test Hubbard U term in Nickel Oxide" setup=[TestCases] begin +# The unfolding of the kpoints is not supported with MPI +@testitem "Test Hubbard U term in Nickel Oxide" #= + =# tags=[:dont_test_mpi] setup=[TestCases] begin using DFTK using PseudoPotentialData using Unitful @@ -85,7 +87,10 @@ end end @testset "Test symmetry consistency" begin n_hub = scfres.nhubbard + #basis_nosym = DFTK.unfold_bz(basis) + #scfres_nosym = self_consistent_field(basis_nosym; tol=1e-10, ρ=ρ0) scfres_nosym = DFTK.unfold_bz(scfres) - @test norm(n_hub .- scfres_nosym.nhubbard) < 1e-8 + nhub_nosym = DFTK.compute_nhubbard(manifold, scfres_nosym.basis, scfres_nosym.ψ, scfres_nosym.occupation) + @test norm(n_hub .- nhub_nosym) < 1e-8 end end From 074f5394e2d791fd35eda6086c182b0ddef310af Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Wed, 22 Oct 2025 17:19:53 +0200 Subject: [PATCH 26/50] New simplified version - HubbardOperator removed, NonlocalOperator used in its stead - TermHubbard.P now is stored in the non reshaped form - nhubbard gets back reshaped each time to be passed to the NonlocalOperator - Termhubbard checks now moved to Hubbard constructor - Hubbard struct does not support anymore the iatom field in the OrbitalManifold - symmetrize_nhubbard has been moved to symmetry.jl - hubbard.jl example has been updated, now the band gap has no harcoded part - hubbard.jl test is still under revision due to problems with BZ unfolding, current version fails in local testing setup --- src/terms/hubbard.jl | 6 ++---- test/hubbard.jl | 9 +++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 1f8e415be3..a1fa0d0919 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -71,8 +71,7 @@ function compute_nhubbard(manifold::OrbitalManifold, filled_occ = filled_occupation(basis.model) nspins = basis.model.n_spin_components - manifold_atoms = isnothing(manifold.iatom) ? findall(at -> at.species==manifold.species, - basis.model.atoms) : Int[manifold.iatom] + manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) natoms = length(manifold_atoms) # Number of atoms of the species in the manifold l = labels[1].l projectors = reshape_hubbard_proj(basis, projectors, labels, manifold) @@ -99,8 +98,7 @@ end function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} - manifold_atoms = isnothing(manifold.iatom) ? findall(at -> at.species==manifold.species, - basis.model.atoms) : Int[manifold.iatom] + manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) natoms = length(manifold_atoms) nprojs = length(labels) l = labels[1].l diff --git a/test/hubbard.jl b/test/hubbard.jl index 3f898fa591..4a64853432 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -81,16 +81,13 @@ end # of the Hubbard code and are in good agreement with Quantum Espresso ref = -354.907446880021 e_total = scfres.energies.total - @test abs(e_total - ref) < 1e-8 + @test e_total ≈ ref ref_hub = 0.17629078433258719 - @test abs(scfres.energies.Hubbard - ref_hub) < 1e-8 + @test scfres.energies.Hubbard ≈ ref_hub end @testset "Test symmetry consistency" begin n_hub = scfres.nhubbard - #basis_nosym = DFTK.unfold_bz(basis) - #scfres_nosym = self_consistent_field(basis_nosym; tol=1e-10, ρ=ρ0) scfres_nosym = DFTK.unfold_bz(scfres) - nhub_nosym = DFTK.compute_nhubbard(manifold, scfres_nosym.basis, scfres_nosym.ψ, scfres_nosym.occupation) - @test norm(n_hub .- nhub_nosym) < 1e-8 + @test n_hub ≈ scfres_nosym.nhubbard end end From 41bebad82e7be15a0a5d8f6048fde8209eb48373 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Wed, 22 Oct 2025 17:59:08 +0200 Subject: [PATCH 27/50] New simplified version - HubbardOperator removed, NonlocalOperator used in its stead - TermHubbard.P now is stored in the non reshaped form - nhubbard gets back reshaped each time to be passed to the NonlocalOperator - Termhubbard checks now moved to Hubbard constructor - Hubbard struct does not support anymore the iatom field in the OrbitalManifold - symmetrize_nhubbard has been moved to symmetry.jl - hubbard.jl example has been updated, now the band gap has no harcoded part - hubbard.jl test is still under revision due to problems with BZ unfolding, current version fails in local testing setup --- test/hamiltonian_consistency.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 43513d2319..7ace268a92 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -14,8 +14,7 @@ function test_matrix_repr_operator(hamk, ψk; atol=1e-8) @test norm(operator_matrix*ψk - operator*ψk) < atol catch e allowed_missing_operators = Union{DFTK.DivAgradOperator, - DFTK.MagneticFieldOperator, - DFTK.HubbardOperator} + DFTK.MagneticFieldOperator} @assert operator isa allowed_missing_operators @info "Matrix of operator $(nameof(typeof(operator))) not yet supported" maxlog=1 end From a43171736f60b33fc3d20adc5fc791f090afd2a7 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Wed, 22 Oct 2025 19:18:03 +0200 Subject: [PATCH 28/50] New simplified version - HubbardOperator removed, NonlocalOperator used in its stead - TermHubbard.P now is stored in the non reshaped form - nhubbard gets back reshaped each time to be passed to the NonlocalOperator - Termhubbard checks now moved to Hubbard constructor - Hubbard struct does not support anymore the iatom field in the OrbitalManifold - symmetrize_nhubbard has been moved to symmetry.jl - hubbard.jl example has been updated, now the band gap has no harcoded part --- src/symmetry.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/symmetry.jl b/src/symmetry.jl index 2d53b4ce64..9db1c9bf56 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -522,7 +522,7 @@ function unfold_bz(scfres) eigenvalues = unfold_array(scfres.basis, basis_unfolded, scfres.eigenvalues, false) occupation = unfold_array(scfres.basis, basis_unfolded, scfres.occupation, false) energies, ham = energy_hamiltonian(basis_unfolded, ψ, occupation; - scfres.ρ, eigenvalues, scfres.εF) + scfres.ρ, scfres.nhubbard, eigenvalues, scfres.εF) @assert energies.total ≈ scfres.energies.total new_scfres = (; basis=basis_unfolded, ψ, ham, eigenvalues, occupation) merge(scfres, new_scfres) From 8a87487e9f3f9a9aca646ad0b4449ea040d58d17 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 23 Oct 2025 16:09:19 +0200 Subject: [PATCH 29/50] Test issue solved --- test/hubbard.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/hubbard.jl b/test/hubbard.jl index 4a64853432..14cb53093b 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -88,6 +88,10 @@ end @testset "Test symmetry consistency" begin n_hub = scfres.nhubbard scfres_nosym = DFTK.unfold_bz(scfres) - @test n_hub ≈ scfres_nosym.nhubbard + nhub_nosym = DFTK.compute_nhubbard(manifold, scfres_nosym.basis, + scfres_nosym.ψ, scfres_nosym.occupation; + projectors=scfres_nosym.basis.terms[8].P, + labels=scfres_nosym.basis.terms[8].labels).nhubbard + @test n_hub ≈ nhub_nosym end end From 8de74987b15bd1d9bcae751ef0ba705a84ca6de1 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 23 Oct 2025 18:55:02 +0200 Subject: [PATCH 30/50] Updating branch --- src/postprocess/band_structure.jl | 3 ++- src/scf/self_consistent_field.jl | 6 +++--- src/terms/hubbard.jl | 30 ++++++++++++++---------------- test/hubbard.jl | 6 ++++-- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index 72006cf094..8b1bf09237 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -16,7 +16,8 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): n_bands=default_n_bands_bandstructure(basis.model), n_extra=3, ρ=nothing, τ=nothing, εF=nothing, occupation=nothing, nhubbard=nothing, - eigensolver=lobpcg_hyper, tol=1e-3, kwargs...) + eigensolver=lobpcg_hyper, tol=1e-3, seed=nothing, + kwargs...) # kcoords are the kpoint coordinates in fractional coordinates if isnothing(ρ) if any(t isa TermNonlinear for t in basis.terms) diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index 0a79cd7a48..4d0687e475 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -182,14 +182,14 @@ Overview of parameters: for term in basis.terms if isa(term, DFTK.TermHubbard) nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; - projectors=term.P, labels=term.labels).nhubbard + projectors=term.P, labels=term.labels) end end # Update info with results gathered so far info_next = (; ham, basis, converged, stage=:iterate, algorithm="SCF", ρin, τ, nhubbard, α=damping, n_iter, nbandsalg.occupation_threshold, - runtime_ns=time_ns() - start_ns, nextstate..., + seed, runtime_ns=time_ns() - start_ns, nextstate..., diagonalization=[nextstate.diagonalization]) # Compute the energy of the new state @@ -233,7 +233,7 @@ Overview of parameters: scfres = (; ham, basis, energies, converged, nbandsalg.occupation_threshold, ρ=ρout, τ, α=damping, eigenvalues, occupation, εF, info.n_bands_converge, info.n_iter, info.n_matvec, ψ, nhubbard, info.diagonalization, stage=:finalize, - info.history_Δρ, info.history_Etot, info.timedout, mixing, + info.history_Δρ, info.history_Etot, info.timedout, mixing, seed, runtime_ns=time_ns() - start_ns, algorithm="SCF") callback(scfres) scfres diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index a1fa0d0919..92359ec9d2 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -54,14 +54,14 @@ Computes a matrix nhubbard of size (nspins, natoms, natoms), where each entry nh Overview of inputs: - `manifold`: OrbitalManifold with the atomic orbital type to define the Hubbard manifold. - `occupation`: Occupation matrix for the bands. -- `positions`: Positions of the atoms in the unit cell. Default is model.positions. +- `projectors` (kwarg): Vector of projection matrices. For each matrix, each column corresponds + to a different atomic orbital projector, as specified in labels. +- `labels` (kwarg): Vector of NamedTuples. Each projectors[ik][:,iproj] column has all relevant + chemical information stored in the corresponding labels[iproj] NamedTuple. Overviw of outputs: - `nhubbard`: 3-tensor of matrices. Outer indices select spin, iatom and jatom, inner indices select m1 and m2 in the manifold. -- `manifold_labels`: Labels for all manifold orbitals, corresponding to different columns of p_I. -- `p_I`: Projectors for the manifold. Those are orthonormalized against all orbitals, - also against those outside of the manifold. """ function compute_nhubbard(manifold::OrbitalManifold, basis::PlaneWaveBasis{T}, @@ -92,8 +92,6 @@ function compute_nhubbard(manifold::OrbitalManifold, end nhubbard = symmetrize_nhubbard(nhubbard, basis.model, basis.symmetries, basis.model.positions[manifold_atoms]) - - (; nhubbard, manifold_labels=labels) end function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, @@ -125,17 +123,17 @@ Hubbard energy: 1/2 Σ_{σI} U * Tr[nhubbard[σ,i,i] * (1 - nhubbard[σ,i,i])] ``` """ -struct Hubbard +struct Hubbard{T} manifold::OrbitalManifold - U::Float64 - function Hubbard(manifold::OrbitalManifold, u) + U::T + function Hubbard(manifold::OrbitalManifold, U) if isnothing(manifold.label) || isnothing(manifold.species) error("Hubbard term needs both a species and a label inside OrbitalManifold") elseif !isnothing(manifold.iatom) error("Hubbard term does not support iatom specification inside OrbitalManifold") end - u = austrip(u) - new(manifold, u) + U = austrip(U) + new{typeof(U)}(manifold, U) end end function (hubbard::Hubbard)(basis::AbstractBasis) @@ -144,11 +142,11 @@ function (hubbard::Hubbard)(basis::AbstractBasis) TermHubbard(hubbard.manifold, hubbard.U, projectors_matrix, labels) end -struct TermHubbard{PT, L} <: Term - manifold:: OrbitalManifold - U::Float64 - P:: PT - labels:: L +struct TermHubbard{T, PT, L} <: Term + manifold::OrbitalManifold + U::T + P::PT + labels::L end @timing "ene_ops: hubbard" function ene_ops(term::TermHubbard, diff --git a/test/hubbard.jl b/test/hubbard.jl index 14cb53093b..e379886e71 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -88,10 +88,12 @@ end @testset "Test symmetry consistency" begin n_hub = scfres.nhubbard scfres_nosym = DFTK.unfold_bz(scfres) + term_hub = only(findfirst(term -> typeof(term) == DFTK.TermHubbard, + scfres_nosym.basis.terms)) nhub_nosym = DFTK.compute_nhubbard(manifold, scfres_nosym.basis, scfres_nosym.ψ, scfres_nosym.occupation; - projectors=scfres_nosym.basis.terms[8].P, - labels=scfres_nosym.basis.terms[8].labels).nhubbard + projectors=term_hub.P, + labels=term_hub.labels) @test n_hub ≈ nhub_nosym end end From 75cc62c602512cb3fb3f729b293af8165c6d6f84 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Fri, 24 Oct 2025 16:00:56 +0200 Subject: [PATCH 31/50] Test hubbard.jl fixed for mpi case --- test/hubbard.jl | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/hubbard.jl b/test/hubbard.jl index e379886e71..000a4448d9 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -42,8 +42,7 @@ end # The unfolding of the kpoints is not supported with MPI -@testitem "Test Hubbard U term in Nickel Oxide" #= - =# tags=[:dont_test_mpi] setup=[TestCases] begin +@testitem "Test Hubbard U term in Nickel Oxide" setup=[TestCases] begin using DFTK using PseudoPotentialData using Unitful @@ -85,15 +84,18 @@ end ref_hub = 0.17629078433258719 @test scfres.energies.Hubbard ≈ ref_hub end - @testset "Test symmetry consistency" begin - n_hub = scfres.nhubbard - scfres_nosym = DFTK.unfold_bz(scfres) - term_hub = only(findfirst(term -> typeof(term) == DFTK.TermHubbard, - scfres_nosym.basis.terms)) - nhub_nosym = DFTK.compute_nhubbard(manifold, scfres_nosym.basis, - scfres_nosym.ψ, scfres_nosym.occupation; - projectors=term_hub.P, - labels=term_hub.labels) - @test n_hub ≈ nhub_nosym + if mpi_nprocs() == 1 + @testset "Test symmetry consistency" begin + n_hub = scfres.nhubbard + scfres_nosym = DFTK.unfold_bz(scfres) + term_idx = only(findfirst(term -> isa(term, DFTK.TermHubbard), + scfres_nosym.basis.terms)) + term_hub = scfres_nosym.basis.terms[term_idx] + nhub_nosym = DFTK.compute_nhubbard(manifold, scfres_nosym.basis, + scfres_nosym.ψ, scfres_nosym.occupation; + projectors=term_hub.P, + labels=term_hub.labels) + @test n_hub ≈ nhub_nosym + end end end From b2da690ec4824120b649d981055c770d818ac77c Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Fri, 24 Oct 2025 16:19:21 +0200 Subject: [PATCH 32/50] New comments addressed --- Project.toml | 2 +- src/symmetry.jl | 3 ++- src/terms/hubbard.jl | 1 + test/hubbard.jl | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 87b2c213ed..63633872ea 100644 --- a/Project.toml +++ b/Project.toml @@ -77,7 +77,7 @@ ASEconvert = "0.1" AbstractFFTs = "1" Aqua = "0.8.5" Artifacts = "1" -AtomsBase = "0.5" +AtomsBase = "0.5.2" AtomsBuilder = "0.2" AtomsCalculators = "0.2.3" AtomsIO = "0.3" diff --git a/src/symmetry.jl b/src/symmetry.jl index 9db1c9bf56..3e065f3eca 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -447,7 +447,8 @@ function symmetrize_nhubbard(nhubbard::Array{Matrix{Complex{T}}}, ns[σ, iatom, iatom] .+= WigD' * nhubbard[σ, sym_atom, sym_atom] * WigD end end - ns .= ns ./ nsym + ns ./= nsym + ns end """" diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 92359ec9d2..e51943d8b0 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -27,6 +27,7 @@ end function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, manifold::OrbitalManifold) where {T} + # We extract the labels only for orbitals belonging to the manifold manifold_labels = filter(manifold, labels) isempty(manifold_labels) && @warn "Projector for $(manifold) not found." proj_indices = findall(orb->manifold(orb)==true, labels) diff --git a/test/hubbard.jl b/test/hubbard.jl index 000a4448d9..c8b73787e7 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -40,8 +40,7 @@ @test D ≈ D3d end end - -# The unfolding of the kpoints is not supported with MPI + @testitem "Test Hubbard U term in Nickel Oxide" setup=[TestCases] begin using DFTK using PseudoPotentialData @@ -84,6 +83,7 @@ end ref_hub = 0.17629078433258719 @test scfres.energies.Hubbard ≈ ref_hub end + # The unfolding of the kpoints is not supported with MPI if mpi_nprocs() == 1 @testset "Test symmetry consistency" begin n_hub = scfres.nhubbard From 761ee8b7bf5dd6d81d3f546ff9be7d1ca4a33dbd Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Tue, 28 Oct 2025 13:34:55 +0100 Subject: [PATCH 33/50] New comments have been addressed --- src/terms/hubbard.jl | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index e51943d8b0..7fa86f3556 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -30,7 +30,7 @@ function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, # We extract the labels only for orbitals belonging to the manifold manifold_labels = filter(manifold, labels) isempty(manifold_labels) && @warn "Projector for $(manifold) not found." - proj_indices = findall(orb->manifold(orb)==true, labels) + proj_indices = findall(orb->manifold(orb) == true, labels) manifold_projectors = map(enumerate(projectors)) do (ik, projk) projk[:, proj_indices] end @@ -72,7 +72,7 @@ function compute_nhubbard(manifold::OrbitalManifold, filled_occ = filled_occupation(basis.model) nspins = basis.model.n_spin_components - manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) + manifold_atoms = findall(at -> at.species == manifold.species, basis.model.atoms) natoms = length(manifold_atoms) # Number of atoms of the species in the manifold l = labels[1].l projectors = reshape_hubbard_proj(basis, projectors, labels, manifold) @@ -97,25 +97,23 @@ end function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} - manifold_atoms = findall(at -> at.species==manifold.species, basis.model.atoms) + manifold_atoms = findall(at -> at.species == manifold.species, basis.model.atoms) natoms = length(manifold_atoms) nprojs = length(labels) l = labels[1].l @assert all(label -> label.l==l, labels) - @assert length(labels) == natoms * (2*l+1) "Labels length error: - $(length(labels)) != $(natoms)*$(2*l+1)" + @assert length(labels) == natoms * (2*l+1) p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for i in 1:length(projectors)] for (idx, iatom) in enumerate(manifold_atoms) for i in 1:2*l+1:nprojs - if iatom == labels[i].iatom - for (ik, projk) in enumerate(projectors) - p_I[ik][idx] = projk[:, i:i+2*l] - end + iatom != labels[i].iatom && continue + for (ik, projk) in enumerate(projectors) + p_I[ik][idx] = projk[:, i:i+2*l] end end end - return p_I + p_I end @doc raw""" @@ -161,21 +159,21 @@ end proj = term.P filled_occ = filled_occupation(basis.model) - types = findall(at -> at.species==term.manifold.species, basis.model.atoms) + types = findall(at -> at.species == term.manifold.species, basis.model.atoms) natoms = length(types) - nspins = basis.model.n_spin_components + n_spin = basis.model.n_spin_components nproj_atom = size(nhubbard[1,1,1], 1) # This is the number of projectors per atom, namely 2l+1 # For the ops we have to reshape nhubbard to match the NonlocalOperator structure, using a block diagonal form - nhub = [zeros(Complex{T}, nproj_atom*natoms, nproj_atom*natoms) - for _ in 1:nspins] + nhub = [zeros(Complex{T}, nproj_atom*natoms, nproj_atom*natoms) for _ in 1:n_spin] E = zero(T) - for σ in 1:nspins, iatom in 1:natoms - nhub[σ][1+nproj_atom*(iatom-1):nproj_atom*iatom, 1+nproj_atom*(iatom-1):nproj_atom*iatom] = + for σ in 1:n_spin, iatom in 1:natoms + proj_range = 1+nproj_atom*(iatom-1):nproj_atom*iatom, + nhub[σ][proj_range, proj_range] = nhubbard[σ, iatom, iatom] - E += filled_occ * 1/2 * term.U * + E += filled_occ * 1/T(2) * term.U * real(tr(nhubbard[σ, iatom, iatom] * (I - nhubbard[σ, iatom, iatom]))) end - ops = [NonlocalOperator(basis, kpt, proj[ik], 1/2 * term.U * (I - 2*nhub[kpt.spin])) + ops = [NonlocalOperator(basis, kpt, proj[ik], 1/T{2} * term.U * (I - 2*nhub[kpt.spin])) for (ik,kpt) in enumerate(basis.kpoints)] return (; E, ops) end From ea48cca3e48cb45bb0557f87b11e750208b0affe Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Tue, 28 Oct 2025 16:14:55 +0100 Subject: [PATCH 34/50] Update examples/hubbard.jl Co-authored-by: Bruno Ploumhans <13494793+Technici4n@users.noreply.github.com> --- examples/hubbard.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hubbard.jl b/examples/hubbard.jl index 8a06e71bd9..7ece574908 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -43,7 +43,7 @@ manifold = DFTK.OrbitalManifold(;species=:Ni, label="3D") # Run SCF model = model_DFT(lattice, atoms, positions; extra_terms=[DFTK.Hubbard(manifold, U)], functionals=PBE(), temperature=5e-3, magnetic_moments) -basis = PlaneWaveBasis(model; Ecut = 32, kgrid = [2, 2, 2] ) +basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2] ) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) # Run band computation From b7e90bb617b52dc13c492592076d769a5f0e603f Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Wed, 29 Oct 2025 01:03:35 +0100 Subject: [PATCH 35/50] New comments have been addressed --- examples/hubbard.jl | 2 +- ext/DFTKPlotsExt.jl | 2 +- src/DFTK.jl | 1 + src/terms/hubbard.jl | 21 ++++++++++----------- test/hamiltonian_consistency.jl | 4 ++-- test/hubbard.jl | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/hubbard.jl b/examples/hubbard.jl index 8a06e71bd9..8430d8caad 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -38,7 +38,7 @@ plot_pdos(bands; p, iatom=1, label="3D", colors=[:yellow, :orange], εrange) # To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant U = 10u"eV" -manifold = DFTK.OrbitalManifold(;species=:Ni, label="3D") +manifold = OrbitalManifold(;species=:Ni, label="3D") # Run SCF model = model_DFT(lattice, atoms, positions; extra_terms=[DFTK.Hubbard(manifold, U)], diff --git a/ext/DFTKPlotsExt.jl b/ext/DFTKPlotsExt.jl index 76128a9788..8e516e1baf 100644 --- a/ext/DFTKPlotsExt.jl +++ b/ext/DFTKPlotsExt.jl @@ -164,7 +164,7 @@ function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom, label=nothi spinlabels = spin_components(basis.model) pdos = DFTK.sum_pdos(compute_pdos(εs, basis, ψ, eigenvalues; positions, temperature, smearing), - [DFTK.OrbitalManifold(;iatom, label)]) + [OrbitalManifold(;iatom, label)]) for σ = 1:n_spin plot_label = n_spin > 1 ? "$(species) $(orb_name) $(spinlabels[σ]) spin" : "$(species) $(orb_name)" Plots.plot!(p, (εs .- eshift) .* to_unit, pdos[:, σ]; label=plot_label, color=colors[σ]) diff --git a/src/DFTK.jl b/src/DFTK.jl index 5d75f937e9..85cfd850e0 100644 --- a/src/DFTK.jl +++ b/src/DFTK.jl @@ -113,6 +113,7 @@ export Ewald export PspCorrection export Entropy export Hubbard +export OrbitalManifold export Magnetic export PairwisePotential export Anyonic diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 7fa86f3556..719e3d0ad0 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -30,7 +30,7 @@ function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, # We extract the labels only for orbitals belonging to the manifold manifold_labels = filter(manifold, labels) isempty(manifold_labels) && @warn "Projector for $(manifold) not found." - proj_indices = findall(orb->manifold(orb) == true, labels) + proj_indices = findall(orb -> manifold(orb), labels) manifold_projectors = map(enumerate(projectors)) do (ik, projk) projk[:, proj_indices] end @@ -40,7 +40,7 @@ end """ compute_nhubbard(manifold, basis, ψ, occupation; [projectors, labels, positions]) -Computes a matrix nhubbard of size (nspins, natoms, natoms), where each entry nhubbard[iatom, jatom] +Computes a matrix nhubbard of size (n_spin, natoms, natoms), where each entry nhubbard[iatom, jatom] contains the submatrix of the occupation matrix corresponding to the projectors of atom iatom and atom jatom, with dimensions determined by the number of projectors for each atom. The atoms and orbitals are defined by the manifold tuple. @@ -70,14 +70,14 @@ function compute_nhubbard(manifold::OrbitalManifold, projectors, labels, positions = basis.model.positions) where {T} filled_occ = filled_occupation(basis.model) - nspins = basis.model.n_spin_components + n_spin = basis.model.n_spin_components manifold_atoms = findall(at -> at.species == manifold.species, basis.model.atoms) natoms = length(manifold_atoms) # Number of atoms of the species in the manifold l = labels[1].l projectors = reshape_hubbard_proj(basis, projectors, labels, manifold) - nhubbard = Array{Matrix{Complex{T}}}(undef, nspins, natoms, natoms) - for σ in 1:nspins + nhubbard = Array{Matrix{Complex{T}}}(undef, n_spin, natoms, natoms) + for σ in 1:n_spin for idx in 1:length(manifold_atoms), jdx in 1:length(manifold_atoms) nhubbard[σ, idx, jdx] = zeros(Complex{T}, 2*l+1, 2*l+1) for ik = krange_spin(basis, σ) @@ -85,8 +85,8 @@ function compute_nhubbard(manifold::OrbitalManifold, i_projection = projectors[ik][idx]' * ψ[ik] # <ϕI|ψ> # Sums over the bands, dividing by filled_occ to deal # with the physical two spin channels separately - nhubbard[σ, idx, jdx] .+= basis.kweights[ik] * (i_projection * - (diagm(occupation[ik]/filled_occ) * j_projection)) + nhubbard[σ, idx, jdx] .+= (basis.kweights[ik] * i_projection * + diagm(occupation[ik]/filled_occ) * j_projection) end nhubbard[σ, idx, jdx] = mpi_sum(nhubbard[σ, idx, jdx], basis.comm_kpts) end @@ -167,13 +167,12 @@ end nhub = [zeros(Complex{T}, nproj_atom*natoms, nproj_atom*natoms) for _ in 1:n_spin] E = zero(T) for σ in 1:n_spin, iatom in 1:natoms - proj_range = 1+nproj_atom*(iatom-1):nproj_atom*iatom, - nhub[σ][proj_range, proj_range] = - nhubbard[σ, iatom, iatom] + proj_range = (1+nproj_atom*(iatom-1)):(nproj_atom*iatom) + nhub[σ][proj_range, proj_range] = nhubbard[σ, iatom, iatom] E += filled_occ * 1/T(2) * term.U * real(tr(nhubbard[σ, iatom, iatom] * (I - nhubbard[σ, iatom, iatom]))) end - ops = [NonlocalOperator(basis, kpt, proj[ik], 1/T{2} * term.U * (I - 2*nhub[kpt.spin])) + ops = [NonlocalOperator(basis, kpt, proj[ik], 1/T(2) * term.U * (I - 2*nhub[kpt.spin])) for (ik,kpt) in enumerate(basis.kpoints)] return (; E, ops) end diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 7ace268a92..29654e5120 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -92,13 +92,13 @@ end test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) test_consistency_term(Hartree()) - test_consistency_term(Hubbard(DFTK.OrbitalManifold(; species=:Si, label="3P"), 0.01)) + test_consistency_term(Hubbard(OrbitalManifold(; species=:Si, label="3P"), 0.01)) test_consistency_term(Ewald()) test_consistency_term(PspCorrection()) test_consistency_term(Xc([:lda_xc_teter93])) test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) # TODO: fix consistency for meta-GGA with NLCC + test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) # TODO: see JuliaMolSim/DFTK.jl#1180 test_consistency_term(Xc([:mgga_x_scan]; use_nlcc=false)) test_consistency_term(Xc([:mgga_c_scan]; use_nlcc=false), spin_polarization=:collinear) test_consistency_term(Xc([:mgga_x_b00]; use_nlcc=false)) diff --git a/test/hubbard.jl b/test/hubbard.jl index c8b73787e7..5cc169e38f 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -50,7 +50,7 @@ end # Hubbard parameters U = 10u"eV" - manifold = DFTK.OrbitalManifold(;species=:Ni, label="3D") + manifold = OrbitalManifold(;species=:Ni, label="3D") a = 7.9 # Bohr lattice = a * [[ 1.0 0.5 0.5]; @@ -88,8 +88,8 @@ end @testset "Test symmetry consistency" begin n_hub = scfres.nhubbard scfres_nosym = DFTK.unfold_bz(scfres) - term_idx = only(findfirst(term -> isa(term, DFTK.TermHubbard), - scfres_nosym.basis.terms)) + term_idx = findfirst(term -> isa(term, DFTK.TermHubbard), + scfres_nosym.basis.terms) term_hub = scfres_nosym.basis.terms[term_idx] nhub_nosym = DFTK.compute_nhubbard(manifold, scfres_nosym.basis, scfres_nosym.ψ, scfres_nosym.occupation; From 2fe39dc1d7ded76e4f32d8353b2b12c9e5ed9829 Mon Sep 17 00:00:00 2001 From: "Michael F. Herbst" Date: Wed, 29 Oct 2025 08:47:34 +0100 Subject: [PATCH 36/50] Be more explicit --- test/hamiltonian_consistency.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 29654e5120..e76fa780b5 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -98,7 +98,8 @@ end test_consistency_term(Xc([:lda_xc_teter93])) test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) # TODO: see JuliaMolSim/DFTK.jl#1180 + # TODO: for use_nlcc=true need to fix consistency for meta-GGA with NLCC (see JuliaMolSim/DFTK.jl#1180) + test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) test_consistency_term(Xc([:mgga_x_scan]; use_nlcc=false)) test_consistency_term(Xc([:mgga_c_scan]; use_nlcc=false), spin_polarization=:collinear) test_consistency_term(Xc([:mgga_x_b00]; use_nlcc=false)) From 3c611b1cbcdd606fb43c14dfe07d59f460e5597f Mon Sep 17 00:00:00 2001 From: "Michael F. Herbst" Date: Wed, 29 Oct 2025 08:54:10 +0100 Subject: [PATCH 37/50] Cosmetic fixes to example --- examples/hubbard.jl | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/hubbard.jl b/examples/hubbard.jl index 14142cb4c8..cf350a06af 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -15,15 +15,14 @@ lattice = a * [[ 1.0 0.5 0.5]; [ 0.5 0.5 1.0]] pseudopotentials = PseudoFamily("dojo.nc.sr.pbe.v0_4_1.standard.upf") Ni = ElementPsp(:Ni, pseudopotentials) -O = ElementPsp(:O, pseudopotentials) +O = ElementPsp(:O, pseudopotentials) atoms = [Ni, O, Ni, O] positions = [zeros(3), ones(3) / 4, ones(3) / 2, ones(3) * 3 / 4] magnetic_moments = [2, 0, -1, 0] # First, we run an SCF and band computation without the Hubbard term -model = model_DFT(lattice, atoms, positions; temperature=5e-3, - functionals=PBE(), magnetic_moments=magnetic_moments) -basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2] ) +model = model_DFT(lattice, atoms, positions; temperature=5e-3, functionals=PBE(), magnetic_moments) +basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2]) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) bands = compute_bands(scfres, MonkhorstPack(4, 4, 4)) lowest_unocc_band = findfirst(ε -> ε-bands.εF > 0, bands.eigenvalues[1]) @@ -38,10 +37,10 @@ plot_pdos(bands; p, iatom=1, label="3D", colors=[:yellow, :orange], εrange) # To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant U = 10u"eV" -manifold = OrbitalManifold(;species=:Ni, label="3D") +manifold = OrbitalManifold(; species=:Ni, label="3D") -# Run SCF -model = model_DFT(lattice, atoms, positions; extra_terms=[DFTK.Hubbard(manifold, U)], +# Run SCF with a DFT+U setup, notice the `extra_terms` keyword argument, setting up the Hubbard +U term. +model = model_DFT(lattice, atoms, positions; extra_terms=[Hubbard(manifold, U)], functionals=PBE(), temperature=5e-3, magnetic_moments) basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2] ) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) @@ -57,4 +56,3 @@ band_gap = bands_hub.eigenvalues[1][lowest_unocc_band] - bands_hub.eigenvalues[1 εrange = (εF - austrip(width), εF + austrip(width)) p = plot_dos(bands_hub; p, colors=[:blue, :blue], εrange) plot_pdos(bands_hub; p, iatom=1, label="3D", colors=[:green, :purple], εrange) - From aecf0bd0da0e2ea54eefd992fbcb85cb34ccb86c Mon Sep 17 00:00:00 2001 From: "Michael F. Herbst" Date: Wed, 29 Oct 2025 08:58:10 +0100 Subject: [PATCH 38/50] Announce feature --- docs/src/features.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/features.md b/docs/src/features.md index fd2820cee2..b543594325 100644 --- a/docs/src/features.md +++ b/docs/src/features.md @@ -8,6 +8,7 @@ and runs out of the box on Linux, windows and macOS, see [Installation](@ref). ## Standard methods and models - DFT models (LDA, GGA, meta-GGA): Any functional from the [libxc](https://libxc.gitlab.io/) library +- DFT+U and Hubbard corrections - **Norm-conserving pseudopotentials**: Goedecker-type (GTH) or numerical (in UPF pseudopotential format), see [Pseudopotentials](@ref). @@ -25,7 +26,7 @@ and runs out of the box on Linux, windows and macOS, see [Installation](@ref). ## Ground-state properties and post-processing - Total energy, forces, stresses -- Density of states (DOS), local density of states (LDOS) +- Density of states (DOS), local density of states (LDOS), projected density of states (PDOS) - Band structures - Easy access to all intermediate quantities (e.g. density, Bloch waves) From 1d4d73c33a9e593128233e8f8d1d91da674e85f3 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Wed, 5 Nov 2025 16:51:18 +0100 Subject: [PATCH 39/50] Minor changes --- src/common/spherical_harmonics.jl | 10 +++++----- src/scf/self_consistent_field.jl | 8 ++++---- src/terms/hubbard.jl | 13 +++++++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/common/spherical_harmonics.jl b/src/common/spherical_harmonics.jl index 82291d5550..287a8521bb 100644 --- a/src/common/spherical_harmonics.jl +++ b/src/common/spherical_harmonics.jl @@ -61,16 +61,16 @@ function wigner_d_matrix(l::Integer, Wcart::AbstractMatrix{T}) where {T} return [one(T);;] end rng = Xoshiro(1234) - neq = 2*l+2 # We need at least 2*l+1 equations, we add one for numerical stability - B = Matrix{T}(undef, 2*l+1, neq) - A = Matrix{T}(undef, 2*l+1, neq) + neq = 2l+2 # We need at least 2l+1 equations, we add one for numerical stability + B = Matrix{T}(undef, 2l+1, neq) + A = Matrix{T}(undef, 2l+1, neq) for n in 1:neq r = rand(rng, T, 3) r = r / norm(r) r0 = Wcart * r for m in -l:l - B[m+l+1, n] = DFTK.ylm_real(l, m, r0) - A[m+l+1, n] = DFTK.ylm_real(l, m, r) + B[m+l+1, n] = ylm_real(l, m, r0) + A[m+l+1, n] = ylm_real(l, m, r) end end κ = cond(A) diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index 22002315ff..02527452dc 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -233,10 +233,10 @@ Overview of parameters: # Callback is run one last time with final state to allow callback to clean up scfres = (; ham, basis, energies, converged, nbandsalg.occupation_threshold, - ρ=ρout, τ, α=damping, eigenvalues, occupation, εF, info.n_bands_converge, - info.n_iter, info.n_matvec, ψ, nhubbard, info.diagonalization, stage=:finalize, - info.history_Δρ, info.history_Etot, info.timedout, mixing, seed, - runtime_ns=time_ns() - start_ns, algorithm="SCF") + ρ=ρout, τ, nhubbard, α=damping, eigenvalues, occupation, εF, + info.n_bands_converge, info.n_iter, info.n_matvec, ψ, info.diagonalization, + stage=:finalize, info.history_Δρ, info.history_Etot, info.timedout, mixing, + seed, runtime_ns=time_ns() - start_ns, algorithm="SCF") callback(scfres) scfres end diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 719e3d0ad0..286f0db397 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -49,7 +49,7 @@ Computes a matrix nhubbard of size (n_spin, natoms, natoms), where each entry nh where n or ibnd is the band index, ``weights[ik ibnd] = kweights[ik] * occupation[ik, ibnd]`` and ``Pᵢₘ₁`` is the pseudoatomic orbital projector for atom i and orbital m₁ - (usually just the magnetic quantum number, since l is usually fixed). + (just the magnetic quantum number, since l is fixed, as is usual in the literature). For details on the projectors see `atomic_orbital_projectors`. Overview of inputs: @@ -60,9 +60,8 @@ Overview of inputs: - `labels` (kwarg): Vector of NamedTuples. Each projectors[ik][:,iproj] column has all relevant chemical information stored in the corresponding labels[iproj] NamedTuple. -Overviw of outputs: -- `nhubbard`: 3-tensor of matrices. Outer indices select spin, iatom and jatom, - inner indices select m1 and m2 in the manifold. +Overview of outputs: +- `nhubbard`: 3-tensor of matrices. See above for details. """ function compute_nhubbard(manifold::OrbitalManifold, basis::PlaneWaveBasis{T}, @@ -95,6 +94,12 @@ function compute_nhubbard(manifold::OrbitalManifold, basis.symmetries, basis.model.positions[manifold_atoms]) end +""" +This function reshapes for each kpoint the projectors matrix to a vector of matrices, + taking only the columns corresponding to orbitals in the manifold and splitting them + into different matrices, one for each atom. Columns in the same matrix differ only in + the value of the magnetic quantum number m of the corresponding orbitals. +""" function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} manifold_atoms = findall(at -> at.species == manifold.species, basis.model.atoms) From 15304e7fc810040b742e1ba7a48f5671c439546f Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Mon, 10 Nov 2025 23:47:17 +0100 Subject: [PATCH 40/50] Modifications to the OrbitalManifold interface --- examples/hubbard.jl | 2 +- ext/DFTKPlotsExt.jl | 7 ++-- src/postprocess/dos.jl | 4 +-- src/terms/hubbard.jl | 76 +++++++++++++++++++++++++++++++----------- test/hubbard.jl | 10 +++--- 5 files changed, 68 insertions(+), 31 deletions(-) diff --git a/examples/hubbard.jl b/examples/hubbard.jl index 14142cb4c8..82d627c397 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -38,7 +38,7 @@ plot_pdos(bands; p, iatom=1, label="3D", colors=[:yellow, :orange], εrange) # To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant U = 10u"eV" -manifold = OrbitalManifold(;species=:Ni, label="3D") +manifold = OrbitalManifold([1,3], Ni.psp, 2, 1) # Run SCF model = model_DFT(lattice, atoms, positions; extra_terms=[DFTK.Hubbard(manifold, U)], diff --git a/ext/DFTKPlotsExt.jl b/ext/DFTKPlotsExt.jl index 8e516e1baf..3a1d8a7c28 100644 --- a/ext/DFTKPlotsExt.jl +++ b/ext/DFTKPlotsExt.jl @@ -142,7 +142,7 @@ function plot_ldos(basis, eigenvalues, ψ; εF=nothing, unit=u"hartree", end plot_ldos(scfres; kwargs...) = plot_ldos(scfres.basis, scfres.eigenvalues, scfres.ψ; scfres.εF, kwargs...) -function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom, label=nothing, +function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatoms, label=nothing, positions=basis.model.positions, εF=nothing, unit=u"hartree", temperature=basis.model.temperature, @@ -155,16 +155,17 @@ function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom, label=nothi n_spin = basis.model.n_spin_components to_unit = ustrip(auconvert(unit, 1.0)) - species = isnothing(iatom) ? "all atoms" : "atom $(iatom) ($(basis.model.atoms[iatom].species))" + species = isnothing(iatoms) ? "all atoms" : "atoms $(iatoms) ($(basis.model.atoms[iatoms].species))" orb_name = isnothing(label) ? "all orbitals" : label # Plot pdos isnothing(p) && (p = Plots.plot()) p = Plots.plot(p; kwargs...) spinlabels = spin_components(basis.model) + labels = basis.terms[findfirst(term -> isa(term, DFTK.TermHubbard), basis.terms)].labels pdos = DFTK.sum_pdos(compute_pdos(εs, basis, ψ, eigenvalues; positions, temperature, smearing), - [OrbitalManifold(;iatom, label)]) + [OrbitalManifold(basis.model.atoms, labels; iatoms, label)]) for σ = 1:n_spin plot_label = n_spin > 1 ? "$(species) $(orb_name) $(spinlabels[σ]) spin" : "$(species) $(orb_name)" Plots.plot!(p, (εs .- eshift) .* to_unit, pdos[:, σ]; label=plot_label, color=colors[σ]) diff --git a/src/postprocess/dos.jl b/src/postprocess/dos.jl index 8e250abd08..c32288f76e 100644 --- a/src/postprocess/dos.jl +++ b/src/postprocess/dos.jl @@ -219,7 +219,7 @@ This function extracts and sums up all the PDOSes, directly from the output of t Overview of inputs: - `pdos_res`: Whole output from compute_pdos. -- `manifolds`: Vector of OrbitalManifolds to select the desired projectors pdos. +- `manifolds`: Vector of (;iatoms, label) NamedTuples to select the desired projectors pdos. Overview of outputs: - `pdos`: Vector containing the pdos(ε). @@ -228,7 +228,7 @@ function sum_pdos(pdos_res, manifolds::AbstractVector) pdos = zeros(Float64, length(pdos_res.εs), size(pdos_res.pdos, 3)) for σ in 1:size(pdos_res.pdos, 3) for (j, orb) in enumerate(pdos_res.projector_labels) - if any(manifold(orb) for manifold in manifolds) + if any(is_on_manifold(orb, manifold) for manifold in manifolds) pdos[:, σ] += pdos_res.pdos[:, j, σ] end end diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 286f0db397..e87e72beac 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -13,24 +13,61 @@ All fields are optional, only the given ones will be used for selection. Can be called with an orbital NamedTuple and returns a boolean stating whether the orbital belongs to the manifold. """ +#@kwdef struct OrbitalManifold +# iatom ::Union{Int64, Nothing} = nothing +# species ::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing} = nothing +# label ::Union{String, Nothing} = nothing +#end +#function (s::OrbitalManifold)(orbital) +# iatom_match = isnothing(s.iatom) || (s.iatom == orbital.iatom) +# species_match = isnothing(s.species) || (s.species == orbital.species) +# label_match = isnothing(s.label) || (s.label == orbital.label) +# iatom_match && species_match && label_match +#end @kwdef struct OrbitalManifold - iatom ::Union{Int64, Nothing} = nothing - species ::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing} = nothing - label ::Union{String, Nothing} = nothing + iatoms::Union{Vector{Int64}, Nothing} = nothing + psp = nothing + projector_l::Union{Int64, Nothing} = nothing + projector_i::Union{Int64, Nothing} = nothing end -function (s::OrbitalManifold)(orbital) - iatom_match = isnothing(s.iatom) || (s.iatom == orbital.iatom) - species_match = isnothing(s.species) || (s.species == orbital.species) - label_match = isnothing(s.label) || (s.label == orbital.label) - iatom_match && species_match && label_match +function is_on_manifold(orbital; iatoms=nothing, species=nothing, + l=nothing, n=nothing, label=nothing) + iatom_match = isnothing(iatoms) || (orbital.iatom in iatoms) + species_match = isnothing(species) || (species == orbital.species) + label_match = isnothing(label) || (label == orbital.label) + l_match = isnothing(l) || (l == orbital.l) + n_match = isnothing(n) || (n == orbital.n) + iatom_match && species_match && l_match && n_match && label_match +end +function is_on_manifold(orbital, manifold::OrbitalManifold) + is_on_manifold(orbital; iatoms=manifold.iatoms, l=manifold.projector_l, n=manifold.projector_i) +end + +function OrbitalManifold(atoms, labels; iatoms::Union{Vector{Int64}, Nothing}=nothing, + label::Union{String, Nothing}=nothing, + species::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing}=nothing) + hub_atoms = Int64[] + for orbital in labels + if is_on_manifold(orbital; iatoms, species, label) + append!(hub_atoms, orbital.iatom) + end + end + isempty(hub_atoms) && error("Unable to create Hubbard manifold. No atom matches the given keywords") + # If species is nothing, there can be errors if the iatoms correspond to different atomic species + model_atom = atoms[hub_atoms[1]] + !all(atom -> atom.psp == model_atom.psp, atoms[hub_atoms]) && + error("The given Hubbard manifold contains more than one atomic pseudopotential species") + projector_l = isnothing(label) ? nothing : labels[hub_atoms[1]].l + projector_i = isnothing(label) ? nothing : labels[hub_atoms[1]].n + OrbitalManifold(;iatoms=hub_atoms, psp=model_atom.psp, projector_l, projector_i) end function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, manifold::OrbitalManifold) where {T} # We extract the labels only for orbitals belonging to the manifold - manifold_labels = filter(manifold, labels) - isempty(manifold_labels) && @warn "Projector for $(manifold) not found." - proj_indices = findall(orb -> manifold(orb), labels) + proj_indices = findall(orb -> is_on_manifold(orb, manifold), labels) + isempty(proj_indices) && @warn "Projector for $(manifold) not found." + manifold_labels = labels[proj_indices] manifold_projectors = map(enumerate(projectors)) do (ik, projk) projk[:, proj_indices] end @@ -71,7 +108,7 @@ function compute_nhubbard(manifold::OrbitalManifold, filled_occ = filled_occupation(basis.model) n_spin = basis.model.n_spin_components - manifold_atoms = findall(at -> at.species == manifold.species, basis.model.atoms) + manifold_atoms = findall(at -> at.psp == manifold.psp, basis.model.atoms) natoms = length(manifold_atoms) # Number of atoms of the species in the manifold l = labels[1].l projectors = reshape_hubbard_proj(basis, projectors, labels, manifold) @@ -102,11 +139,11 @@ This function reshapes for each kpoint the projectors matrix to a vector of matr """ function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} - manifold_atoms = findall(at -> at.species == manifold.species, basis.model.atoms) + manifold_atoms = findall(at -> at.psp == manifold.psp, basis.model.atoms) natoms = length(manifold_atoms) nprojs = length(labels) l = labels[1].l - @assert all(label -> label.l==l, labels) + @assert all(label -> label.l==l, labels) "$(labels)" @assert length(labels) == natoms * (2*l+1) p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for i in 1:length(projectors)] for (idx, iatom) in enumerate(manifold_atoms) @@ -130,11 +167,10 @@ Hubbard energy: struct Hubbard{T} manifold::OrbitalManifold U::T - function Hubbard(manifold::OrbitalManifold, U) - if isnothing(manifold.label) || isnothing(manifold.species) - error("Hubbard term needs both a species and a label inside OrbitalManifold") - elseif !isnothing(manifold.iatom) - error("Hubbard term does not support iatom specification inside OrbitalManifold") + function Hubbard((manifold::OrbitalManifold), U) + if isnothing(manifold.iatoms) || isnothing(manifold.projector_l) || + isnothing(manifold.projector_i) + error("Hubbard term needs specification of atoms and orbital") end U = austrip(U) new{typeof(U)}(manifold, U) @@ -164,7 +200,7 @@ end proj = term.P filled_occ = filled_occupation(basis.model) - types = findall(at -> at.species == term.manifold.species, basis.model.atoms) + types = findall(at -> at.psp == term.manifold.psp, basis.model.atoms) natoms = length(types) n_spin = basis.model.n_spin_components nproj_atom = size(nhubbard[1,1,1], 1) # This is the number of projectors per atom, namely 2l+1 diff --git a/test/hubbard.jl b/test/hubbard.jl index 5cc169e38f..cadc67a4ec 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -48,10 +48,6 @@ end using UnitfulAtomic using LinearAlgebra - # Hubbard parameters - U = 10u"eV" - manifold = OrbitalManifold(;species=:Ni, label="3D") - a = 7.9 # Bohr lattice = a * [[ 1.0 0.5 0.5]; [ 0.5 1.0 0.5]; @@ -65,9 +61,13 @@ end [0.5, 0.5, 0.5], [0.75, 0.75, 0.75]] magnetic_moments = [2, 0, -1, 0] + + # Hubbard parameters + U = 10u"eV" + manifold = OrbitalManifold([1,3], Ni.psp, 2, 1) model = model_DFT(lattice, atoms, positions; - extra_terms=[DFTK.Hubbard(manifold, U)], + extra_terms=[Hubbard(manifold, U)], temperature=0.01, functionals=PBE(), smearing=DFTK.Smearing.Gaussian(), magnetic_moments=magnetic_moments) basis = PlaneWaveBasis(model; Ecut = 15, kgrid = [2, 2, 2]) From f649481728a770eb293cb4eaafe846ef8fb1fd15 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Mon, 10 Nov 2025 23:58:41 +0100 Subject: [PATCH 41/50] Name 'nhubbard' converted to 'hubbard_n' --- src/postprocess/band_structure.jl | 8 ++--- src/scf/self_consistent_field.jl | 20 ++++++------ src/symmetry.jl | 14 ++++---- src/terms/hubbard.jl | 49 ++++++++++++---------------- src/workarounds/forwarddiff_rules.jl | 4 +-- test/hubbard.jl | 4 +-- 6 files changed, 45 insertions(+), 54 deletions(-) diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index a3f6207346..388699c1cd 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -15,7 +15,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): kgrid::Union{AbstractKgrid,AbstractKgridGenerator}; n_bands=default_n_bands_bandstructure(basis.model), n_extra=3, ρ=nothing, τ=nothing, εF=nothing, - occupation=nothing, nhubbard=nothing, + occupation=nothing, hubbard_n=nothing, eigensolver=lobpcg_hyper, tol=1e-3, seed=nothing, kwargs...) # kcoords are the kpoint coordinates in fractional coordinates @@ -37,7 +37,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): # Create new basis with new kpoints bs_basis = PlaneWaveBasis(basis, kgrid) - ham = Hamiltonian(bs_basis; ρ, τ, nhubbard, occupation) + ham = Hamiltonian(bs_basis; ρ, τ, hubbard_n, occupation) eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands + n_extra; n_conv_check=n_bands, tol, kwargs...) if !eigres.converged @@ -69,9 +69,9 @@ function compute_bands(scfres::NamedTuple, kgrid::Union{AbstractKgrid,AbstractKgridGenerator}; n_bands=default_n_bands_bandstructure(scfres), kwargs...) τ = haskey(scfres, :τ) ? scfres.τ : nothing - nhubbard = haskey(scfres, :nhubbard) ? scfres.nhubbard : nothing + hubbard_n = haskey(scfres, :hubbard_n) ? scfres.hubbard_n : nothing compute_bands(scfres.basis, kgrid; - scfres.ρ, τ, nhubbard, scfres.occupation, + scfres.ρ, τ, hubbard_n, scfres.occupation, scfres.εF, n_bands, kwargs...) end diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index 02527452dc..e5eae6228f 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -131,7 +131,7 @@ Overview of parameters: basis::PlaneWaveBasis{T}; ρ=guess_density(basis), τ=any(needs_τ, basis.terms) ? zero(ρ) : nothing, - nhubbard=nothing, + hubbard_n=nothing, ψ=nothing, tol=1e-6, is_converged=ScfConvergenceDensity(tol), @@ -160,12 +160,12 @@ Overview of parameters: # We do density mixing in the real representation # TODO support other mixing types function fixpoint_map(ρin, info) - (; ψ, occupation, eigenvalues, εF, n_iter, converged, timedout, τ, nhubbard) = info + (; ψ, occupation, eigenvalues, εF, n_iter, converged, timedout, τ, hubbard_n) = info n_iter += 1 # Note that ρin is not the density of ψ, and the eigenvalues # are not the self-consistent ones, which makes this energy non-variational - energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρin, τ, nhubbard, eigenvalues, εF) + energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρin, τ, hubbard_n, eigenvalues, εF) # Diagonalize `ham` to get the new state nextstate = next_density(ham, nbandsalg, fermialg; eigensolver, ψ, eigenvalues, @@ -183,20 +183,20 @@ Overview of parameters: end for term in basis.terms if isa(term, DFTK.TermHubbard) - nhubbard = compute_nhubbard(term.manifold, basis, ψ, occupation; + hubbard_n = compute_hubbard_n(term.manifold, basis, ψ, occupation; projectors=term.P, labels=term.labels) end end # Update info with results gathered so far info_next = (; ham, basis, converged, stage=:iterate, algorithm="SCF", - ρin, τ, nhubbard, α=damping, n_iter, nbandsalg.occupation_threshold, + ρin, τ, hubbard_n, α=damping, n_iter, nbandsalg.occupation_threshold, seed, runtime_ns=time_ns() - start_ns, nextstate..., diagonalization=[nextstate.diagonalization]) # Compute the energy of the new state if compute_consistent_energies - (; energies) = energy(basis, ψ, occupation; ρ=ρout, τ, nhubbard, eigenvalues, εF) + (; energies) = energy(basis, ψ, occupation; ρ=ρout, τ, hubbard_n, eigenvalues, εF) end history_Etot = vcat(info.history_Etot, energies.total) history_Δρ = vcat(info.history_Δρ, norm(Δρ) * sqrt(basis.dvol)) @@ -218,7 +218,7 @@ Overview of parameters: ρnext, info_next end - info_init = (; ρin=ρ, τ, nhubbard, ψ, occupation=nothing, eigenvalues=nothing, εF=nothing, + info_init = (; ρin=ρ, τ, hubbard_n, ψ, occupation=nothing, eigenvalues=nothing, εF=nothing, n_iter=0, n_matvec=0, timedout=false, converged=false, history_Etot=T[], history_Δρ=T[]) @@ -228,12 +228,12 @@ Overview of parameters: # We do not use the return value of solver but rather the one that got updated by fixpoint_map # ψ is consistent with ρout, so we return that. We also perform a last energy computation # to return a correct variational energy - (; ρin, ρout, τ, nhubbard, ψ, occupation, eigenvalues, εF, converged) = info - energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρout, τ, nhubbard, eigenvalues, εF) + (; ρin, ρout, τ, hubbard_n, ψ, occupation, eigenvalues, εF, converged) = info + energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρout, τ, hubbard_n, eigenvalues, εF) # Callback is run one last time with final state to allow callback to clean up scfres = (; ham, basis, energies, converged, nbandsalg.occupation_threshold, - ρ=ρout, τ, nhubbard, α=damping, eigenvalues, occupation, εF, + ρ=ρout, τ, hubbard_n, α=damping, eigenvalues, occupation, εF, info.n_bands_converge, info.n_iter, info.n_matvec, ψ, info.diagonalization, stage=:finalize, info.history_Δρ, info.history_Etot, info.timedout, mixing, seed, runtime_ns=time_ns() - start_ns, algorithm="SCF") diff --git a/src/symmetry.jl b/src/symmetry.jl index 3e065f3eca..a410c8af47 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -425,26 +425,26 @@ end """ Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ -function symmetrize_nhubbard(nhubbard::Array{Matrix{Complex{T}}}, +function symmetrize_hubbard_n(hubbard_n::Array{Matrix{Complex{T}}}, model, symmetries, positions) where {T} # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) # WARNING: To implement +V this will need to be changed! - nspins = size(nhubbard, 1) - natoms = size(nhubbard, 2) + nspins = size(hubbard_n, 1) + natoms = size(hubbard_n, 2) nsym = length(symmetries) # We extract the l value from the manifold size per atom (2l+1) - l = Int((size(nhubbard[1, 1, 1], 1)-1)/2) + l = Int((size(hubbard_n[1, 1, 1], 1)-1)/2) ldim = 2*l+1 - # Initialize the nhubbard matrix + # Initialize the hubbard_n matrix ns = [zeros(Complex{T}, ldim, ldim) for _ in 1:nspins, _ in 1:natoms, _ in 1:natoms] for symmetry in symmetries Wcart = model.lattice * symmetry.W * model.inv_lattice WigD = wigner_d_matrix(l, Wcart) for σ in 1:nspins, iatom in 1:natoms sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry) - ns[σ, iatom, iatom] .+= WigD' * nhubbard[σ, sym_atom, sym_atom] * WigD + ns[σ, iatom, iatom] .+= WigD' * hubbard_n[σ, sym_atom, sym_atom] * WigD end end ns ./= nsym @@ -523,7 +523,7 @@ function unfold_bz(scfres) eigenvalues = unfold_array(scfres.basis, basis_unfolded, scfres.eigenvalues, false) occupation = unfold_array(scfres.basis, basis_unfolded, scfres.occupation, false) energies, ham = energy_hamiltonian(basis_unfolded, ψ, occupation; - scfres.ρ, scfres.nhubbard, eigenvalues, scfres.εF) + scfres.ρ, scfres.hubbard_n, eigenvalues, scfres.εF) @assert energies.total ≈ scfres.energies.total new_scfres = (; basis=basis_unfolded, ψ, ham, eigenvalues, occupation) merge(scfres, new_scfres) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index e87e72beac..eda092f452 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -2,7 +2,9 @@ using LinearAlgebra using Random """ -Structure for manifold choice and projectors extraction. +Structure for Hubbard manifold choice and projectors extraction. + It is to be noted that, despite the name used in literature, this is + not a manifold in the mathematical sense. Overview of fields: - `iatom`: Atom position in the atoms array. @@ -13,17 +15,6 @@ All fields are optional, only the given ones will be used for selection. Can be called with an orbital NamedTuple and returns a boolean stating whether the orbital belongs to the manifold. """ -#@kwdef struct OrbitalManifold -# iatom ::Union{Int64, Nothing} = nothing -# species ::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing} = nothing -# label ::Union{String, Nothing} = nothing -#end -#function (s::OrbitalManifold)(orbital) -# iatom_match = isnothing(s.iatom) || (s.iatom == orbital.iatom) -# species_match = isnothing(s.species) || (s.species == orbital.species) -# label_match = isnothing(s.label) || (s.label == orbital.label) -# iatom_match && species_match && label_match -#end @kwdef struct OrbitalManifold iatoms::Union{Vector{Int64}, Nothing} = nothing psp = nothing @@ -75,14 +66,14 @@ function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, end """ - compute_nhubbard(manifold, basis, ψ, occupation; [projectors, labels, positions]) + compute_hubbard_n(manifold, basis, ψ, occupation; [projectors, labels, positions]) -Computes a matrix nhubbard of size (n_spin, natoms, natoms), where each entry nhubbard[iatom, jatom] +Computes a matrix hubbard_n of size (n_spin, natoms, natoms), where each entry hubbard_n[iatom, jatom] contains the submatrix of the occupation matrix corresponding to the projectors of atom iatom and atom jatom, with dimensions determined by the number of projectors for each atom. The atoms and orbitals are defined by the manifold tuple. - nhubbard[σ, iatom, jatom][m1, m2] = Σₖ₍ₛₚᵢₙ₎Σₙ weights[ik, ibnd] * ψₙₖ' * Pᵢₘ₁ * Pᵢₘ₂' * ψₙₖ + hubbard_n[σ, iatom, jatom][m1, m2] = Σₖ₍ₛₚᵢₙ₎Σₙ weights[ik, ibnd] * ψₙₖ' * Pᵢₘ₁ * Pᵢₘ₂' * ψₙₖ where n or ibnd is the band index, ``weights[ik ibnd] = kweights[ik] * occupation[ik, ibnd]`` and ``Pᵢₘ₁`` is the pseudoatomic orbital projector for atom i and orbital m₁ @@ -98,9 +89,9 @@ Overview of inputs: chemical information stored in the corresponding labels[iproj] NamedTuple. Overview of outputs: -- `nhubbard`: 3-tensor of matrices. See above for details. +- `hubbard_n`: 3-tensor of matrices. See above for details. """ -function compute_nhubbard(manifold::OrbitalManifold, +function compute_hubbard_n(manifold::OrbitalManifold, basis::PlaneWaveBasis{T}, ψ, occupation; projectors, labels, @@ -112,22 +103,22 @@ function compute_nhubbard(manifold::OrbitalManifold, natoms = length(manifold_atoms) # Number of atoms of the species in the manifold l = labels[1].l projectors = reshape_hubbard_proj(basis, projectors, labels, manifold) - nhubbard = Array{Matrix{Complex{T}}}(undef, n_spin, natoms, natoms) + hubbard_n = Array{Matrix{Complex{T}}}(undef, n_spin, natoms, natoms) for σ in 1:n_spin for idx in 1:length(manifold_atoms), jdx in 1:length(manifold_atoms) - nhubbard[σ, idx, jdx] = zeros(Complex{T}, 2*l+1, 2*l+1) + hubbard_n[σ, idx, jdx] = zeros(Complex{T}, 2*l+1, 2*l+1) for ik = krange_spin(basis, σ) j_projection = ψ[ik]' * projectors[ik][jdx] # <ψ|ϕJ> i_projection = projectors[ik][idx]' * ψ[ik] # <ϕI|ψ> # Sums over the bands, dividing by filled_occ to deal # with the physical two spin channels separately - nhubbard[σ, idx, jdx] .+= (basis.kweights[ik] * i_projection * + hubbard_n[σ, idx, jdx] .+= (basis.kweights[ik] * i_projection * diagm(occupation[ik]/filled_occ) * j_projection) end - nhubbard[σ, idx, jdx] = mpi_sum(nhubbard[σ, idx, jdx], basis.comm_kpts) + hubbard_n[σ, idx, jdx] = mpi_sum(hubbard_n[σ, idx, jdx], basis.comm_kpts) end end - nhubbard = symmetrize_nhubbard(nhubbard, basis.model, + hubbard_n = symmetrize_hubbard_n(hubbard_n, basis.model, basis.symmetries, basis.model.positions[manifold_atoms]) end @@ -161,7 +152,7 @@ end @doc raw""" Hubbard energy: ```math -1/2 Σ_{σI} U * Tr[nhubbard[σ,i,i] * (1 - nhubbard[σ,i,i])] +1/2 Σ_{σI} U * Tr[hubbard_n[σ,i,i] * (1 - hubbard_n[σ,i,i])] ``` """ struct Hubbard{T} @@ -191,10 +182,10 @@ end @timing "ene_ops: hubbard" function ene_ops(term::TermHubbard, basis::PlaneWaveBasis{T}, - ψ, occupation; nhubbard=nothing, + ψ, occupation; hubbard_n=nothing, labels=term.labels, kwargs...) where {T} - if isnothing(nhubbard) + if isnothing(hubbard_n) return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end proj = term.P @@ -203,15 +194,15 @@ end types = findall(at -> at.psp == term.manifold.psp, basis.model.atoms) natoms = length(types) n_spin = basis.model.n_spin_components - nproj_atom = size(nhubbard[1,1,1], 1) # This is the number of projectors per atom, namely 2l+1 - # For the ops we have to reshape nhubbard to match the NonlocalOperator structure, using a block diagonal form + nproj_atom = size(hubbard_n[1,1,1], 1) # This is the number of projectors per atom, namely 2l+1 + # For the ops we have to reshape hubbard_n to match the NonlocalOperator structure, using a block diagonal form nhub = [zeros(Complex{T}, nproj_atom*natoms, nproj_atom*natoms) for _ in 1:n_spin] E = zero(T) for σ in 1:n_spin, iatom in 1:natoms proj_range = (1+nproj_atom*(iatom-1)):(nproj_atom*iatom) - nhub[σ][proj_range, proj_range] = nhubbard[σ, iatom, iatom] + nhub[σ][proj_range, proj_range] = hubbard_n[σ, iatom, iatom] E += filled_occ * 1/T(2) * term.U * - real(tr(nhubbard[σ, iatom, iatom] * (I - nhubbard[σ, iatom, iatom]))) + real(tr(hubbard_n[σ, iatom, iatom] * (I - hubbard_n[σ, iatom, iatom]))) end ops = [NonlocalOperator(basis, kpt, proj[ik], 1/T(2) * term.U * (I - 2*nhub[kpt.spin])) for (ik,kpt) in enumerate(basis.kpoints)] diff --git a/src/workarounds/forwarddiff_rules.jl b/src/workarounds/forwarddiff_rules.jl index ef3875ca8d..e8b8ea389a 100644 --- a/src/workarounds/forwarddiff_rules.jl +++ b/src/workarounds/forwarddiff_rules.jl @@ -275,8 +275,8 @@ end # This has to be changed whenever the scfres structure changes (; ham, basis=basis_dual, energies, ρ, eigenvalues, occupation, εF, ψ, - scfres.τ, # TODO make τ and nhubbard also differentiable for meta-GGA DFPT - scfres.nhubbard, + scfres.τ, # TODO make τ and hubbard_n also differentiable for meta-GGA DFPT + scfres.hubbard_n, # non-differentiable metadata: response=getfield.(δresults, :info_gmres), scfres.converged, scfres.occupation_threshold, scfres.α, scfres.n_iter, diff --git a/test/hubbard.jl b/test/hubbard.jl index cadc67a4ec..30e16a953b 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -86,12 +86,12 @@ end # The unfolding of the kpoints is not supported with MPI if mpi_nprocs() == 1 @testset "Test symmetry consistency" begin - n_hub = scfres.nhubbard + n_hub = scfres.hubbard_n scfres_nosym = DFTK.unfold_bz(scfres) term_idx = findfirst(term -> isa(term, DFTK.TermHubbard), scfres_nosym.basis.terms) term_hub = scfres_nosym.basis.terms[term_idx] - nhub_nosym = DFTK.compute_nhubbard(manifold, scfres_nosym.basis, + nhub_nosym = DFTK.compute_hubbard_n(manifold, scfres_nosym.basis, scfres_nosym.ψ, scfres_nosym.occupation; projectors=term_hub.P, labels=term_hub.labels) From 5c05a9f91ed10eff8010cb648c397eaddca4849f Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Tue, 11 Nov 2025 17:01:04 +0100 Subject: [PATCH 42/50] New constructors added for OrbitalManifold. Testing --- examples/collinear_magnetism.jl | 2 +- examples/dos.jl | 4 ++-- examples/hubbard.jl | 4 ++-- ext/DFTKPlotsExt.jl | 2 +- src/postprocess/dos.jl | 2 +- src/pseudo/PspUpf.jl | 8 +++++++ src/scf/self_consistent_field.jl | 2 +- src/symmetry.jl | 4 ++-- src/terms/hubbard.jl | 36 +++++++++++++++++++------------- test/hamiltonian_consistency.jl | 2 +- test/hubbard.jl | 8 +++---- 11 files changed, 45 insertions(+), 29 deletions(-) diff --git a/examples/collinear_magnetism.jl b/examples/collinear_magnetism.jl index 8bb9fbd95e..4d0a55670d 100644 --- a/examples/collinear_magnetism.jl +++ b/examples/collinear_magnetism.jl @@ -109,7 +109,7 @@ plot_dos(bands_666) # is sufficient. # We can clearly see that the origin of this spin-polarization traces back to the # 3D orbital contribution if we look at the corresponding projected density of states (PDOS). -plot_pdos(bands_666; iatom=1, label="3D") +plot_pdos(bands_666; iatoms=[1], label="3D") # Similarly the band structure shows clear differences between both spin components. using Unitful diff --git a/examples/dos.jl b/examples/dos.jl index b3cf17bd87..043bf7b773 100644 --- a/examples/dos.jl +++ b/examples/dos.jl @@ -28,5 +28,5 @@ plot_dos(scfres) plot_ldos(scfres; n_points=100, ldos_xyz=[:, 10, 10]) # Plot the projected DOS -p = plot_pdos(scfres; iatom=1, label="3S", εrange=(-0.3, 0.5)) -plot_pdos(scfres; p, colors=[:red], iatom=1, label="3P", εrange=(-0.3, 0.5)) +p = plot_pdos(scfres; iatoms=[1], label="3S", εrange=(-0.3, 0.5)) +plot_pdos(scfres; p, colors=[:red], iatoms=[1], label="3P", εrange=(-0.3, 0.5)) diff --git a/examples/hubbard.jl b/examples/hubbard.jl index add442ca68..ba618512eb 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -33,7 +33,7 @@ band_gap = bands.eigenvalues[1][lowest_unocc_band] - bands.eigenvalues[1][lowest width = 5.0u"eV" εrange = (εF - austrip(width), εF + austrip(width)) p = plot_dos(bands; εrange, colors=[:red, :red]) -plot_pdos(bands; p, iatom=1, label="3D", colors=[:yellow, :orange], εrange) +plot_pdos(bands; p, iatoms=[1], label="3D", colors=[:yellow, :orange], εrange) # To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant U = 10u"eV" @@ -55,4 +55,4 @@ band_gap = bands_hub.eigenvalues[1][lowest_unocc_band] - bands_hub.eigenvalues[1 εF = bands_hub.εF εrange = (εF - austrip(width), εF + austrip(width)) p = plot_dos(bands_hub; p, colors=[:blue, :blue], εrange) -plot_pdos(bands_hub; p, iatom=1, label="3D", colors=[:green, :purple], εrange) +plot_pdos(bands_hub; p, iatoms=[1], label="3D", colors=[:green, :purple], εrange) diff --git a/ext/DFTKPlotsExt.jl b/ext/DFTKPlotsExt.jl index 3a1d8a7c28..cc2f63ceae 100644 --- a/ext/DFTKPlotsExt.jl +++ b/ext/DFTKPlotsExt.jl @@ -142,7 +142,7 @@ function plot_ldos(basis, eigenvalues, ψ; εF=nothing, unit=u"hartree", end plot_ldos(scfres; kwargs...) = plot_ldos(scfres.basis, scfres.eigenvalues, scfres.ψ; scfres.εF, kwargs...) -function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatoms, label=nothing, +function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatoms=nothing, label=nothing, positions=basis.model.positions, εF=nothing, unit=u"hartree", temperature=basis.model.temperature, diff --git a/src/postprocess/dos.jl b/src/postprocess/dos.jl index c32288f76e..1d07210e3c 100644 --- a/src/postprocess/dos.jl +++ b/src/postprocess/dos.jl @@ -219,7 +219,7 @@ This function extracts and sums up all the PDOSes, directly from the output of t Overview of inputs: - `pdos_res`: Whole output from compute_pdos. -- `manifolds`: Vector of (;iatoms, label) NamedTuples to select the desired projectors pdos. +- `manifolds`: Vector of OrbitalManifolds to select the desired projectors pdos. Overview of outputs: - `pdos`: Vector containing the pdos(ε). diff --git a/src/pseudo/PspUpf.jl b/src/pseudo/PspUpf.jl index 7df3f0bab3..b3486df67a 100644 --- a/src/pseudo/PspUpf.jl +++ b/src/pseudo/PspUpf.jl @@ -175,6 +175,14 @@ count_n_pswfc_radial(psp::PspUpf, l) = length(psp.r2_pswfcs[l+1]) pswfc_label(psp::PspUpf, i, l) = psp.pswfc_labels[l+1][i] +function pswfc_indices(psp::PspUpf, label) + for l in 0:psp.lmax, n in 1:DFTK.count_n_pswfc_radial(psp, l) + if (DFTK.pswfc_label(psp, n, l) == label) + return (; l, n) + end + end +end + function eval_psp_pswfc_real(psp::PspUpf, i, l, r::T)::T where {T<:Real} psp.r2_pswfcs_interp[l+1][i](r) / r^2 # TODO if r is below a threshold, return zero end diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index e5eae6228f..2e7434970c 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -184,7 +184,7 @@ Overview of parameters: for term in basis.terms if isa(term, DFTK.TermHubbard) hubbard_n = compute_hubbard_n(term.manifold, basis, ψ, occupation; - projectors=term.P, labels=term.labels) + projectors=term.P, labels=term.labels) end end diff --git a/src/symmetry.jl b/src/symmetry.jl index a410c8af47..debe8e9238 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -426,7 +426,7 @@ end Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ function symmetrize_hubbard_n(hubbard_n::Array{Matrix{Complex{T}}}, - model, symmetries, positions) where {T} + model, symmetries, positions) where {T} # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) # WARNING: To implement +V this will need to be changed! @@ -434,7 +434,7 @@ function symmetrize_hubbard_n(hubbard_n::Array{Matrix{Complex{T}}}, natoms = size(hubbard_n, 2) nsym = length(symmetries) # We extract the l value from the manifold size per atom (2l+1) - l = Int((size(hubbard_n[1, 1, 1], 1)-1)/2) + l = div(size(hubbard_n[1, 1, 1], 1)-1,2) ldim = 2*l+1 # Initialize the hubbard_n matrix diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index eda092f452..42c4b08dbe 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -16,11 +16,17 @@ Can be called with an orbital NamedTuple and returns a boolean stating whether the orbital belongs to the manifold. """ @kwdef struct OrbitalManifold - iatoms::Union{Vector{Int64}, Nothing} = nothing psp = nothing - projector_l::Union{Int64, Nothing} = nothing - projector_i::Union{Int64, Nothing} = nothing + iatoms::Union{Vector{Int64}, Nothing} = nothing + l::Union{Int64, Nothing} = nothing + i::Union{Int64, Nothing} = nothing end +OrbitalManifold(atom::ElementPsp, iatoms::Vector{Int64}; l, i) = OrbitalManifold(atom.psp, iatoms, l, i) +function OrbitalManifold(atom::ElementPsp, iatoms::Vector{Int64}, label::String) + l, i = DFTK.pswfc_indices(atom.psp, label) + OrbitalManifold(atom.psp, iatoms, l, i) +end + function is_on_manifold(orbital; iatoms=nothing, species=nothing, l=nothing, n=nothing, label=nothing) iatom_match = isnothing(iatoms) || (orbital.iatom in iatoms) @@ -31,10 +37,12 @@ function is_on_manifold(orbital; iatoms=nothing, species=nothing, iatom_match && species_match && l_match && n_match && label_match end function is_on_manifold(orbital, manifold::OrbitalManifold) - is_on_manifold(orbital; iatoms=manifold.iatoms, l=manifold.projector_l, n=manifold.projector_i) + is_on_manifold(orbital; iatoms=manifold.iatoms, l=manifold.l, n=manifold.i) end -function OrbitalManifold(atoms, labels; iatoms::Union{Vector{Int64}, Nothing}=nothing, +function OrbitalManifold(atoms::Union{Vector{ElementPsp}, Vector{DFTK.Element}}, + labels; + iatoms::Union{Vector{Int64}, Nothing}=nothing, label::Union{String, Nothing}=nothing, species::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing}=nothing) hub_atoms = Int64[] @@ -48,9 +56,9 @@ function OrbitalManifold(atoms, labels; iatoms::Union{Vector{Int64}, Nothing}=no model_atom = atoms[hub_atoms[1]] !all(atom -> atom.psp == model_atom.psp, atoms[hub_atoms]) && error("The given Hubbard manifold contains more than one atomic pseudopotential species") - projector_l = isnothing(label) ? nothing : labels[hub_atoms[1]].l - projector_i = isnothing(label) ? nothing : labels[hub_atoms[1]].n - OrbitalManifold(;iatoms=hub_atoms, psp=model_atom.psp, projector_l, projector_i) + l = isnothing(label) ? nothing : labels[hub_atoms[1]].l + i = isnothing(label) ? nothing : labels[hub_atoms[1]].n + OrbitalManifold(;iatoms=hub_atoms, psp=model_atom.psp, l, i) end function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, @@ -92,10 +100,10 @@ Overview of outputs: - `hubbard_n`: 3-tensor of matrices. See above for details. """ function compute_hubbard_n(manifold::OrbitalManifold, - basis::PlaneWaveBasis{T}, - ψ, occupation; - projectors, labels, - positions = basis.model.positions) where {T} + basis::PlaneWaveBasis{T}, + ψ, occupation; + projectors, labels, + positions = basis.model.positions) where {T} filled_occ = filled_occupation(basis.model) n_spin = basis.model.n_spin_components @@ -159,8 +167,8 @@ struct Hubbard{T} manifold::OrbitalManifold U::T function Hubbard((manifold::OrbitalManifold), U) - if isnothing(manifold.iatoms) || isnothing(manifold.projector_l) || - isnothing(manifold.projector_i) + if isnothing(manifold.iatoms) || isnothing(manifold.l) || + isnothing(manifold.i) error("Hubbard term needs specification of atoms and orbital") end U = austrip(U) diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index e76fa780b5..a2321def26 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -92,7 +92,7 @@ end test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) test_consistency_term(Hartree()) - test_consistency_term(Hubbard(OrbitalManifold(; species=:Si, label="3P"), 0.01)) + test_consistency_term(Hubbard(OrbitalManifold(Si, [1,2], "3P"), 0.01)) test_consistency_term(Ewald()) test_consistency_term(PspCorrection()) test_consistency_term(Xc([:lda_xc_teter93])) diff --git a/test/hubbard.jl b/test/hubbard.jl index 30e16a953b..d9983eb44e 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -89,12 +89,12 @@ end n_hub = scfres.hubbard_n scfres_nosym = DFTK.unfold_bz(scfres) term_idx = findfirst(term -> isa(term, DFTK.TermHubbard), - scfres_nosym.basis.terms) + scfres_nosym.basis.terms) term_hub = scfres_nosym.basis.terms[term_idx] nhub_nosym = DFTK.compute_hubbard_n(manifold, scfres_nosym.basis, - scfres_nosym.ψ, scfres_nosym.occupation; - projectors=term_hub.P, - labels=term_hub.labels) + scfres_nosym.ψ, scfres_nosym.occupation; + projectors=term_hub.P, + labels=term_hub.labels) @test n_hub ≈ nhub_nosym end end From a021e326b53431e936fa9a6fd9962040c0386f36 Mon Sep 17 00:00:00 2001 From: Bruno Ploumhans <13494793+Technici4n@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:25:18 +0100 Subject: [PATCH 43/50] Various changes --- examples/collinear_magnetism.jl | 2 +- examples/dos.jl | 4 +- examples/hubbard.jl | 17 +++- ext/DFTKPlotsExt.jl | 10 +- src/postprocess/band_structure.jl | 2 +- src/postprocess/dos.jl | 15 +-- src/pseudo/PspUpf.jl | 8 -- src/scf/self_consistent_field.jl | 12 +-- src/symmetry.jl | 12 +-- src/terms/hubbard.jl | 147 ++++++++++++++---------------- test/hubbard.jl | 4 +- 11 files changed, 110 insertions(+), 123 deletions(-) diff --git a/examples/collinear_magnetism.jl b/examples/collinear_magnetism.jl index 4d0a55670d..8bb9fbd95e 100644 --- a/examples/collinear_magnetism.jl +++ b/examples/collinear_magnetism.jl @@ -109,7 +109,7 @@ plot_dos(bands_666) # is sufficient. # We can clearly see that the origin of this spin-polarization traces back to the # 3D orbital contribution if we look at the corresponding projected density of states (PDOS). -plot_pdos(bands_666; iatoms=[1], label="3D") +plot_pdos(bands_666; iatom=1, label="3D") # Similarly the band structure shows clear differences between both spin components. using Unitful diff --git a/examples/dos.jl b/examples/dos.jl index 043bf7b773..b3cf17bd87 100644 --- a/examples/dos.jl +++ b/examples/dos.jl @@ -28,5 +28,5 @@ plot_dos(scfres) plot_ldos(scfres; n_points=100, ldos_xyz=[:, 10, 10]) # Plot the projected DOS -p = plot_pdos(scfres; iatoms=[1], label="3S", εrange=(-0.3, 0.5)) -plot_pdos(scfres; p, colors=[:red], iatoms=[1], label="3P", εrange=(-0.3, 0.5)) +p = plot_pdos(scfres; iatom=1, label="3S", εrange=(-0.3, 0.5)) +plot_pdos(scfres; p, colors=[:red], iatom=1, label="3P", εrange=(-0.3, 0.5)) diff --git a/examples/hubbard.jl b/examples/hubbard.jl index ba618512eb..9a5ddf4473 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -21,7 +21,8 @@ positions = [zeros(3), ones(3) / 4, ones(3) / 2, ones(3) * 3 / 4] magnetic_moments = [2, 0, -1, 0] # First, we run an SCF and band computation without the Hubbard term -model = model_DFT(lattice, atoms, positions; temperature=5e-3, functionals=PBE(), magnetic_moments) +model = model_DFT(lattice, atoms, positions; temperature=5e-3, + functionals=PBE(), magnetic_moments) basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2]) scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) bands = compute_bands(scfres, MonkhorstPack(4, 4, 4)) @@ -33,11 +34,17 @@ band_gap = bands.eigenvalues[1][lowest_unocc_band] - bands.eigenvalues[1][lowest width = 5.0u"eV" εrange = (εF - austrip(width), εF + austrip(width)) p = plot_dos(bands; εrange, colors=[:red, :red]) -plot_pdos(bands; p, iatoms=[1], label="3D", colors=[:yellow, :orange], εrange) +plot_pdos(bands; p, iatom=1, label="3D", colors=[:yellow, :orange], εrange) -# To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant +# To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant. +# +# In DFTK there are a few ways to construct the `OrbitalManifold`. +# Here, we will apply the Hubbard correction on the 3D orbital of all Nickel atoms. +# +# Note that "manifold" is the standard term used in the literature for the set of atomic orbitals +# used to compute the Hubbard correction, but it is not a manifold in the mathematical sense. U = 10u"eV" -manifold = OrbitalManifold([1,3], Ni.psp, 2, 1) +manifold = OrbitalManifold(atoms, Ni, "3D") # Run SCF with a DFT+U setup, notice the `extra_terms` keyword argument, setting up the Hubbard +U term. model = model_DFT(lattice, atoms, positions; extra_terms=[Hubbard(manifold, U)], @@ -55,4 +62,4 @@ band_gap = bands_hub.eigenvalues[1][lowest_unocc_band] - bands_hub.eigenvalues[1 εF = bands_hub.εF εrange = (εF - austrip(width), εF + austrip(width)) p = plot_dos(bands_hub; p, colors=[:blue, :blue], εrange) -plot_pdos(bands_hub; p, iatoms=[1], label="3D", colors=[:green, :purple], εrange) +plot_pdos(bands_hub; p, iatom=1, label="3D", colors=[:green, :purple], εrange) diff --git a/ext/DFTKPlotsExt.jl b/ext/DFTKPlotsExt.jl index cc2f63ceae..d88d94f472 100644 --- a/ext/DFTKPlotsExt.jl +++ b/ext/DFTKPlotsExt.jl @@ -142,7 +142,7 @@ function plot_ldos(basis, eigenvalues, ψ; εF=nothing, unit=u"hartree", end plot_ldos(scfres; kwargs...) = plot_ldos(scfres.basis, scfres.eigenvalues, scfres.ψ; scfres.εF, kwargs...) -function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatoms=nothing, label=nothing, +function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatom=nothing, label=nothing, positions=basis.model.positions, εF=nothing, unit=u"hartree", temperature=basis.model.temperature, @@ -155,17 +155,17 @@ function plot_pdos(basis::PlaneWaveBasis{T}, eigenvalues, ψ; iatoms=nothing, la n_spin = basis.model.n_spin_components to_unit = ustrip(auconvert(unit, 1.0)) - species = isnothing(iatoms) ? "all atoms" : "atoms $(iatoms) ($(basis.model.atoms[iatoms].species))" + species = isnothing(iatom) ? "all atoms" : "atom $(iatom) ($(basis.model.atoms[iatom].species))" orb_name = isnothing(label) ? "all orbitals" : label # Plot pdos isnothing(p) && (p = Plots.plot()) p = Plots.plot(p; kwargs...) spinlabels = spin_components(basis.model) - labels = basis.terms[findfirst(term -> isa(term, DFTK.TermHubbard), basis.terms)].labels pdos = DFTK.sum_pdos(compute_pdos(εs, basis, ψ, eigenvalues; - positions, temperature, smearing), - [OrbitalManifold(basis.model.atoms, labels; iatoms, label)]) + positions, temperature, smearing), + [l -> ((isnothing(iatom) || l.iatom == iatom) + && (isnothing(label) || l.label == label))]) for σ = 1:n_spin plot_label = n_spin > 1 ? "$(species) $(orb_name) $(spinlabels[σ]) spin" : "$(species) $(orb_name)" Plots.plot!(p, (εs .- eshift) .* to_unit, pdos[:, σ]; label=plot_label, color=colors[σ]) diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index 388699c1cd..64a54fe69f 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -71,7 +71,7 @@ function compute_bands(scfres::NamedTuple, τ = haskey(scfres, :τ) ? scfres.τ : nothing hubbard_n = haskey(scfres, :hubbard_n) ? scfres.hubbard_n : nothing compute_bands(scfres.basis, kgrid; - scfres.ρ, τ, hubbard_n, scfres.occupation, + scfres.ρ, τ, hubbard_n, scfres.occupation, scfres.εF, n_bands, kwargs...) end diff --git a/src/postprocess/dos.jl b/src/postprocess/dos.jl index 1d07210e3c..cf5493272a 100644 --- a/src/postprocess/dos.jl +++ b/src/postprocess/dos.jl @@ -136,8 +136,8 @@ The projectors are computed by decomposition into a form factor multiplied by a Overview of inputs: - `positions` : Positions of the atoms in the unit cell. Default is model.positions. -- `isonmanifold` (opt) : (see notes below) OrbitalManifold struct to select only a subset of orbitals - for the computation. +- `isonmanifold` (opt) : (see notes below) A function, typically a lambda, + to select projectors to include in the pdos. Overview of outputs: - `projectors`: Vector of matrices of projectors @@ -212,23 +212,24 @@ function atomic_orbital_projections(basis::PlaneWaveBasis{T}, ψ; end """ - sum_pdos(pdos_res, manifolds) + sum_pdos(pdos_res, projector_filters) This function extracts and sums up all the PDOSes, directly from the output of the `compute_pdos` function, - that match any of the manifolds. + that match any of the filters. Overview of inputs: - `pdos_res`: Whole output from compute_pdos. -- `manifolds`: Vector of OrbitalManifolds to select the desired projectors pdos. +- `projector_filters`: Vector of functions, typically lambdas, + to select projectors to include in the pdos. Overview of outputs: - `pdos`: Vector containing the pdos(ε). """ -function sum_pdos(pdos_res, manifolds::AbstractVector) +function sum_pdos(pdos_res, projector_filters::AbstractVector) pdos = zeros(Float64, length(pdos_res.εs), size(pdos_res.pdos, 3)) for σ in 1:size(pdos_res.pdos, 3) for (j, orb) in enumerate(pdos_res.projector_labels) - if any(is_on_manifold(orb, manifold) for manifold in manifolds) + if any(filt(orb) for filt in projector_filters) pdos[:, σ] += pdos_res.pdos[:, j, σ] end end diff --git a/src/pseudo/PspUpf.jl b/src/pseudo/PspUpf.jl index b3486df67a..7df3f0bab3 100644 --- a/src/pseudo/PspUpf.jl +++ b/src/pseudo/PspUpf.jl @@ -175,14 +175,6 @@ count_n_pswfc_radial(psp::PspUpf, l) = length(psp.r2_pswfcs[l+1]) pswfc_label(psp::PspUpf, i, l) = psp.pswfc_labels[l+1][i] -function pswfc_indices(psp::PspUpf, label) - for l in 0:psp.lmax, n in 1:DFTK.count_n_pswfc_radial(psp, l) - if (DFTK.pswfc_label(psp, n, l) == label) - return (; l, n) - end - end -end - function eval_psp_pswfc_real(psp::PspUpf, i, l, r::T)::T where {T<:Real} psp.r2_pswfcs_interp[l+1][i](r) / r^2 # TODO if r is below a threshold, return zero end diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index 2e7434970c..86a72e6078 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -181,11 +181,11 @@ Overview of parameters: if any(needs_τ, basis.terms) τ = compute_kinetic_energy_density(basis, ψ, occupation) end - for term in basis.terms - if isa(term, DFTK.TermHubbard) - hubbard_n = compute_hubbard_n(term.manifold, basis, ψ, occupation; - projectors=term.P, labels=term.labels) - end + ihubbard = findfirst(t -> t isa TermHubbard, basis.terms) + if !isnothing(ihubbard) + term = basis.terms[ihubbard] + hubbard_n = compute_hubbard_n(term.manifold, basis, ψ, occupation; + projectors=term.P, labels=term.labels) end # Update info with results gathered so far @@ -233,7 +233,7 @@ Overview of parameters: # Callback is run one last time with final state to allow callback to clean up scfres = (; ham, basis, energies, converged, nbandsalg.occupation_threshold, - ρ=ρout, τ, hubbard_n, α=damping, eigenvalues, occupation, εF, + ρ=ρout, τ, hubbard_n, α=damping, eigenvalues, occupation, εF, info.n_bands_converge, info.n_iter, info.n_matvec, ψ, info.diagonalization, stage=:finalize, info.history_Δρ, info.history_Etot, info.timedout, mixing, seed, runtime_ns=time_ns() - start_ns, algorithm="SCF") diff --git a/src/symmetry.jl b/src/symmetry.jl index debe8e9238..a31961da3f 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -425,23 +425,23 @@ end """ Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ -function symmetrize_hubbard_n(hubbard_n::Array{Matrix{Complex{T}}}, - model, symmetries, positions) where {T} +function symmetrize_hubbard_n(manifold::OrbitalManifold, + hubbard_n::Array{Matrix{Complex{T}}}, + model, symmetries) where {T} # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) # WARNING: To implement +V this will need to be changed! + positions = model.positions[manifold.iatoms] nspins = size(hubbard_n, 1) natoms = size(hubbard_n, 2) nsym = length(symmetries) - # We extract the l value from the manifold size per atom (2l+1) - l = div(size(hubbard_n[1, 1, 1], 1)-1,2) - ldim = 2*l+1 + ldim = 2*manifold.l+1 # Initialize the hubbard_n matrix ns = [zeros(Complex{T}, ldim, ldim) for _ in 1:nspins, _ in 1:natoms, _ in 1:natoms] for symmetry in symmetries Wcart = model.lattice * symmetry.W * model.inv_lattice - WigD = wigner_d_matrix(l, Wcart) + WigD = wigner_d_matrix(manifold.l, Wcart) for σ in 1:nspins, iatom in 1:natoms sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry) ns[σ, iatom, iatom] .+= WigD' * hubbard_n[σ, sym_atom, sym_atom] * WigD diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 42c4b08dbe..c7bebe9956 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -3,68 +3,62 @@ using Random """ Structure for Hubbard manifold choice and projectors extraction. - It is to be noted that, despite the name used in literature, this is - not a manifold in the mathematical sense. + +It is to be noted that, despite the name used in literature, this is +not a manifold in the mathematical sense. Overview of fields: -- `iatom`: Atom position in the atoms array. -- `species`: Chemical Element as in ElementPsp. -- `label`: Orbital name, e.g.: "3S". +- `psp`: Pseudopotential containing the atomic orbital projectors +- `iatoms`: Atom indices that are part of the manifold. +- `l`: Angular momentum quantum number of the manifold. +- `i`: Index of the atomic orbital within the given l. -All fields are optional, only the given ones will be used for selection. -Can be called with an orbital NamedTuple and returns a boolean - stating whether the orbital belongs to the manifold. +See also the convenience constructors, to construct a manifold more easily. """ -@kwdef struct OrbitalManifold - psp = nothing - iatoms::Union{Vector{Int64}, Nothing} = nothing - l::Union{Int64, Nothing} = nothing - i::Union{Int64, Nothing} = nothing +struct OrbitalManifold + psp::NormConservingPsp + iatoms::Vector{Int64} + l::Int64 + i::Int64 end -OrbitalManifold(atom::ElementPsp, iatoms::Vector{Int64}; l, i) = OrbitalManifold(atom.psp, iatoms, l, i) -function OrbitalManifold(atom::ElementPsp, iatoms::Vector{Int64}, label::String) - l, i = DFTK.pswfc_indices(atom.psp, label) - OrbitalManifold(atom.psp, iatoms, l, i) +function OrbitalManifold(atoms::Vector{<:Element}, atom::ElementPsp, label::AbstractString) + (; l, i) = find_pswfc(atom.psp, label) + OrbitalManifold(atom.psp, findall(at -> at === atom, atoms), l, i) end - -function is_on_manifold(orbital; iatoms=nothing, species=nothing, - l=nothing, n=nothing, label=nothing) - iatom_match = isnothing(iatoms) || (orbital.iatom in iatoms) - species_match = isnothing(species) || (species == orbital.species) - label_match = isnothing(label) || (label == orbital.label) - l_match = isnothing(l) || (l == orbital.l) - n_match = isnothing(n) || (n == orbital.n) - iatom_match && species_match && l_match && n_match && label_match +function OrbitalManifold(psp::NormConservingPsp, iatoms::Vector{Int64}, label::AbstractString) + (; l, i) = find_pswfc(psp, label) + OrbitalManifold(psp, iatoms, l, i) end -function is_on_manifold(orbital, manifold::OrbitalManifold) - is_on_manifold(orbital; iatoms=manifold.iatoms, l=manifold.l, n=manifold.i) +function OrbitalManifold(atom::ElementPsp, iatoms::Vector{Int64}, label::AbstractString) + OrbitalManifold(atom.psp, iatoms, label) end -function OrbitalManifold(atoms::Union{Vector{ElementPsp}, Vector{DFTK.Element}}, - labels; - iatoms::Union{Vector{Int64}, Nothing}=nothing, - label::Union{String, Nothing}=nothing, - species::Union{Symbol, AtomsBase.ChemicalSpecies, Nothing}=nothing) - hub_atoms = Int64[] - for orbital in labels - if is_on_manifold(orbital; iatoms, species, label) - append!(hub_atoms, orbital.iatom) +function find_pswfc(psp::NormConservingPsp, label::String) + for l = 0:psp.lmax, i = 1:count_n_pswfc_radial(psp, l) + if pswfc_label(psp, i, l) == label + return (; l, i) end end - isempty(hub_atoms) && error("Unable to create Hubbard manifold. No atom matches the given keywords") - # If species is nothing, there can be errors if the iatoms correspond to different atomic species - model_atom = atoms[hub_atoms[1]] - !all(atom -> atom.psp == model_atom.psp, atoms[hub_atoms]) && - error("The given Hubbard manifold contains more than one atomic pseudopotential species") - l = isnothing(label) ? nothing : labels[hub_atoms[1]].l - i = isnothing(label) ? nothing : labels[hub_atoms[1]].n - OrbitalManifold(;iatoms=hub_atoms, psp=model_atom.psp, l, i) + error("Could not find pseudo atomic orbital with label $label " + * "in pseudopotential $(psp.identifier).") +end + +function check_hubbard_manifold(manifold::OrbitalManifold, model::Model) + for atom in model.atoms[manifold.iatoms] + atom isa ElementPsp || error("Orbital manifold elements must have a psp.") + atom.psp === manifold.psp || error("Orbital manifold psp does not match the psp of atom $atom") + end + isempty(manifold.iatoms) && error("Orbital manifold has no atoms.") + # Tricky: make sure that the iatoms are consistent with the symmetries of the model, + # i.e. that the manifold would be a valid atom group + _check_symmetries(model.symmetries, model.lattice, [manifold.iatoms], model.positions) + + nothing end -function extract_manifold(basis::PlaneWaveBasis{T}, projectors, labels, - manifold::OrbitalManifold) where {T} +function extract_manifold(manifold::OrbitalManifold, projectors, labels) # We extract the labels only for orbitals belonging to the manifold - proj_indices = findall(orb -> is_on_manifold(orb, manifold), labels) + proj_indices = findall(orb -> orb.iatom ∈ manifold.iatoms && orb.l == manifold.l && orb.n == manifold.i, labels) isempty(proj_indices) && @warn "Projector for $(manifold) not found." manifold_labels = labels[proj_indices] manifold_projectors = map(enumerate(projectors)) do (ik, projk) @@ -102,54 +96,50 @@ Overview of outputs: function compute_hubbard_n(manifold::OrbitalManifold, basis::PlaneWaveBasis{T}, ψ, occupation; - projectors, labels, - positions = basis.model.positions) where {T} + projectors, labels) where {T} filled_occ = filled_occupation(basis.model) n_spin = basis.model.n_spin_components - manifold_atoms = findall(at -> at.psp == manifold.psp, basis.model.atoms) - natoms = length(manifold_atoms) # Number of atoms of the species in the manifold - l = labels[1].l - projectors = reshape_hubbard_proj(basis, projectors, labels, manifold) + manifold_atoms = manifold.iatoms + natoms = length(manifold_atoms) + l = manifold.l + projectors = reshape_hubbard_proj(projectors, labels, manifold) hubbard_n = Array{Matrix{Complex{T}}}(undef, n_spin, natoms, natoms) for σ in 1:n_spin - for idx in 1:length(manifold_atoms), jdx in 1:length(manifold_atoms) - hubbard_n[σ, idx, jdx] = zeros(Complex{T}, 2*l+1, 2*l+1) + for idx in 1:natoms, jdx in 1:natoms + hubbard_n[σ, idx, jdx] = zeros(Complex{T}, 2l+1, 2l+1) for ik = krange_spin(basis, σ) j_projection = ψ[ik]' * projectors[ik][jdx] # <ψ|ϕJ> i_projection = projectors[ik][idx]' * ψ[ik] # <ϕI|ψ> # Sums over the bands, dividing by filled_occ to deal # with the physical two spin channels separately hubbard_n[σ, idx, jdx] .+= (basis.kweights[ik] * i_projection * - diagm(occupation[ik]/filled_occ) * j_projection) + diagm(occupation[ik]/filled_occ) * j_projection) end hubbard_n[σ, idx, jdx] = mpi_sum(hubbard_n[σ, idx, jdx], basis.comm_kpts) end end - hubbard_n = symmetrize_hubbard_n(hubbard_n, basis.model, - basis.symmetries, basis.model.positions[manifold_atoms]) + hubbard_n = symmetrize_hubbard_n(manifold, hubbard_n, basis.model, basis.symmetries) end """ This function reshapes for each kpoint the projectors matrix to a vector of matrices, - taking only the columns corresponding to orbitals in the manifold and splitting them - into different matrices, one for each atom. Columns in the same matrix differ only in - the value of the magnetic quantum number m of the corresponding orbitals. +taking only the columns corresponding to orbitals in the manifold and splitting them +into different matrices, one for each atom. Columns in the same matrix differ only in +the value of the magnetic quantum number m of the corresponding orbitals. """ -function reshape_hubbard_proj(basis, projectors::Vector{Matrix{Complex{T}}}, +function reshape_hubbard_proj(projectors::Vector{Matrix{Complex{T}}}, labels, manifold) where {T} - manifold_atoms = findall(at -> at.psp == manifold.psp, basis.model.atoms) - natoms = length(manifold_atoms) - nprojs = length(labels) - l = labels[1].l - @assert all(label -> label.l==l, labels) "$(labels)" - @assert length(labels) == natoms * (2*l+1) - p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for i in 1:length(projectors)] - for (idx, iatom) in enumerate(manifold_atoms) - for i in 1:2*l+1:nprojs + natoms = length(manifold.iatoms) + l = manifold.l + @assert all(label -> label.l == manifold.l, labels) "$(labels)" + @assert length(labels) == natoms * (2l+1) + p_I = [Vector{Matrix{Complex{T}}}(undef, natoms) for _ = 1:length(projectors)] + for (idx, iatom) in enumerate(manifold.iatoms) + for i = 1:2l+1:length(labels) iatom != labels[i].iatom && continue for (ik, projk) in enumerate(projectors) - p_I[ik][idx] = projk[:, i:i+2*l] + p_I[ik][idx] = projk[:, i:i+2l] end end end @@ -166,18 +156,16 @@ Hubbard energy: struct Hubbard{T} manifold::OrbitalManifold U::T - function Hubbard((manifold::OrbitalManifold), U) - if isnothing(manifold.iatoms) || isnothing(manifold.l) || - isnothing(manifold.i) - error("Hubbard term needs specification of atoms and orbital") - end + function Hubbard(manifold::OrbitalManifold, U) U = austrip(U) new{typeof(U)}(manifold, U) end end function (hubbard::Hubbard)(basis::AbstractBasis) + check_hubbard_manifold(hubbard.manifold, basis.model) + projs, labels = atomic_orbital_projectors(basis) - labels, projectors_matrix = extract_manifold(basis, projs, labels, hubbard.manifold) + labels, projectors_matrix = extract_manifold(hubbard.manifold, projs, labels) TermHubbard(hubbard.manifold, hubbard.U, projectors_matrix, labels) end @@ -191,7 +179,6 @@ end @timing "ene_ops: hubbard" function ene_ops(term::TermHubbard, basis::PlaneWaveBasis{T}, ψ, occupation; hubbard_n=nothing, - labels=term.labels, kwargs...) where {T} if isnothing(hubbard_n) return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) diff --git a/test/hubbard.jl b/test/hubbard.jl index d9983eb44e..d8a8da223d 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -64,7 +64,7 @@ end # Hubbard parameters U = 10u"eV" - manifold = OrbitalManifold([1,3], Ni.psp, 2, 1) + manifold = OrbitalManifold(atoms, Ni, "3D") model = model_DFT(lattice, atoms, positions; extra_terms=[Hubbard(manifold, U)], @@ -88,7 +88,7 @@ end @testset "Test symmetry consistency" begin n_hub = scfres.hubbard_n scfres_nosym = DFTK.unfold_bz(scfres) - term_idx = findfirst(term -> isa(term, DFTK.TermHubbard), + term_idx = findfirst(term -> isa(term, DFTK.TermHubbard), scfres_nosym.basis.terms) term_hub = scfres_nosym.basis.terms[term_idx] nhub_nosym = DFTK.compute_hubbard_n(manifold, scfres_nosym.basis, From e2d97d6276a0c2efe370972262216d21dc48c5f1 Mon Sep 17 00:00:00 2001 From: Bruno Ploumhans <13494793+Technici4n@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:27:12 +0100 Subject: [PATCH 44/50] Minor tweaks --- src/postprocess/band_structure.jl | 11 +++++----- src/symmetry.jl | 3 +-- src/terms/hubbard.jl | 4 ++-- src/terms/operators.jl | 1 + src/terms/xc.jl | 2 +- src/workarounds/forwarddiff_rules.jl | 4 ++-- test/hamiltonian_consistency.jl | 31 +++++++++++++++++----------- 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index 64a54fe69f..d991a3405f 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -14,10 +14,9 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): @timing function compute_bands(basis::PlaneWaveBasis, kgrid::Union{AbstractKgrid,AbstractKgridGenerator}; n_bands=default_n_bands_bandstructure(basis.model), - n_extra=3, ρ=nothing, τ=nothing, εF=nothing, - occupation=nothing, hubbard_n=nothing, - eigensolver=lobpcg_hyper, tol=1e-3, seed=nothing, - kwargs...) + n_extra=3, ρ=nothing, τ=nothing, hubbard_n=nothing, + εF=nothing, eigensolver=lobpcg_hyper, tol=1e-3, + seed=nothing, kwargs...) # kcoords are the kpoint coordinates in fractional coordinates if isnothing(ρ) if any(t isa TermNonlinear for t in basis.terms) @@ -37,7 +36,7 @@ All kwargs not specified below are passed to [`diagonalize_all_kblocks`](@ref): # Create new basis with new kpoints bs_basis = PlaneWaveBasis(basis, kgrid) - ham = Hamiltonian(bs_basis; ρ, τ, hubbard_n, occupation) + ham = Hamiltonian(bs_basis; ρ, τ, hubbard_n) eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands + n_extra; n_conv_check=n_bands, tol, kwargs...) if !eigres.converged @@ -71,7 +70,7 @@ function compute_bands(scfres::NamedTuple, τ = haskey(scfres, :τ) ? scfres.τ : nothing hubbard_n = haskey(scfres, :hubbard_n) ? scfres.hubbard_n : nothing compute_bands(scfres.basis, kgrid; - scfres.ρ, τ, hubbard_n, scfres.occupation, + scfres.ρ, τ, hubbard_n, scfres.εF, n_bands, kwargs...) end diff --git a/src/symmetry.jl b/src/symmetry.jl index a31961da3f..3e57d32c24 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -434,7 +434,6 @@ function symmetrize_hubbard_n(manifold::OrbitalManifold, positions = model.positions[manifold.iatoms] nspins = size(hubbard_n, 1) natoms = size(hubbard_n, 2) - nsym = length(symmetries) ldim = 2*manifold.l+1 # Initialize the hubbard_n matrix @@ -447,7 +446,7 @@ function symmetrize_hubbard_n(manifold::OrbitalManifold, ns[σ, iatom, iatom] .+= WigD' * hubbard_n[σ, sym_atom, sym_atom] * WigD end end - ns ./= nsym + ns ./= length(symmetries) ns end diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index c7bebe9956..83d7c7fe08 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -148,9 +148,9 @@ function reshape_hubbard_proj(projectors::Vector{Matrix{Complex{T}}}, end @doc raw""" -Hubbard energy: +Hubbard energy, following the Dudarev et al. (1998) rotationally invariant formalism: ```math -1/2 Σ_{σI} U * Tr[hubbard_n[σ,i,i] * (1 - hubbard_n[σ,i,i])] +1/2 Σ_{σI} U * Tr[hubbard_n[σ,I,I] * (1 - hubbard_n[σ,I,I])] ``` """ struct Hubbard{T} diff --git a/src/terms/operators.jl b/src/terms/operators.jl index ae6dab5ee0..516dcfbbce 100644 --- a/src/terms/operators.jl +++ b/src/terms/operators.jl @@ -155,6 +155,7 @@ function apply!(Hψ, op::DivAgradOperator, ψ; end # TODO Implement Base.Matrix(op::DivAgradOperator) + # Optimize RFOs by combining terms that can be combined function optimize_operators(ops) ops = [op for op in ops if !(op isa NoopOperator)] diff --git a/src/terms/xc.jl b/src/terms/xc.jl index b9ead3cdd3..fae8429dd5 100644 --- a/src/terms/xc.jl +++ b/src/terms/xc.jl @@ -84,7 +84,7 @@ function xc_potential_real(term::TermXc, basis::PlaneWaveBasis{T}, ψ, occupatio max_ρ_derivs = maximum(max_required_derivative, term.functionals) density = LibxcDensities(basis, max_ρ_derivs, ρ, τ) - if !isnothing(term.ρcore) && !isnothing(τ) + if !isnothing(term.ρcore) && needs_τ(term) negative_α = @views any(1:n_spin) do iσ # α = (τ - τ_W) / τ_unif should be positive with τ_W = |∇ρ|² / 8ρ # equivalently, check 8ρτ - |∇ρ|² ≥ 0 diff --git a/src/workarounds/forwarddiff_rules.jl b/src/workarounds/forwarddiff_rules.jl index e8b8ea389a..a11f98cee1 100644 --- a/src/workarounds/forwarddiff_rules.jl +++ b/src/workarounds/forwarddiff_rules.jl @@ -275,8 +275,8 @@ end # This has to be changed whenever the scfres structure changes (; ham, basis=basis_dual, energies, ρ, eigenvalues, occupation, εF, ψ, - scfres.τ, # TODO make τ and hubbard_n also differentiable for meta-GGA DFPT - scfres.hubbard_n, + # TODO make τ and hubbard_n also differentiable for meta-GGA/DFT+U DFPT + scfres.τ, scfres.hubbard_n, # non-differentiable metadata: response=getfield.(δresults, :info_gmres), scfres.converged, scfres.occupation_threshold, scfres.α, scfres.n_iter, diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index a2321def26..1dd3ef7e09 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -22,14 +22,16 @@ function test_matrix_repr_operator(hamk, ψk; atol=1e-8) end function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, 3], - kshift=[0, 1, 0]/2, lattice=testcase.lattice, Ecut=15, - spin_polarization=:none) + kshift=[0, 1, 0]/2, lattice=testcase.lattice, + atom=nothing, Ecut=10, spin_polarization=:none) sspol = spin_polarization != :none ? " ($spin_polarization)" : "" xc = term isa Xc ? "($(first(term.functionals)))" : "" @testset "$(typeof(term))$xc $sspol" begin n_dim = 3 - count(iszero, eachcol(lattice)) - Si = n_dim == 3 ? ElementPsp(14, load_psp(testcase.psp_upf)) : ElementCoulomb(:Si) - atoms = [Si, Si] + if isnothing(atom) + atom = n_dim == 3 ? ElementPsp(14, load_psp(testcase.psp_gth)) : ElementCoulomb(:Si) + end + atoms = [atom, atom] model = Model(lattice, atoms, testcase.positions; terms=[term], spin_polarization, symmetries=true) basis = PlaneWaveBasis(model; Ecut, kgrid=MonkhorstPack(kgrid; kshift)) @@ -73,6 +75,9 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, end diff_predicted = mpi_sum(diff_predicted, basis.comm_kpts) + # Make sure that we don't accidentally test 0 == 0 + @test abs(diff) > atol + err = abs(diff - diff_predicted) @test err < rtol * abs(E0.total) || err < atol end @@ -83,7 +88,7 @@ end @testitem "Hamiltonian consistency" setup=[TestCases, HamConsistency] tags=[:dont_test_mpi] begin using DFTK using LinearAlgebra - using .HamConsistency: test_consistency_term + using .HamConsistency: test_consistency_term, testcase test_consistency_term(Kinetic()) test_consistency_term(AtomicLocal()) @@ -92,18 +97,20 @@ end test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) test_consistency_term(Hartree()) - test_consistency_term(Hubbard(OrbitalManifold(Si, [1,2], "3P"), 0.01)) + let + Si = ElementPsp(14, load_psp(testcase.psp_upf)) + test_consistency_term(Hubbard(OrbitalManifold(Si, [1, 2], "3P"), 0.01), atom=Si) + end test_consistency_term(Ewald()) test_consistency_term(PspCorrection()) test_consistency_term(Xc([:lda_xc_teter93])) test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) - # TODO: for use_nlcc=true need to fix consistency for meta-GGA with NLCC (see JuliaMolSim/DFTK.jl#1180) - test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false)) - test_consistency_term(Xc([:mgga_x_scan]; use_nlcc=false)) - test_consistency_term(Xc([:mgga_c_scan]; use_nlcc=false), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_b00]; use_nlcc=false)) - test_consistency_term(Xc([:mgga_c_b94]; use_nlcc=false), spin_polarization=:collinear) + test_consistency_term(Xc([:mgga_x_tpss])) + test_consistency_term(Xc([:mgga_x_scan])) + test_consistency_term(Xc([:mgga_c_scan]), spin_polarization=:collinear) + test_consistency_term(Xc([:mgga_x_b00])) + test_consistency_term(Xc([:mgga_c_b94]), spin_polarization=:collinear) let a = 6 From 2da2d032484d4665a9dbe7aae8b7f64c20dc14ad Mon Sep 17 00:00:00 2001 From: Bruno Ploumhans <13494793+Technici4n@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:59:35 +0100 Subject: [PATCH 45/50] Fix tests not testing anything --- src/scf/self_consistent_field.jl | 4 +-- src/terms/hubbard.jl | 35 ++++++++--------------- test/hamiltonian_consistency.jl | 49 +++++++++++++++++++++++--------- test/hubbard.jl | 6 ++-- 4 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index 86a72e6078..5a11b2eb27 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -183,9 +183,7 @@ Overview of parameters: end ihubbard = findfirst(t -> t isa TermHubbard, basis.terms) if !isnothing(ihubbard) - term = basis.terms[ihubbard] - hubbard_n = compute_hubbard_n(term.manifold, basis, ψ, occupation; - projectors=term.P, labels=term.labels) + hubbard_n = compute_hubbard_n(basis.terms[ihubbard], basis, ψ, occupation) end # Update info with results gathered so far diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 83d7c7fe08..d502441fed 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -68,42 +68,31 @@ function extract_manifold(manifold::OrbitalManifold, projectors, labels) end """ - compute_hubbard_n(manifold, basis, ψ, occupation; [projectors, labels, positions]) + compute_hubbard_n(term::TermHubbard, basis, ψ, occupation) Computes a matrix hubbard_n of size (n_spin, natoms, natoms), where each entry hubbard_n[iatom, jatom] - contains the submatrix of the occupation matrix corresponding to the projectors - of atom iatom and atom jatom, with dimensions determined by the number of projectors for each atom. - The atoms and orbitals are defined by the manifold tuple. +contains the submatrix of the occupation matrix corresponding to the projectors +of atom iatom and atom jatom, with dimensions determined by the number of projectors for each atom. +The atoms and orbitals are defined by the manifold tuple. hubbard_n[σ, iatom, jatom][m1, m2] = Σₖ₍ₛₚᵢₙ₎Σₙ weights[ik, ibnd] * ψₙₖ' * Pᵢₘ₁ * Pᵢₘ₂' * ψₙₖ - where n or ibnd is the band index, ``weights[ik ibnd] = kweights[ik] * occupation[ik, ibnd]`` - and ``Pᵢₘ₁`` is the pseudoatomic orbital projector for atom i and orbital m₁ - (just the magnetic quantum number, since l is fixed, as is usual in the literature). - For details on the projectors see `atomic_orbital_projectors`. - -Overview of inputs: -- `manifold`: OrbitalManifold with the atomic orbital type to define the Hubbard manifold. -- `occupation`: Occupation matrix for the bands. -- `projectors` (kwarg): Vector of projection matrices. For each matrix, each column corresponds - to a different atomic orbital projector, as specified in labels. -- `labels` (kwarg): Vector of NamedTuples. Each projectors[ik][:,iproj] column has all relevant - chemical information stored in the corresponding labels[iproj] NamedTuple. - -Overview of outputs: -- `hubbard_n`: 3-tensor of matrices. See above for details. +where n or ibnd is the band index, ``weights[ik ibnd] = kweights[ik] * occupation[ik, ibnd]`` +and ``Pᵢₘ₁`` is the pseudoatomic orbital projector for atom i and orbital m₁ +(just the magnetic quantum number, since l is fixed, as is usual in the literature). +For details on the projectors see `atomic_orbital_projectors`. """ -function compute_hubbard_n(manifold::OrbitalManifold, +function compute_hubbard_n(term::TermHubbard, basis::PlaneWaveBasis{T}, - ψ, occupation; - projectors, labels) where {T} + ψ, occupation) where {T} filled_occ = filled_occupation(basis.model) n_spin = basis.model.n_spin_components + manifold = term.manifold manifold_atoms = manifold.iatoms natoms = length(manifold_atoms) l = manifold.l - projectors = reshape_hubbard_proj(projectors, labels, manifold) + projectors = reshape_hubbard_proj(term.P, term.labels, manifold) hubbard_n = Array{Matrix{Complex{T}}}(undef, n_spin, natoms, natoms) for σ in 1:n_spin for idx in 1:natoms, jdx in 1:natoms diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 1dd3ef7e09..edbf2f248e 100644 --- a/test/hamiltonian_consistency.jl +++ b/test/hamiltonian_consistency.jl @@ -49,7 +49,11 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, compute_density(basis, ψ, occupation) end τ = compute_kinetic_energy_density(basis, ψ, occupation) - E0, ham = energy_hamiltonian(basis, ψ, occupation; ρ, τ) + hubbard_n = nothing + if term isa Hubbard + hubbard_n = DFTK.compute_hubbard_n(only(basis.terms), basis, ψ, occupation) + end + E0, ham = energy_hamiltonian(basis, ψ, occupation; ρ, τ, hubbard_n) @assert length(basis.terms) == 1 @@ -60,7 +64,14 @@ function test_consistency_term(term; rtol=1e-4, atol=1e-8, ε=1e-6, kgrid=[1, 2, compute_density(basis, ψ_trial, occupation) end τ_trial = compute_kinetic_energy_density(basis, ψ_trial, occupation) - (; energies) = energy_hamiltonian(basis, ψ_trial, occupation; ρ=ρ_trial, τ=τ_trial) + hubbard_n_trial = nothing + if term isa Hubbard + hubbard_n_trial = DFTK.compute_hubbard_n(only(basis.terms), basis, + ψ_trial, occupation) + end + (; energies) = energy_hamiltonian(basis, ψ_trial, occupation; + ρ=ρ_trial, τ=τ_trial, + hubbard_n=hubbard_n_trial) energies.total end diff = (compute_E(ε) - compute_E(-ε)) / (2ε) @@ -91,8 +102,6 @@ end using .HamConsistency: test_consistency_term, testcase test_consistency_term(Kinetic()) - test_consistency_term(AtomicLocal()) - test_consistency_term(AtomicNonlocal()) test_consistency_term(ExternalFromReal(X -> cos(X[1]))) test_consistency_term(ExternalFromFourier(X -> abs(norm(X)))) test_consistency_term(LocalNonlinearity(ρ -> ρ^2)) @@ -100,17 +109,29 @@ end let Si = ElementPsp(14, load_psp(testcase.psp_upf)) test_consistency_term(Hubbard(OrbitalManifold(Si, [1, 2], "3P"), 0.01), atom=Si) + test_consistency_term(Hubbard(OrbitalManifold(Si, [1, 2], "3P"), 0.01), atom=Si, + spin_polarization=:collinear) + end + # Disabled since the energy is constant, and the test guards against 0 differences + # test_consistency_term(Ewald()) + # test_consistency_term(PspCorrection()) + for psp in [testcase.psp_gth, testcase.psp_upf] + Si = ElementPsp(14, load_psp(psp)) + test_consistency_term(AtomicLocal(), atom=Si) + test_consistency_term(AtomicNonlocal(), atom=Si) + test_consistency_term(Xc([:lda_xc_teter93]), atom=Si) + test_consistency_term(Xc([:lda_xc_teter93]), atom=Si, spin_polarization=:collinear) + test_consistency_term(Xc([:gga_x_pbe]), atom=Si, spin_polarization=:collinear) + # TODO: for use_nlcc=true need to fix consistency for meta-GGA with NLCC + # (see JuliaMolSim/DFTK.jl#1180) + test_consistency_term(Xc([:mgga_x_tpss]; use_nlcc=false), atom=Si) + test_consistency_term(Xc([:mgga_x_scan]; use_nlcc=false), atom=Si) + test_consistency_term(Xc([:mgga_c_scan]; use_nlcc=false), atom=Si, + spin_polarization=:collinear) + test_consistency_term(Xc([:mgga_x_b00]; use_nlcc=false), atom=Si) + test_consistency_term(Xc([:mgga_c_b94]; use_nlcc=false), atom=Si, + spin_polarization=:collinear) end - test_consistency_term(Ewald()) - test_consistency_term(PspCorrection()) - test_consistency_term(Xc([:lda_xc_teter93])) - test_consistency_term(Xc([:lda_xc_teter93]), spin_polarization=:collinear) - test_consistency_term(Xc([:gga_x_pbe]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_tpss])) - test_consistency_term(Xc([:mgga_x_scan])) - test_consistency_term(Xc([:mgga_c_scan]), spin_polarization=:collinear) - test_consistency_term(Xc([:mgga_x_b00])) - test_consistency_term(Xc([:mgga_c_b94]), spin_polarization=:collinear) let a = 6 diff --git a/test/hubbard.jl b/test/hubbard.jl index d8a8da223d..ab246788b6 100644 --- a/test/hubbard.jl +++ b/test/hubbard.jl @@ -91,10 +91,8 @@ end term_idx = findfirst(term -> isa(term, DFTK.TermHubbard), scfres_nosym.basis.terms) term_hub = scfres_nosym.basis.terms[term_idx] - nhub_nosym = DFTK.compute_hubbard_n(manifold, scfres_nosym.basis, - scfres_nosym.ψ, scfres_nosym.occupation; - projectors=term_hub.P, - labels=term_hub.labels) + nhub_nosym = DFTK.compute_hubbard_n(term_hub, scfres_nosym.basis, + scfres_nosym.ψ, scfres_nosym.occupation) @test n_hub ≈ nhub_nosym end end From 9f7dbcdd5bb279a2ee642c8af069fec8b7f7a9ac Mon Sep 17 00:00:00 2001 From: Bruno Ploumhans <13494793+Technici4n@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:22:19 +0100 Subject: [PATCH 46/50] Reorder OrbitalManifold constructors --- src/terms/hubbard.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index d502441fed..6d83f883da 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -22,16 +22,15 @@ struct OrbitalManifold i::Int64 end function OrbitalManifold(atoms::Vector{<:Element}, atom::ElementPsp, label::AbstractString) - (; l, i) = find_pswfc(atom.psp, label) - OrbitalManifold(atom.psp, findall(at -> at === atom, atoms), l, i) + OrbitalManifold(atom, findall(at -> at === atom, atoms), label) +end +function OrbitalManifold(atom::ElementPsp, iatoms::Vector{Int64}, label::AbstractString) + OrbitalManifold(atom.psp, iatoms, label) end function OrbitalManifold(psp::NormConservingPsp, iatoms::Vector{Int64}, label::AbstractString) (; l, i) = find_pswfc(psp, label) OrbitalManifold(psp, iatoms, l, i) end -function OrbitalManifold(atom::ElementPsp, iatoms::Vector{Int64}, label::AbstractString) - OrbitalManifold(atom.psp, iatoms, label) -end function find_pswfc(psp::NormConservingPsp, label::String) for l = 0:psp.lmax, i = 1:count_n_pswfc_radial(psp, l) From ca16a444040f4567f6addc403d33a10ce514c156 Mon Sep 17 00:00:00 2001 From: Bruno Ploumhans <13494793+Technici4n@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:22:57 +0100 Subject: [PATCH 47/50] Move hubbard_n computation down --- src/terms/hubbard.jl | 114 +++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index 6d83f883da..a71efc9978 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -66,6 +66,63 @@ function extract_manifold(manifold::OrbitalManifold, projectors, labels) (; manifold_labels, manifold_projectors) end +@doc raw""" +Hubbard energy, following the Dudarev et al. (1998) rotationally invariant formalism: +```math +1/2 Σ_{σI} U * Tr[hubbard_n[σ,I,I] * (1 - hubbard_n[σ,I,I])] +``` +""" +struct Hubbard{T} + manifold::OrbitalManifold + U::T + function Hubbard(manifold::OrbitalManifold, U) + U = austrip(U) + new{typeof(U)}(manifold, U) + end +end +function (hubbard::Hubbard)(basis::AbstractBasis) + check_hubbard_manifold(hubbard.manifold, basis.model) + + projs, labels = atomic_orbital_projectors(basis) + labels, projectors_matrix = extract_manifold(hubbard.manifold, projs, labels) + TermHubbard(hubbard.manifold, hubbard.U, projectors_matrix, labels) +end + +struct TermHubbard{T, PT, L} <: Term + manifold::OrbitalManifold + U::T + P::PT + labels::L +end + +@timing "ene_ops: hubbard" function ene_ops(term::TermHubbard, + basis::PlaneWaveBasis{T}, + ψ, occupation; hubbard_n=nothing, + kwargs...) where {T} + if isnothing(hubbard_n) + return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) + end + proj = term.P + + filled_occ = filled_occupation(basis.model) + types = findall(at -> at.psp == term.manifold.psp, basis.model.atoms) + natoms = length(types) + n_spin = basis.model.n_spin_components + nproj_atom = size(hubbard_n[1,1,1], 1) # This is the number of projectors per atom, namely 2l+1 + # For the ops we have to reshape hubbard_n to match the NonlocalOperator structure, using a block diagonal form + nhub = [zeros(Complex{T}, nproj_atom*natoms, nproj_atom*natoms) for _ in 1:n_spin] + E = zero(T) + for σ in 1:n_spin, iatom in 1:natoms + proj_range = (1+nproj_atom*(iatom-1)):(nproj_atom*iatom) + nhub[σ][proj_range, proj_range] = hubbard_n[σ, iatom, iatom] + E += filled_occ * 1/T(2) * term.U * + real(tr(hubbard_n[σ, iatom, iatom] * (I - hubbard_n[σ, iatom, iatom]))) + end + ops = [NonlocalOperator(basis, kpt, proj[ik], 1/T(2) * term.U * (I - 2*nhub[kpt.spin])) + for (ik,kpt) in enumerate(basis.kpoints)] + return (; E, ops) +end + """ compute_hubbard_n(term::TermHubbard, basis, ψ, occupation) @@ -134,60 +191,3 @@ function reshape_hubbard_proj(projectors::Vector{Matrix{Complex{T}}}, p_I end - -@doc raw""" -Hubbard energy, following the Dudarev et al. (1998) rotationally invariant formalism: -```math -1/2 Σ_{σI} U * Tr[hubbard_n[σ,I,I] * (1 - hubbard_n[σ,I,I])] -``` -""" -struct Hubbard{T} - manifold::OrbitalManifold - U::T - function Hubbard(manifold::OrbitalManifold, U) - U = austrip(U) - new{typeof(U)}(manifold, U) - end -end -function (hubbard::Hubbard)(basis::AbstractBasis) - check_hubbard_manifold(hubbard.manifold, basis.model) - - projs, labels = atomic_orbital_projectors(basis) - labels, projectors_matrix = extract_manifold(hubbard.manifold, projs, labels) - TermHubbard(hubbard.manifold, hubbard.U, projectors_matrix, labels) -end - -struct TermHubbard{T, PT, L} <: Term - manifold::OrbitalManifold - U::T - P::PT - labels::L -end - -@timing "ene_ops: hubbard" function ene_ops(term::TermHubbard, - basis::PlaneWaveBasis{T}, - ψ, occupation; hubbard_n=nothing, - kwargs...) where {T} - if isnothing(hubbard_n) - return (; E=zero(T), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) - end - proj = term.P - - filled_occ = filled_occupation(basis.model) - types = findall(at -> at.psp == term.manifold.psp, basis.model.atoms) - natoms = length(types) - n_spin = basis.model.n_spin_components - nproj_atom = size(hubbard_n[1,1,1], 1) # This is the number of projectors per atom, namely 2l+1 - # For the ops we have to reshape hubbard_n to match the NonlocalOperator structure, using a block diagonal form - nhub = [zeros(Complex{T}, nproj_atom*natoms, nproj_atom*natoms) for _ in 1:n_spin] - E = zero(T) - for σ in 1:n_spin, iatom in 1:natoms - proj_range = (1+nproj_atom*(iatom-1)):(nproj_atom*iatom) - nhub[σ][proj_range, proj_range] = hubbard_n[σ, iatom, iatom] - E += filled_occ * 1/T(2) * term.U * - real(tr(hubbard_n[σ, iatom, iatom] * (I - hubbard_n[σ, iatom, iatom]))) - end - ops = [NonlocalOperator(basis, kpt, proj[ik], 1/T(2) * term.U * (I - 2*nhub[kpt.spin])) - for (ik,kpt) in enumerate(basis.kpoints)] - return (; E, ops) -end From 7aab38e18c28c15df063d7185c6504289b8c25a8 Mon Sep 17 00:00:00 2001 From: Bruno Ploumhans <13494793+Technici4n@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:19:19 +0100 Subject: [PATCH 48/50] Review comments --- src/pseudo/NormConservingPsp.jl | 10 +++++++++ src/symmetry.jl | 9 ++++---- src/terms/hubbard.jl | 37 +++++++++++++++++++-------------- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/pseudo/NormConservingPsp.jl b/src/pseudo/NormConservingPsp.jl index ccff5bc874..b3088195a2 100644 --- a/src/pseudo/NormConservingPsp.jl +++ b/src/pseudo/NormConservingPsp.jl @@ -221,3 +221,13 @@ count_n_pswfc(psp::NormConservingPsp, l) = count_n_pswfc_radial(psp, l) * (2l + function count_n_pswfc(psp::NormConservingPsp) sum(l -> count_n_pswfc(psp, l), 0:psp.lmax; init=0)::Int end + +function find_pswfc(psp::NormConservingPsp, label::String) + for l = 0:psp.lmax, i = 1:count_n_pswfc_radial(psp, l) + if pswfc_label(psp, i, l) == label + return (; l, i) + end + end + error("Could not find pseudo atomic orbital with label $label " + * "in pseudopotential $(psp.identifier).") +end diff --git a/src/symmetry.jl b/src/symmetry.jl index 3e57d32c24..af22a2bdbf 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -425,9 +425,9 @@ end """ Symmetrize the Hubbard occupation matrix according to the l quantum number of the manifold. """ -function symmetrize_hubbard_n(manifold::OrbitalManifold, - hubbard_n::Array{Matrix{Complex{T}}}, - model, symmetries) where {T} +function symmetrize_hubbard_n(model, manifold::OrbitalManifold, + hubbard_n::Array{Matrix{Complex{T}}}; + symmetries, tol_symmetry=SYMMETRY_TOLERANCE) where {T} # For now we apply symmetries only on nII terms, not on cross-atom terms (nIJ) # WARNING: To implement +V this will need to be changed! @@ -442,7 +442,8 @@ function symmetrize_hubbard_n(manifold::OrbitalManifold, Wcart = model.lattice * symmetry.W * model.inv_lattice WigD = wigner_d_matrix(manifold.l, Wcart) for σ in 1:nspins, iatom in 1:natoms - sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry) + sym_atom = find_symmetry_preimage(positions, positions[iatom], symmetry; + tol_symmetry) ns[σ, iatom, iatom] .+= WigD' * hubbard_n[σ, sym_atom, sym_atom] * WigD end end diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index a71efc9978..a3b17343d1 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -32,32 +32,38 @@ function OrbitalManifold(psp::NormConservingPsp, iatoms::Vector{Int64}, label::A OrbitalManifold(psp, iatoms, l, i) end -function find_pswfc(psp::NormConservingPsp, label::String) - for l = 0:psp.lmax, i = 1:count_n_pswfc_radial(psp, l) - if pswfc_label(psp, i, l) == label - return (; l, i) - end - end - error("Could not find pseudo atomic orbital with label $label " - * "in pseudopotential $(psp.identifier).") -end - function check_hubbard_manifold(manifold::OrbitalManifold, model::Model) for atom in model.atoms[manifold.iatoms] atom isa ElementPsp || error("Orbital manifold elements must have a psp.") - atom.psp === manifold.psp || error("Orbital manifold psp does not match the psp of atom $atom") + atom.psp === manifold.psp || error("Orbital manifold psp $(manifold.psp.identifier) " * + "does not match the psp of atom $atom") end isempty(manifold.iatoms) && error("Orbital manifold has no atoms.") # Tricky: make sure that the iatoms are consistent with the symmetries of the model, # i.e. that the manifold would be a valid atom group - _check_symmetries(model.symmetries, model.lattice, [manifold.iatoms], model.positions) + atom_positions = model.positions[manifold.iatoms] + for symop in model.symmetries + W, w = symop.W, symop.w + for coord in atom_positions + # If all elements of a difference in diffs is integer, then + # W * coord + w and pos are equivalent lattice positions + if !any(c -> is_approx_integer(W * coord + w - c; atol=SYMMETRY_TOLERANCE), + atom_positions) + error("Inconsistency between orbital manifold and model symmetries: " * + "Cannot map the atom at position $coord to another atom of the manifold " * + "under the symmetry operation (W, w):\n($W, $w)") + end + end + end nothing end function extract_manifold(manifold::OrbitalManifold, projectors, labels) # We extract the labels only for orbitals belonging to the manifold - proj_indices = findall(orb -> orb.iatom ∈ manifold.iatoms && orb.l == manifold.l && orb.n == manifold.i, labels) + proj_indices = findall(orb -> (orb.iatom ∈ manifold.iatoms + && orb.l == manifold.l + && orb.n == manifold.i), labels) isempty(proj_indices) && @warn "Projector for $(manifold) not found." manifold_labels = labels[proj_indices] manifold_projectors = map(enumerate(projectors)) do (ik, projk) @@ -105,8 +111,7 @@ end proj = term.P filled_occ = filled_occupation(basis.model) - types = findall(at -> at.psp == term.manifold.psp, basis.model.atoms) - natoms = length(types) + natoms = length(term.manifold.iatoms) n_spin = basis.model.n_spin_components nproj_atom = size(hubbard_n[1,1,1], 1) # This is the number of projectors per atom, namely 2l+1 # For the ops we have to reshape hubbard_n to match the NonlocalOperator structure, using a block diagonal form @@ -164,7 +169,7 @@ function compute_hubbard_n(term::TermHubbard, hubbard_n[σ, idx, jdx] = mpi_sum(hubbard_n[σ, idx, jdx], basis.comm_kpts) end end - hubbard_n = symmetrize_hubbard_n(manifold, hubbard_n, basis.model, basis.symmetries) + hubbard_n = symmetrize_hubbard_n(basis.model, manifold, hubbard_n; basis.symmetries) end """ From 35336699078386e03335598449afc2e6d364d5c3 Mon Sep 17 00:00:00 2001 From: Francesco Sicignano Date: Thu, 13 Nov 2025 10:51:42 +0100 Subject: [PATCH 49/50] Minor typo in documentation --- src/terms/hubbard.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index a3b17343d1..a0a4764c4c 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -131,14 +131,14 @@ end """ compute_hubbard_n(term::TermHubbard, basis, ψ, occupation) -Computes a matrix hubbard_n of size (n_spin, natoms, natoms), where each entry hubbard_n[iatom, jatom] +Computes a matrix hubbard_n of size (n_spin, natoms, natoms), where each entry hubbard_n[σ, iatom, jatom] contains the submatrix of the occupation matrix corresponding to the projectors of atom iatom and atom jatom, with dimensions determined by the number of projectors for each atom. The atoms and orbitals are defined by the manifold tuple. hubbard_n[σ, iatom, jatom][m1, m2] = Σₖ₍ₛₚᵢₙ₎Σₙ weights[ik, ibnd] * ψₙₖ' * Pᵢₘ₁ * Pᵢₘ₂' * ψₙₖ -where n or ibnd is the band index, ``weights[ik ibnd] = kweights[ik] * occupation[ik, ibnd]`` +where n or ibnd is the band index, ``weights[ik, ibnd] = kweights[ik] * occupation[ik, ibnd]`` and ``Pᵢₘ₁`` is the pseudoatomic orbital projector for atom i and orbital m₁ (just the magnetic quantum number, since l is fixed, as is usual in the literature). For details on the projectors see `atomic_orbital_projectors`. @@ -161,7 +161,7 @@ function compute_hubbard_n(term::TermHubbard, for ik = krange_spin(basis, σ) j_projection = ψ[ik]' * projectors[ik][jdx] # <ψ|ϕJ> i_projection = projectors[ik][idx]' * ψ[ik] # <ϕI|ψ> - # Sums over the bands, dividing by filled_occ to deal + # Sums over the bands, dividing by filled_occ to deal # with the physical two spin channels separately hubbard_n[σ, idx, jdx] .+= (basis.kweights[ik] * i_projection * diagm(occupation[ik]/filled_occ) * j_projection) From 872c29a48325a4472534a3db7941896e3fa52390 Mon Sep 17 00:00:00 2001 From: Bruno Ploumhans <13494793+Technici4n@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:28:25 +0100 Subject: [PATCH 50/50] Reduce example running time, update manifold naming comment --- examples/hubbard.jl | 12 ++++++------ src/terms/hubbard.jl | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/hubbard.jl b/examples/hubbard.jl index 9a5ddf4473..720711ca32 100644 --- a/examples/hubbard.jl +++ b/examples/hubbard.jl @@ -23,8 +23,8 @@ magnetic_moments = [2, 0, -1, 0] # First, we run an SCF and band computation without the Hubbard term model = model_DFT(lattice, atoms, positions; temperature=5e-3, functionals=PBE(), magnetic_moments) -basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2]) -scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) +basis = PlaneWaveBasis(model; Ecut=20, kgrid=[2, 2, 2]) +scfres = self_consistent_field(basis; tol=1e-6, ρ=guess_density(basis, magnetic_moments)) bands = compute_bands(scfres, MonkhorstPack(4, 4, 4)) lowest_unocc_band = findfirst(ε -> ε-bands.εF > 0, bands.eigenvalues[1]) band_gap = bands.eigenvalues[1][lowest_unocc_band] - bands.eigenvalues[1][lowest_unocc_band-1] @@ -39,18 +39,18 @@ plot_pdos(bands; p, iatom=1, label="3D", colors=[:yellow, :orange], εrange) # To perform and Hubbard computation, we have to define the Hubbard manifold and associated constant. # # In DFTK there are a few ways to construct the `OrbitalManifold`. -# Here, we will apply the Hubbard correction on the 3D orbital of all Nickel atoms. +# Here, we will apply the Hubbard correction on the 3D orbital of all nickel atoms. # # Note that "manifold" is the standard term used in the literature for the set of atomic orbitals -# used to compute the Hubbard correction, but it is not a manifold in the mathematical sense. +# used to compute the Hubbard correction, but it is not meant in the mathematical sense. U = 10u"eV" manifold = OrbitalManifold(atoms, Ni, "3D") # Run SCF with a DFT+U setup, notice the `extra_terms` keyword argument, setting up the Hubbard +U term. model = model_DFT(lattice, atoms, positions; extra_terms=[Hubbard(manifold, U)], functionals=PBE(), temperature=5e-3, magnetic_moments) -basis = PlaneWaveBasis(model; Ecut=32, kgrid=[2, 2, 2] ) -scfres = self_consistent_field(basis; tol=1e-10, ρ=guess_density(basis, magnetic_moments)) +basis = PlaneWaveBasis(model; Ecut=20, kgrid=[2, 2, 2]) +scfres = self_consistent_field(basis; tol=1e-6, ρ=guess_density(basis, magnetic_moments)) # Run band computation bands_hub = compute_bands(scfres, MonkhorstPack(4, 4, 4)) diff --git a/src/terms/hubbard.jl b/src/terms/hubbard.jl index a0a4764c4c..d14303790e 100644 --- a/src/terms/hubbard.jl +++ b/src/terms/hubbard.jl @@ -4,8 +4,9 @@ using Random """ Structure for Hubbard manifold choice and projectors extraction. -It is to be noted that, despite the name used in literature, this is -not a manifold in the mathematical sense. +"Manifold" is the standard name used in the literature to refer +to the set of atomic orbitals used to compute the Hubbard correction. +It is to be noted that this is not meant in the mathematical sense of "manifold". Overview of fields: - `psp`: Pseudopotential containing the atomic orbital projectors