/ DFTK.jl Public
Switch branches/tags
 include("scf_callbacks.jl") # Struct to store some options for forward-diff / reverse-diff response # (unused in primal calculations) @kwdef struct ResponseOptions verbose = false end """ Obtain new density ρ by diagonalizing ham. Follows the policy imposed by the bands data structure to determine and adjust the number of bands to be computed. """ function next_density(ham::Hamiltonian, nbandsalg::NbandsAlgorithm=AdaptiveBands(ham.basis.model), fermialg::AbstractFermiAlgorithm=default_fermialg(ham.basis.model); eigensolver=lobpcg_hyper, ψ=nothing, eigenvalues=nothing, occupation=nothing, kwargs...) n_bands_converge, n_bands_compute = determine_n_bands(nbandsalg, occupation, eigenvalues, ψ) if isnothing(ψ) increased_n_bands = true else @assert length(ψ) == length(ham.basis.kpoints) n_bands_compute = max(n_bands_compute, maximum(ψk -> size(ψk, 2), ψ)) increased_n_bands = n_bands_compute > size(ψ, 2) end # TODO Synchronize since right now it is assumed that the same number of bands are # computed for each k-Point n_bands_compute = mpi_max(n_bands_compute, ham.basis.comm_kpts) eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands_compute; ψguess=ψ, n_conv_check=n_bands_converge, kwargs...) eigres.converged || (@warn "Eigensolver not converged" iterations=eigres.iterations) # Check maximal occupation of the unconverged bands is sensible. occupation, εF = compute_occupation(ham.basis, eigres.λ, fermialg; tol_n_elec=nbandsalg.occupation_threshold) minocc = maximum(minimum, occupation) # TODO This is a bit hackish, but needed right now as we increase the number of bands # to be computed only between SCF steps. Should be revisited once we have a better # way to deal with such things in LOBPCG. if !increased_n_bands && minocc > nbandsalg.occupation_threshold @warn("Detected large minimal occupation $minocc. SCF could be unstable. " * "Try switching to adaptive band selection (nbandsalg=AdaptiveBands(model)) " * "or request more converged bands than$n_bands_converge (e.g. " * "nbandsalg=AdaptiveBands(model; n_bands_converge=\$(n_bands_converge + 3))") end ρout = compute_density(ham.basis, eigres.X, occupation; nbandsalg.occupation_threshold) (; ψ=eigres.X, eigenvalues=eigres.λ, occupation, εF, ρout, diagonalization=eigres, n_bands_converge, nbandsalg.occupation_threshold) end @doc raw""" self_consistent_field(basis; [tol, mixing, damping, ρ, ψ]) Solve the Kohn-Sham equations with a density-based SCF algorithm using damped, preconditioned iterations where ρ_\text{next} = α P^{-1} (ρ_\text{out} - ρ_\text{in}). Overview of parameters: - ρ: Initial density - ψ: Initial orbitals - tol: Tolerance for the density change (\|ρ_\text{out} - ρ_\text{in}\|) to flag convergence. Default is 1e-6. - is_converged: Convergence control callback. Typical objects passed here are DFTK.ScfConvergenceDensity(tol) (the default), DFTK.ScfConvergenceEnergy(tol) or DFTK.ScfConvergenceForce(tol). - maxiter: Maximal number of SCF iterations - mixing: Mixing method, which determines the preconditioner P^{-1} in the above equation. Typical mixings are [LdosMixing](@ref), [KerkerMixing](@ref), [SimpleMixing](@ref) or [DielectricMixing](@ref). Default is LdosMixing() - damping: Damping parameter α in the above equation. Default is 0.8. - nbandsalg: By default DFTK uses nbandsalg=AdaptiveBands(model), which adaptively determines the number of bands to compute. If you want to influence this algorithm or use a predefined number of bands in each SCF step, pass a [FixedBands](@ref) or [AdaptiveBands](@ref). - callback: Function called at each SCF iteration. Usually takes care of printing the intermediate state. """ @timing function self_consistent_field( basis::PlaneWaveBasis{T}; ρ=guess_density(basis), ψ=nothing, tol=1e-6, is_converged=ScfConvergenceDensity(tol), maxiter=100, mixing=LdosMixing(), damping=0.8, solver=scf_anderson_solver(), eigensolver=lobpcg_hyper, determine_diagtol=ScfDiagtol(), nbandsalg::NbandsAlgorithm=AdaptiveBands(basis.model), fermialg::AbstractFermiAlgorithm=default_fermialg(basis.model), callback=ScfDefaultCallback(; show_damping=false), compute_consistent_energies=true, response=ResponseOptions(), # Dummy here, only for AD ) where {T} # All these variables will get updated by fixpoint_map if !isnothing(ψ) @assert length(ψ) == length(basis.kpoints) end occupation = nothing eigenvalues = nothing ρout = ρ εF = nothing n_iter = 0 energies = nothing ham = nothing info = (; n_iter=0, ρin=ρ) # Populate info with initial values converged = false # We do density mixing in the real representation # TODO support other mixing types function fixpoint_map(ρin) converged && return ρin # No more iterations if convergence flagged 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) # Diagonalize ham to get the new state nextstate = next_density(ham, nbandsalg, fermialg; eigensolver, ψ, eigenvalues, occupation, miniter=1, tol=determine_diagtol(info)) ψ, eigenvalues, occupation, εF, ρout = nextstate # Update info with results gathered so far info = (; ham, basis, converged, stage=:iterate, algorithm="SCF", ρin, ρout, α=damping, n_iter, nbandsalg.occupation_threshold, nextstate..., diagonalization=[nextstate.diagonalization]) # Compute the energy of the new state if compute_consistent_energies energies = energy_hamiltonian(basis, ψ, occupation; ρ=ρout, eigenvalues, εF).energies end info = merge(info, (; energies)) # Apply mixing and pass it the full info as kwargs δρ = mix_density(mixing, basis, ρout - ρin; info...) ρnext = ρin .+ T(damping) .* δρ info = merge(info, (; ρnext)) callback(info) is_converged(info) && (converged = true) ρnext end # Tolerance and maxiter are only dummy here: Convergence is flagged by is_converged # inside the fixpoint_map. solver(fixpoint_map, ρout, maxiter; tol=eps(T)) # 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 energies, ham = energy_hamiltonian(basis, ψ, occupation; ρ=ρout, eigenvalues, εF) # Measure for the accuracy of the SCF # TODO probably should be tracked all the way ... norm_Δρ = norm(info.ρout - info.ρin) * sqrt(basis.dvol) # Callback is run one last time with final state to allow callback to clean up info = (; ham, basis, energies, converged, nbandsalg.occupation_threshold, ρ=ρout, α=damping, eigenvalues, occupation, εF, info.n_bands_converge, n_iter, ψ, info.diagonalization, stage=:finalize, algorithm="SCF", norm_Δρ) callback(info) info end