diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 39d23fff17..e4847be22d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.5.8 +current_version = 0.5.11 commit = True tag = False diff --git a/CITATION.bib b/CITATION.bib index c2137ba20d..1d53810a1f 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -1,6 +1,6 @@ % Extended abstract describing DFTK along with a short summary of its features @article{DFTKjcon, - author = {Michael F. Herbst and Antoine Levitt and Eric Cancès}, + author = {Herbst, Michael F. and Levitt, Antoine and Cancès, Eric}, doi = {10.21105/jcon.00069}, journal = {Proc. JuliaCon Conf.}, title = {DFTK: A Julian approach for simulating electrons in solids}, @@ -9,13 +9,33 @@ @article{DFTKjcon year = {2021}, } +% Paper describing the calculation of response properties in DFTK. +% Used for forward-diff derivatives, `solve_ΩplusK_split` and `apply_χ0` functions. +@unpublished{ResponseCalculations, + author = {Cancès, Eric and Herbst, Michael F. and Kemlin, Gaspard and Levitt, Antoine and Stamm, Benjamin}, + title = {Numerical Stability and Efficiency of Response Property Calculations in Density Functional Theory}, + year = {Submitted}, + note = {https://arxiv.org/abs/2210.04512}, +} + +% Paper describing the energy cutoff smearing method `BlowupCHV` +@unpublished{BlowupCHV, + author = {Cancès, Eric and Hassan, Muhammad and Vidal, Laurent Vidal}, + title = {Modified-Operator Method for the Calculation of Band Diagrams of Crystalline Materials}, + year = {Submitted}, + note = {https://hal.archives-ouvertes.fr/hal-03794000/}, +} + % Paper describing the adaptive damping strategy implemented by % the scf_potential_mixing_adaptive function -@unpublished{AdaptiveDamping, +@article{AdaptiveDamping, author = {Herbst, Michael F. and Levitt, Antoine}, + doi = {10.1016/j.jcp.2022.111127}, + journal = {Journal of Computational Physics}, title = {A robust and efficient line search for self-consistent field iterations}, - year = {submitted}, - note = {arXiv:2109.14018}, + volume = {459}, + pages = {111127}, + year = {2022}, } % Paper describing the HybridMixing, DielectricMixing and LdosMixing SCF preconditioners diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..4f068dab15 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,27 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: 'DFTK: The Density-functional toolkit' +message: Cite this paper whenever you use DFTK. +type: software +authors: + - email: herbst@acom.rwth-aachen.de + given-names: Michael F. + family-names: Herbst + affiliation: RWTH Aachen University + orcid: 'https://orcid.org/0000-0003-0378-7921' + - family-names: Levitt + given-names: Antoine + email: antoine.levitt@inria.fr + affiliation: Inria Paris + - given-names: Eric + family-names: Cancès + email: eric.cances@enpc.fr + affiliation: 'CERMICS, Ecole des Ponts' +identifiers: + - type: doi + value: 10.21105/jcon.00069 + description: Extended abstract describing the software +url: 'https://dftk.org' +license: MIT diff --git a/Project.toml b/Project.toml index d6af832343..9e27ac6225 100644 --- a/Project.toml +++ b/Project.toml @@ -1,12 +1,11 @@ name = "DFTK" uuid = "acf6eb54-70d9-11e9-0013-234b7a5f5337" authors = ["Michael F. Herbst ", "Antoine Levitt "] -version = "0.5.8" +version = "0.5.11" [deps] AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" AtomsBase = "a963bdd2-2df7-4f54-a1ee-49d51e6be12a" -BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" Brillouin = "23470ee3-d0df-4052-8b1a-8cbd6363e7f0" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" @@ -14,6 +13,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" DftFunctionals = "6bd331d2-b28d-4fd3-880e-1a1c7f37947f" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" InteratomicPotentials = "a9efe35a-c65d-452d-b8a8-82646cd5cb04" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" @@ -48,13 +48,13 @@ spglib_jll = "ac4a9f1e-bdb2-5204-990c-47c8b2f70d4e" [compat] AbstractFFTs = "1" AtomsBase = "0.2.2" -BlockArrays = "0.16.2" -Brillouin = "0.5 - 0.5.8" # Upper bound temporary until memory bug resolved. +Brillouin = "0.5.4" ChainRulesCore = "1.15" Conda = "1" DftFunctionals = "0.2" FFTW = "1" ForwardDiff = "0.10" +GPUArraysCore = "0.1" InteratomicPotentials = "0.2" Interpolations = "0.12, 0.13, 0.14" IterTools = "1" @@ -62,7 +62,7 @@ IterativeSolvers = "0.8, 0.9" Libxc = "0.3.9" LineSearches = "7" LinearMaps = "2, 3" -MPI = "0.19" +MPI = "0.19, 0.20" NLsolve = "4" Optim = "1" OrderedCollections = "1" @@ -84,6 +84,7 @@ spglib_jll = "1.15" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" FiniteDiff = "6a86dc24-6348-571c-b903-95158fe2bd41" @@ -101,4 +102,4 @@ WriteVTK = "64499a7a-5c06-52f2-abe2-ccb03c286192" wannier90_jll = "c5400fa0-8d08-52c2-913f-1e3f656c1ce9" [targets] -test = ["Test", "Aqua", "DoubleFloats", "FiniteDiff", "FiniteDifferences", "GenericLinearAlgebra", "IntervalArithmetic", "Plots", "Random", "KrylovKit", "Logging", "JLD2", "WriteVTK", "wannier90_jll", "QuadGK", "ComponentArrays"] +test = ["Test", "Aqua", "CUDA", "DoubleFloats", "FiniteDiff", "FiniteDifferences", "GenericLinearAlgebra", "IntervalArithmetic", "Plots", "Random", "KrylovKit", "Logging", "JLD2", "WriteVTK", "wannier90_jll", "QuadGK", "ComponentArrays"] diff --git a/README.md b/README.md index d6ee493e7b..83759ec6f1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ | **Documentation** | **Build Status** | **License** | |:--------------------------------------------------------------------------------- |:----------------------------------------------- |:-------------------------------- | -| [![][docs-img]][docs-url] [![][ddocs-img]][ddocs-url] [![][slack-img]][slack-url] | [![][ci-img]][ci-url] [![][ccov-img]][ccov-url] | [![][license-img]][license-url] | +| [![][docs-img]][docs-url] [![][ddocs-img]][ddocs-url] [![][zulip-img]][zulip-url] | [![][ci-img]][ci-url] [![][ccov-img]][ccov-url] | [![][license-img]][license-url] | [ddocs-img]: https://img.shields.io/badge/docs-dev-blue.svg [ddocs-url]: https://docs.dftk.org/dev @@ -12,8 +12,8 @@ [docs-img]: https://img.shields.io/badge/docs-stable-blue.svg [docs-url]: https://docs.dftk.org/stable -[slack-img]: https://img.shields.io/badge/chat-on_slack-808493.svg?logo=slack -[slack-url]: https://join.slack.com/t/juliamolsim/shared_invite/zt-tc060co0-HgiKApazzsQzBHDlQ58A7g +[zulip-img]: https://img.shields.io/badge/chat-on_zulip-808493.svg?logo=zulip +[zulip-url]: https://juliamolsim.zulipchat.com/#narrow/stream/332493-dftk [ci-img]: https://github.com/JuliaMolSim/DFTK.jl/workflows/CI/badge.svg?branch=master&event=push [ci-url]: https://github.com/JuliaMolSim/DFTK.jl/actions @@ -76,4 +76,4 @@ on github. If you want to contribute but are unsure where to start, take a look at the list of issues tagged [good first issue](https://github.com/JuliaMolSim/DFTK.jl/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) (relatively easy tasks suitable for newcomers) or [help wanted](https://github.com/JuliaMolSim/DFTK.jl/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) (more sizeable but well-defined and isolated). -Don't hesitate to ask for help, through github, email or the [JuliaMolSim slack][slack-url]. +Don't hesitate to ask for help, through github, email or the [JuliaMolSim zulip chat][zulip-url]. diff --git a/docs/Project.toml b/docs/Project.toml index 60144fa699..bff79cff1c 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,6 @@ [deps] AtomsBase = "a963bdd2-2df7-4f54-a1ee-49d51e6be12a" +Brillouin = "23470ee3-d0df-4052-8b1a-8cbd6363e7f0" DFTK = "acf6eb54-70d9-11e9-0013-234b7a5f5337" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" diff --git a/docs/src/guide/tutorial.jl b/docs/src/guide/tutorial.jl index b156e41fba..614f251057 100644 --- a/docs/src/guide/tutorial.jl +++ b/docs/src/guide/tutorial.jl @@ -82,10 +82,10 @@ hcat(scfres.eigenvalues...) # k-point because there are 4 occupied states in the system (4 valence # electrons per silicon atom, two atoms per unit cell, and paired # spins), and the eigensolver gives itself some breathing room by -# computing some extra states (see `n_ep_extra` argument to -# `self_consistent_field`). There are only 8 k-points (instead of -# 4x4x4) because symmetry has been used to reduce the amount of -# computations to just the irreducible k-points (see +# computing some extra states (see the `bands` argument to +# `self_consistent_field` as well as the [`AdaptiveBands`](@ref) documentation). +# There are only 8 k-points (instead of 4x4x4) because symmetry has been used to reduce the +# amount of computations to just the irreducible k-points (see #md # [Crystal symmetries](@ref) #nb # [Crystal symmetries](https://docs.dftk.org/stable/developer/symmetries/) # for details). diff --git a/docs/src/publications.md b/docs/src/publications.md index c0be980dbd..8936b3f973 100644 --- a/docs/src/publications.md +++ b/docs/src/publications.md @@ -20,9 +20,20 @@ The current DFTK reference paper to cite is Additionally the following publications describe DFTK or one of its algorithms: +- E. Cancès, M. F. Herbst, G. Kemlin, A. Levitt and B. Stamm. + [*Numerical stability and efficiency of response property calculations in density functional theory*](https://arxiv.org/abs/2210.04512) + (Submitted). + [ArXiv:2210.04512](https://arxiv.org/abs/2210.04512). + ([Supplementary material and computational scripts](https://github.com/gkemlin/response-calculations-metals)). + +- E. Cancès, M. Hassan and L. Vidal. + [*Modified-Operator Method for the Calculation of Band Diagrams of Crystalline Materials.*](https://hal.archives-ouvertes.fr/hal-03794000) + (Submitted). + [hal-03794000](https://hal.archives-ouvertes.fr/hal-03794000). + - M. F. Herbst and A. Levitt. [*A robust and efficient line search for self-consistent field iterations*](https://arxiv.org/abs/2109.14018) - (Submitted). + Journal of Computational Physics, **459**, 111127 (2022). [ArXiv:2109.14018](https://arxiv.org/abs/2109.14018). ([Supplementary material and computational scripts](https://github.com/mfherbst/supporting-adaptive-damping/)). @@ -41,12 +52,28 @@ Additionally the following publications describe DFTK or one of its algorithms: The following publications report research employing DFTK as a core component. Feel free to drop us a line if you want your work to be added here. +- J. Cazalis. + [*Dirac cones for a mean-field model of graphene*](https://arxiv.org/abs/2207.09893) + (Submitted). + [ArXiv:2207.09893](https://arxiv.org/abs/2207.09893). + ([Computational script](https://github.com/JuliaMolSim/DFTK.jl/blob/f7fcc31c79436b2582ac1604d4ed8ac51a6fd3c8/examples/publications/2022_cazalis.jl)). + +- E. Cancès, L. Garrigue, D. Gontier. + [*A simple derivation of moiré-scale continuous models for twisted bilayer graphene*](https://arxiv.org/abs/2206.05685) + (Submitted). + [ArXiv:2206.05685](https://arxiv.org/abs/2206.05685). + - E. Cancès, G. Dusson, G. Kemlin and A. Levitt. [*Practical error bounds for properties in plane-wave electronic structure calculations*](https://arxiv.org/abs/2111.01470) (Submitted). [ArXiv:2111.01470](https://arxiv.org/abs/2111.01470). ([Supplementary material and computational scripts](https://github.com/gkemlin/paper-forces-estimator)). +- G. Dusson, I. Sigal and B. Stamm. + [*Analysis of the Feshbach-Schur method for the Fourier spectral discretizations of Schrödinger operators*](http://dx.doi.org/10.1090/mcom/3774) + Mathematics of Computation, **?**, ?? (2022). + [ArXiv:2008.10871](https://arxiv.org/abs/2008.10871). + - E. Cancès, G. Kemlin and A. Levitt. [*Convergence analysis of direct minimization and self-consistent iterations*](https://doi.org/10.1137/20M1332864) SIAM Journal on Matrix Analysis and Applications, **42**, 243 (2021). diff --git a/examples/cohen_bergstresser.jl b/examples/cohen_bergstresser.jl index 9ed92d0eae..5aa1a3b1b5 100644 --- a/examples/cohen_bergstresser.jl +++ b/examples/cohen_bergstresser.jl @@ -18,12 +18,12 @@ lattice = Si.lattice_constant / 2 .* [[0 1 1.]; [1 0 1.]; [1 1 0.]]; # Next we build the rather simple model and discretize it with moderate `Ecut`: model = Model(lattice, atoms, positions; terms=[Kinetic(), AtomicLocal()]) -basis = PlaneWaveBasis(model, Ecut=10.0, kgrid=(1, 1, 1)); +basis = PlaneWaveBasis(model, Ecut=10.0, kgrid=(2, 2, 2)); # We diagonalise at the Gamma point to find a Fermi level … ham = Hamiltonian(basis) eigres = diagonalize_all_kblocks(DFTK.lobpcg_hyper, ham, 6) -εF = DFTK.compute_occupation(basis, eigres.λ; occupation_threshold=0).εF +εF = DFTK.compute_occupation(basis, eigres.λ).εF # … and compute and plot 8 bands: using Plots diff --git a/examples/energy_cutoff_smearing.jl b/examples/energy_cutoff_smearing.jl index 3483eb79e7..e6f6f76c13 100644 --- a/examples/energy_cutoff_smearing.jl +++ b/examples/energy_cutoff_smearing.jl @@ -26,13 +26,9 @@ using DFTK using Statistics -a0 = 10.26 # Experimental lattice constant of silicon in bohr +a0 = 10.26 # Experimental lattice constant of silicon in bohr a_list = range(a0 - 1/2, a0 + 1/2; length=20) -Ecut = 5 # very low Ecut to display big irregularities -kgrid = (2, 2, 2) # very sparse k-grid to fasten convergence -n_bands = 8 # Standard number of bands for silicon - function compute_ground_state_energy(a; Ecut, kgrid, kinetic_blowup, kwargs...) lattice = a / 2 * [[0 1 1.]; [1 0 1.]; @@ -42,12 +38,12 @@ function compute_ground_state_energy(a; Ecut, kgrid, kinetic_blowup, kwargs...) positions = [ones(3)/8, -ones(3)/8] model = model_PBE(lattice, atoms, positions; kinetic_blowup) basis = PlaneWaveBasis(model; Ecut, kgrid) - self_consistent_field(basis; kwargs...).energies.total + self_consistent_field(basis; callback=identity, kwargs...).energies.total end -callback = info->nothing # set SCF to non verbose -E0_naive = compute_ground_state_energy.(a_list; kinetic_blowup=BlowupIdentity(), - Ecut, kgrid, n_bands, callback); +Ecut = 5 # Very low Ecut to display big irregularities +kgrid = (2, 2, 2) # Very sparse k-grid to speed up convergence +E0_naive = compute_ground_state_energy.(a_list; kinetic_blowup=BlowupIdentity(), Ecut, kgrid); # To be compared with the same computation for a high `Ecut=100`. The naive approximation # of the energy is shifted for the legibility of the plot. @@ -74,8 +70,7 @@ plot!(p, a_list, E0_ref, label="Ecut=100", color=2) # that is mathematically ensured to provide ``C^2`` regularity of the energy bands. # Let us lauch the computation again with the modified kinetic term. -E0_modified = compute_ground_state_energy.(a_list; kinetic_blowup=BlowupCHV(), - Ecut, kgrid, n_bands, callback, ); +E0_modified = compute_ground_state_energy.(a_list; kinetic_blowup=BlowupCHV(), Ecut, kgrid,); # !!! note "Abinit energy cutoff smearing option" # For the sake of completeness, DFTK also provides the blow-up function `BlowupAbinit` @@ -93,8 +88,7 @@ E0_modified = compute_ground_state_energy.(a_list; kinetic_blowup=BlowupCHV(), estimate_a0(E0_values) = a_list[findmin(E0_values)[2]] a0_naive, a0_ref, a0_modified = estimate_a0.([E0_naive, E0_ref, E0_modified]) -shift = mean(abs.(E0_modified .- E0_ref)) # again, shift for legibility of the plot - +shift = mean(abs.(E0_modified .- E0_ref)) # Shift for legibility of the plot plot!(p, a_list, E0_modified .- shift, label="Ecut=5 + BlowupCHV", color=3) vline!(p, [a0], label="experimental a0", linestyle=:dash, linecolor=:black) vline!(p, [a0_naive], label="a0 Ecut=5", linestyle=:dash, color=1) diff --git a/examples/graphene.jl b/examples/graphene.jl index ee7513ec49..51033cb973 100644 --- a/examples/graphene.jl +++ b/examples/graphene.jl @@ -32,27 +32,7 @@ model = model_PBE(lattice, atoms, positions; temperature) basis = PlaneWaveBasis(model; Ecut, kgrid) scfres = self_consistent_field(basis) -## Choose the points of the band diagram, in reduced coordinates (in the (b1,b2) basis) -Γ = [0, 0, 0] -K = [ 1, 1, 0]/3 -Kp = [-1, 2, 0]/3 -M = (K + Kp)/2 -kpath_coords = [Γ, K, M, Γ] -kpath_names = ["Γ", "K", "M", "Γ"] - -## Build the path manually for now -kline_density = 20 -function build_path(k1, k2) - target_Δk = 1/kline_density # the actual Δk is |k2-k1|/npt - npt = ceil(Int, norm(model.recip_lattice * (k2-k1)) / target_Δk) - [k1 + t * (k2-k1) for t in range(0, 1, length=npt)] -end -kcoords = [] -for i = 1:length(kpath_coords)-1 - append!(kcoords, build_path(kpath_coords[i], kpath_coords[i+1])) -end -klabels = Dict(zip(kpath_names, kpath_coords)) - -## Plot the bands -band_data = compute_bands(basis, kcoords; scfres.ρ) -DFTK.plot_band_data(band_data; scfres.εF, klabels) +## Construct 2D path through Brillouin zone +sgnum = 13 # Graphene space group number +kpath = irrfbz_path(model; dim=2, sgnum) +plot_bandstructure(scfres, kpath; kline_density=20) diff --git a/examples/publications/2022_cazalis.jl b/examples/publications/2022_cazalis.jl index 7a81d49988..b8093c7609 100644 --- a/examples/publications/2022_cazalis.jl +++ b/examples/publications/2022_cazalis.jl @@ -1,5 +1,5 @@ # Model of graphene confined to 2 spatial dimensions studied -# in the paper by Cazalis (arxiv, 2022, TODO add ref) +# in the paper by Cazalis (arxiv, 2022, https://arxiv.org/abs/2207.09893) # The pure 3D Coulomb 1/|x| interaction is used, without pseudopotential. using DFTK @@ -59,30 +59,9 @@ basis = PlaneWaveBasis(model; Ecut, kgrid) ## Run SCF scfres = self_consistent_field(basis, tol=1e-10) -## Choose the points of the band diagram, in reduced coordinates (in the (b1,b2) basis) -Γ = [0, 0, 0] -K = [ 1, 1, 0]/3 -Kp = [-1, 2, 0]/3 -M = (K + Kp)/2 -kpath_coords = [Γ, K, M, Γ] -kpath_names = ["Γ", "K", "M", "Γ"] - -## Build the path -kline_density = 20 -function build_path(k1, k2) - target_Δk = 1/kline_density # the actual Δk is |k2-k1|/npt - npt = ceil(Int, norm(model.recip_lattice * (k2-k1)) / target_Δk) - [k1 + t * (k2-k1) for t in range(0, 1, length=npt)] -end -kcoords = [] -for i = 1:length(kpath_coords)-1 - append!(kcoords, build_path(kpath_coords[i], kpath_coords[i+1])) -end -klabels = Dict(kpath_names[i] => kpath_coords[i] for i=1:length(kpath_coords)) - -## Plot the bands -band_data = compute_bands(basis, kcoords; n_bands=5, scfres.ρ) -p = DFTK.plot_band_data(band_data; klabels, markersize=nothing) +## Plot bands +sgnum = 13 # Graphene space group number +p = plot_bandstructure(scfres, irrfbz_path(model; sgnum); n_bands=5) Plots.hline!(p, [scfres.εF], label="", color="black") -Plots.ylims!(p, -Inf,Inf) +Plots.ylims!(p, (-Inf, Inf)) p diff --git a/examples/supercells.jl b/examples/supercells.jl index 09bbae0118..81445464ae 100644 --- a/examples/supercells.jl +++ b/examples/supercells.jl @@ -84,7 +84,7 @@ self_consistent_field(aluminium_setup(2); is_converged); #- -self_consistent_field(aluminium_setup(4); is_converged, n_bands=30); +self_consistent_field(aluminium_setup(4); is_converged); # When switching off explicitly the `LdosMixing`, by selecting `mixing=SimpleMixing()`, # the performance of number of required SCF steps starts to increase as we increase @@ -94,7 +94,7 @@ self_consistent_field(aluminium_setup(1); is_converged, mixing=SimpleMixing()); #- -self_consistent_field(aluminium_setup(4); is_converged, mixing=SimpleMixing(), n_bands=30); +self_consistent_field(aluminium_setup(4); is_converged, mixing=SimpleMixing()); # For completion let us note that the more traditional `mixing=KerkerMixing()` # approach would also help in this particular setting to obtain a constant diff --git a/examples/wannier90.jl b/examples/wannier90.jl index 46c50b62c7..cfc31c53fa 100644 --- a/examples/wannier90.jl +++ b/examples/wannier90.jl @@ -29,7 +29,8 @@ atoms = [C, C] positions = [[0.0, 0.0, 0.0], [1//3, 2//3, 0.0]] model = model_PBE(lattice, atoms, positions) basis = PlaneWaveBasis(model; Ecut=15, kgrid=[5, 5, 1]) -scfres = self_consistent_field(basis; n_bands=15, tol=1e-8); +nbandsalg = AdaptiveBands(basis; n_bands_converge=15) +scfres = self_consistent_field(basis; nbandsalg, tol=1e-8); # Plot bandstructure of the system diff --git a/src/DFTK.jl b/src/DFTK.jl index 2bbe39762e..d8998ae7bb 100644 --- a/src/DFTK.jl +++ b/src/DFTK.jl @@ -30,6 +30,7 @@ include("common/mpi.jl") include("common/threading.jl") include("common/printing.jl") include("common/cis2pi.jl") +include("common/zeros_like.jl") export PspHgh include("pseudo/NormConservingPsp.jl") @@ -106,7 +107,8 @@ export total_density export spin_density export ρ_from_total_and_spin include("densities.jl") -include("interpolation_transfer.jl") +include("transfer.jl") +include("interpolation.jl") export compute_transfer_matrix export transfer_blochwave export transfer_blochwave_kpt @@ -125,6 +127,7 @@ include("standard_models.jl") export KerkerMixing, KerkerDosMixing, SimpleMixing, DielectricMixing export LdosMixing, HybridMixing, χ0Mixing +export FixedBands, AdaptiveBands export scf_nlsolve_solver export scf_damping_solver export scf_anderson_solver @@ -137,6 +140,7 @@ export load_scfres, save_scfres include("scf/chi0models.jl") include("scf/mixing.jl") include("scf/scf_solvers.jl") +include("scf/nbands_algorithm.jl") include("scf/self_consistent_field.jl") include("scf/direct_minimization.jl") include("scf/newton.jl") @@ -177,8 +181,8 @@ include("external/pymatgen.jl") include("external/stubs.jl") # Function stubs for conditionally defined methods export compute_bands -export high_symmetry_kpath export plot_bandstructure +export irrfbz_path include("postprocess/band_structure.jl") export compute_forces @@ -192,14 +196,16 @@ export plot_dos include("postprocess/dos.jl") export compute_χ0 export apply_χ0 +include("response/cg.jl") include("response/chi0.jl") include("response/hessian.jl") export compute_current include("postprocess/current.jl") -# ForwardDiff workarounds +# Workarounds include("workarounds/dummy_inplace_fft.jl") include("workarounds/forwarddiff_rules.jl") +include("workarounds/gpu_arrays.jl") function __init__() @@ -216,12 +222,15 @@ function __init__() @require DoubleFloats="497a8b3b-efae-58df-a0af-a86822472b78" begin !isdefined(DFTK, :GENERIC_FFT_LOADED) && include("workarounds/fft_generic.jl") end - @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" include("plotting.jl") - @require JLD2="033835bb-8acc-5ee8-8aae-3f567f8a3819" include("external/jld2io.jl") + @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" include("plotting.jl") + @require JLD2="033835bb-8acc-5ee8-8aae-3f567f8a3819" include("external/jld2io.jl") @require WriteVTK="64499a7a-5c06-52f2-abe2-ccb03c286192" include("external/vtkio.jl") @require wannier90_jll="c5400fa0-8d08-52c2-913f-1e3f656c1ce9" begin include("external/wannier90.jl") end + @require CUDA="052768ef-5323-5732-b1bb-66c8b64840ba" begin + include("workarounds/cuda_arrays.jl") + end end end # module DFTK diff --git a/src/Model.jl b/src/Model.jl index 6f9e0c90cd..c203d5f95c 100644 --- a/src/Model.jl +++ b/src/Model.jl @@ -21,8 +21,10 @@ struct Model{T <: Real, VT <: Real} unit_cell_volume::T recip_cell_volume::T - # Electrons, occupation and smearing function - n_electrons::Int # usually consistent with `atoms` field, but doesn't have to + # Computations can be performed at fixed `n_electrons` (`n_electrons` Int, `εF` nothing), + # or fixed Fermi level (expert option, `n_electrons` nothing, `εF` T) + n_electrons::Union{Int, Nothing} + εF::Union{T, Nothing} # spin_polarization values: # :none No spin polarization, αα and ββ density identical, @@ -94,28 +96,44 @@ function Model(lattice::AbstractMatrix{T}, atoms::Vector{<:Element}=Element[], positions::Vector{<:AbstractVector}=Vec3{T}[]; model_name="custom", - n_electrons::Int=sum(n_elec_valence, atoms; init=0), + εF=nothing, + n_electrons::Union{Int,Nothing}=isnothing(εF) ? + n_electrons_from_atoms(atoms) : nothing, + # Force electrostatics with non-neutral cells; results not guaranteed. + # Set to `true` by default for charged systems. + disable_electrostatics_check=all(iszero, charge_ionic.(atoms)), magnetic_moments=T[], terms=[Kinetic()], temperature=zero(T), - smearing=nothing, + smearing=temperature > 0 ? Smearing.FermiDirac() : Smearing.None(), spin_polarization=default_spin_polarization(magnetic_moments), symmetries=default_symmetries(lattice, atoms, positions, magnetic_moments, spin_polarization, terms), ) where {T <: Real} - lattice = Mat3{T}(lattice) - temperature = T(austrip(temperature)) + # Validate εF and n_electrons + if !isnothing(εF) # fixed Fermi level + if !isnothing(n_electrons) + error("Cannot have both a given `n_electrons` and a fixed Fermi level `εF`.") + end + if !disable_electrostatics_check + error("Coulomb electrostatics is incompatible with fixed Fermi level.") + end + else # fixed number of electrons + n_electrons < 0 && error("n_electrons should be non-negative.") + if !disable_electrostatics_check && n_electrons_from_atoms(atoms) != n_electrons + error("Support for non-neutral cells is experimental and likely broken.") + end + end - # Atoms and electrons + # Atoms and terms if length(atoms) != length(positions) error("Length of atoms and positions vectors need to agree.") end - n_electrons < 0 && error("n_electrons should be non-negative. Ensure to provide a " * - "non-empty atoms list or an appropriate `n_electrons` kwarg.") isempty(terms) && error("Model without terms not supported.") atom_groups = [findall(Ref(pot) .== atoms) for pot in Set(atoms)] # Special handling of 1D and 2D systems, and sanity checks + lattice = Mat3{T}(lattice) n_dim = count(!iszero, eachcol(lattice)) n_dim > 0 || error("Check your lattice; we do not do 0D systems") for i = n_dim+1:3 @@ -141,11 +159,8 @@ function Model(lattice::AbstractMatrix{T}, ) n_spin = length(spin_components(spin_polarization)) - if isnothing(smearing) - @assert temperature >= 0 - # Default to Fermi-Dirac smearing when finite temperature - smearing = temperature > 0.0 ? Smearing.FermiDirac() : Smearing.None() - end + temperature = T(austrip(temperature)) + temperature < 0 && error("temperature must be non-negative") if !allunique(string.(nameof.(typeof.(terms)))) error("Having several terms of the same name is not supported.") @@ -163,7 +178,7 @@ function Model(lattice::AbstractMatrix{T}, Model{T,value_type(T)}(model_name, lattice, recip_lattice, n_dim, inv_lattice, inv_recip_lattice, unit_cell_volume, recip_cell_volume, - n_electrons, spin_polarization, n_spin, T(temperature), smearing, + n_electrons, εF, spin_polarization, n_spin, temperature, smearing, atoms, positions, atom_groups, terms, symmetries) end function Model(lattice::AbstractMatrix{<:Integer}, atoms::Vector{<:Element}, @@ -179,9 +194,11 @@ normalize_magnetic_moment(::Nothing)::Vec3{Float64} = (0, 0, 0) normalize_magnetic_moment(mm::Number)::Vec3{Float64} = (0, 0, mm) normalize_magnetic_moment(mm::AbstractVector)::Vec3{Float64} = mm +"""Number of valence electrons.""" +n_electrons_from_atoms(atoms) = sum(n_elec_valence, atoms; init=0) """ -:none if no element has a magnetic moment, else :collinear or :full +`:none` if no element has a magnetic moment, else `:collinear` or `:full`. """ function default_spin_polarization(magnetic_moments) isempty(magnetic_moments) && return :none @@ -244,7 +261,6 @@ function spin_components(spin_polarization::Symbol) end spin_components(model::Model) = spin_components(model.spin_polarization) - # prevent broadcast import Base.Broadcast.broadcastable Base.Broadcast.broadcastable(model::Model) = Ref(model) diff --git a/src/common/ortho.jl b/src/common/ortho.jl index a0f7508339..8e682bcc43 100644 --- a/src/common/ortho.jl +++ b/src/common/ortho.jl @@ -1,2 +1,2 @@ # Orthonormalize -ortho_qr(φk) = Matrix(qr(φk).Q) +@timing ortho_qr(φk) = Matrix(qr(φk).Q) diff --git a/src/common/zeros_like.jl b/src/common/zeros_like.jl new file mode 100644 index 0000000000..ebcdfad05d --- /dev/null +++ b/src/common/zeros_like.jl @@ -0,0 +1,10 @@ +# Create an array of same type as X filled with zeros, minimizing the number +# of allocations. +function zeros_like(X::AbstractArray, T::Type=eltype(X), dims::Integer...=size(X)...) + Z = similar(X, T, dims...) + Z .= 0 + Z +end +zeros_like(X::AbstractArray, dims::Integer...) = zeros_like(X, eltype(X), dims...) +zeros_like(X::Array, T::Type=eltype(X), dims::Integer...=size(X)...) = zeros(T, dims...) +zeros_like(X::StaticArray, T::Type=eltype(X), dims::Integer...=size(X)...) = @SArray zeros(T, dims...) diff --git a/src/densities.jl b/src/densities.jl index 58a098ebd0..c8d3c0aeff 100644 --- a/src/densities.jl +++ b/src/densities.jl @@ -31,6 +31,8 @@ grid `basis`, where the individual k-points are occupied according to `occupatio ψnk_real_chunklocal = Array{complex(T),3}[zeros(complex(T), basis.fft_size) for _ = 1:Threads.nthreads()] + # TODO We should probably pass occupation_threshold here and ignore bands + # below this threshold in the density computation @sync for (ichunk, chunk) in enumerate(Iterators.partition(ik_n, chunk_length)) Threads.@spawn for (ik, n) in chunk # spawn a task per chunk ψnk_real = ψnk_real_chunklocal[ichunk] diff --git a/src/eigen/diag.jl b/src/eigen/diag.jl index 38a555f262..f08f500543 100644 --- a/src/eigen/diag.jl +++ b/src/eigen/diag.jl @@ -13,7 +13,6 @@ function diagonalize_all_kblocks(eigensolver, ham::Hamiltonian, nev_per_kpoint:: prec_type=PreconditionerTPA, interpolate_kpoints=true, tol=1e-6, miniter=1, maxiter=100, n_conv_check=nothing, show_progress=false) - T = complex(eltype(ham.basis)) kpoints = ham.basis.kpoints results = Vector{Any}(undef, length(kpoints)) @@ -28,26 +27,31 @@ function diagonalize_all_kblocks(eigensolver, ham::Hamiltonian, nev_per_kpoint:: "$nev_per_kpoint eigenvalues. Increase Ecut.") end # Get ψguessk - @timing "QR orthonormalization" begin - if ψguess !== nothing - # ψguess provided + if !isnothing(ψguess) + nev_guess = size(ψguess[ik], 2) + if nev_guess > nev_per_kpoint + ψguessk = ψguess[ik][:, 1:nev_per_kpoint] + elseif nev_guess == nev_per_kpoint ψguessk = ψguess[ik] - elseif interpolate_kpoints && ik > 1 - # use information from previous k-point - X0 = interpolate_kpoint(results[ik - 1].X, ham.basis, kpoints[ik - 1], - ham.basis, kpoints[ik]) - ψguessk = ortho_qr(X0) # Re-orthogonalize and renormalize else - ψguessk = random_orbitals(ham.basis, kpt, nev_per_kpoint) + X0 = similar(ψguess[ik], n_Gk, nev_per_kpoint) + X0[:, 1:nev_guess] = ψguess[ik] + X0[:, nev_guess+1:end] = randn(eltype(X0), n_Gk, nev_per_kpoint - nev_guess) + ψguessk = ortho_qr(X0) end + elseif interpolate_kpoints && ik > 1 + # use information from previous k-point + ψguessk = interpolate_kpoint(results[ik - 1].X, ham.basis, kpoints[ik - 1], + ham.basis, kpoints[ik]) + else + ψguessk = random_orbitals(ham.basis, kpt, nev_per_kpoint) end @assert size(ψguessk) == (n_Gk, nev_per_kpoint) prec = nothing - prec_type !== nothing && (prec = prec_type(ham.basis, kpt)) + !isnothing(prec_type) && (prec = prec_type(ham.basis, kpt)) results[ik] = eigensolver(ham.blocks[ik], ψguessk; - prec=prec, tol=tol, miniter=miniter, maxiter=maxiter, - n_conv_check=n_conv_check) + prec, tol, miniter, maxiter, n_conv_check) # Update progress bar if desired !isnothing(progress) && next!(progress) diff --git a/src/eigen/lobpcg_hyper_impl.jl b/src/eigen/lobpcg_hyper_impl.jl index 1b0c73f9b3..c8fd30c3b4 100644 --- a/src/eigen/lobpcg_hyper_impl.jl +++ b/src/eigen/lobpcg_hyper_impl.jl @@ -43,17 +43,79 @@ vprintln(args...) = nothing using LinearAlgebra -using BlockArrays # used for the `mortar` command which makes block matrices +import Base: * +import Base.size, Base.adjoint, Base.Array + +""" +Simple wrapper to represent a matrix formed by the concatenation of column blocks: +it is mostly equivalent to hcat, but doesn't allocate the full matrix. +LazyHcat only supports a few multiplication routines: furthermore, a multiplication +involving this structure will always yield a plain array (and not a LazyHcat structure). +LazyHcat is a lightweight subset of BlockArrays.jl's functionalities, but has the +advantage to be able to store GPU Arrays (BlockArrays is heavily built on Julia's CPU Array). +""" +struct LazyHcat{T <: Number, D <: Tuple} <: AbstractMatrix{T} + blocks::D +end + +function LazyHcat(arrays::AbstractArray...) + @assert length(arrays) != 0 + n_ref = size(arrays[1], 1) + @assert all(size.(arrays, 1) .== n_ref) + + T = promote_type(map(eltype, arrays)...) + + LazyHcat{T, typeof(arrays)}(arrays) +end + +function Base.size(A::LazyHcat) + n = size(A.blocks[1], 1) + m = sum(size(block, 2) for block in A.blocks) + (n,m) +end + +Base.Array(A::LazyHcat) = hcat(A.blocks...) + +Base.adjoint(A::LazyHcat) = Adjoint(A) + +@views function Base.:*(Aadj::Adjoint{T, <: LazyHcat}, B::LazyHcat) where {T} + A = Aadj.parent + rows = size(A)[2] + cols = size(B)[2] + ret = similar(A.blocks[1], rows, cols) + + orow = 0 # row offset + for (iA, blA) in enumerate(A.blocks) + ocol = 0 # column offset + for (iB, blB) in enumerate(B.blocks) + ret[orow .+ (1:size(blA, 2)), ocol .+ (1:size(blB, 2))] .= blA' * blB + ocol += size(blB, 2) + end + orow += size(blA, 2) + end + ret +end + +Base.:*(Aadj::Adjoint{T, <: LazyHcat}, B::AbstractMatrix) where {T} = Aadj * LazyHcat(B) + +@views function *(Ablock::LazyHcat, B::AbstractMatrix) + res = Ablock.blocks[1] * B[1:size(Ablock.blocks[1], 2), :] # First multiplication + offset = size(Ablock.blocks[1], 2) + for block in Ablock.blocks[2:end] + mul!(res, block, B[offset .+ (1:size(block, 2)), :], 1, 1) + offset += size(block, 2) + end + res +end -# when X or Y are BlockArrays, this makes the return value be a proper array (not a BlockArray) -function array_mul(X::AbstractArray{T}, Y) where {T} - Z = Array{T}(undef, size(X, 1), size(Y, 2)) - mul!(Z, X, Y) +function LinearAlgebra.mul!(res::AbstractMatrix, Ablock::LazyHcat, + B::AbstractVecOrMat, α::Number, β::Number) + mul!(res, Ablock*B, I, α, β) end # Perform a Rayleigh-Ritz for the N first eigenvectors. @timing function rayleigh_ritz(X, AX, N) - XAX = array_mul(X', AX) + XAX = X' * AX @assert all(!isnan, XAX) F = eigen(Hermitian(XAX)) F.vectors[:,1:N], F.values[1:N] @@ -174,16 +236,16 @@ end niter = 1 ninners = zeros(Int,0) while true - BYX = BY'X - # XXX the one(T) instead of plain old 1 is because of https://github.com/JuliaArrays/BlockArrays.jl/issues/176 - mul!(X, Y, BYX, -one(T), one(T)) # X -= Y*BY'X + BYX = BY' * X + mul!(X, Y, BYX, -1, 1) # X -= Y*BY'X # If the orthogonalization has produced results below 2eps, we drop them # This is to be able to orthogonalize eg [1;0] against [e^iθ;0], # as can happen in extreme cases in the ortho!(cP, cX) dropped = drop!(X) if dropped != [] - @views mul!(X[:, dropped], Y, BY' * (X[:, dropped]), -one(T), one(T)) # X -= Y*BY'X + X[:, dropped] .-= Y * (BY' * X[:, dropped]) end + if norm(BYX) < tol && niter > 1 push!(ninners, 0) break @@ -219,11 +281,9 @@ function final_retval(X, AX, resid_history, niter, n_matvec) residuals = AX .- X*Diagonal(λ) (λ=λ, X=X, residual_norms=[norm(residuals[:, i]) for i in 1:size(residuals, 2)], - residual_history=resid_history[:, 1:niter+1], - n_matvec=n_matvec) + residual_history=resid_history[:, 1:niter+1], n_matvec=n_matvec) end - ### The algorithm is Xn+1 = rayleigh_ritz(hcat(Xn, A*Xn, Xn-Xn-1)) ### We follow the strategy of Hetmaniuk and Lehoucq, and maintain a B-orthonormal basis Y = (X,R,P) ### After each rayleigh_ritz step, the B-orthonormal X and P are deduced by an orthogonal rotation from Y @@ -234,6 +294,7 @@ end miniter=1, ortho_tol=2eps(real(eltype(X))), n_conv_check=nothing, display_progress=false) N, M = size(X) + # If N is too small, we will likely get in trouble error_message(verb) = "The eigenproblem is too small, and the iterative " * "eigensolver $verb fail; increase the number of " * @@ -252,7 +313,7 @@ end B_ortho!(X, BX) end - n_matvec = M # Count number of matrix-vector products + n_matvec = M # Count number of matrix-vector products AX = similar(X) AX = mul!(AX, A, X) @assert all(!isnan, AX) @@ -274,7 +335,8 @@ end end nlocked = 0 niter = 0 # the first iteration is fake - λs = @views [(X[:,n]'*AX[:,n]) / (X[:,n]'BX[:,n]) for n=1:M] + λs = @views [(X[:, n]'*AX[:, n]) / (X[:, n]'BX[:, n]) for n=1:M] + λs = oftype(X[:, 1], λs) # Offload to GPU if needed new_X = X new_AX = AX new_BX = BX @@ -290,13 +352,13 @@ end # Form Rayleigh-Ritz subspace if niter > 1 - Y = mortar((X, R, P)) - AY = mortar((AX, AR, AP)) - BY = mortar((BX, BR, BP)) # data shared with (X, R, P) in non-general case + Y = LazyHcat(X, R, P) + AY = LazyHcat(AX, AR, AP) + BY = LazyHcat(BX, BR, BP) # data shared with (X, R, P) in non-general case else - Y = mortar((X, R)) - AY = mortar((AX, AR)) - BY = mortar((BX, BR)) # data shared with (X, R) in non-general case + Y = LazyHcat(X, R) + AY = LazyHcat(AX, AR) + BY = LazyHcat(BX, BR) # data shared with (X, R) in non-general case end cX, λs = rayleigh_ritz(Y, AY, M-nlocked) @@ -304,9 +366,9 @@ end # wait on updating P because we have to know which vectors # to lock (and therefore the residuals) before computing P # only for the unlocked vectors. This results in better convergence. - new_X = array_mul(Y, cX) - new_AX = array_mul(AY, cX) # no accuracy loss, since cX orthogonal - new_BX = (B == I) ? new_X : array_mul(BY, cX) + new_X = Y * cX + new_AX = AY * cX # no accuracy loss, since cX orthogonal + new_BX = (B == I) ? new_X : BY * cX end ### Compute new residuals @@ -320,7 +382,7 @@ end vprintln(niter, " ", resid_history[:, niter+1]) if precon !== I @timing "preconditioning" begin - precondprep!(precon, X) # update preconditioner if needed; defaults to noop + precondprep!(precon, X) # update preconditioner if needed; defaults to noop ldiv!(precon, new_R) end end @@ -360,20 +422,23 @@ end # orthogonalization, see Hetmaniuk & Lehoucq, and Duersch et. al. # cP = copy(cX) # cP[Xn_indices,:] .= 0 - e = zeros(eltype(X), size(cX, 1), M - prev_nlocked) - for i in 1:length(Xn_indices) - e[Xn_indices[i], i] = 1 - end + + lenXn = length(Xn_indices) + e = zeros_like(X, size(cX, 1), M - prev_nlocked) + lower_diag = one(similar(X, lenXn, lenXn)) + # e has zeros everywhere except on one of its lower diagonal + e[Xn_indices[1]:last(Xn_indices), 1:lenXn] = lower_diag + cP = cX .- e cP = cP[:, Xn_indices] # orthogonalize against all Xn (including newly locked) ortho!(cP, cX, cX, tol=ortho_tol) # Get new P - new_P = array_mul( Y, cP) - new_AP = array_mul(AY, cP) + new_P = Y * cP + new_AP = AY * cP if B != I - new_BP = array_mul(BY, cP) + new_BP = BY * cP else new_BP = new_P end @@ -418,8 +483,8 @@ end # Orthogonalize R wrt all X, newly active P if niter > 0 - Z = mortar((full_X, P)) - BZ = mortar((full_BX, BP)) # data shared with (full_X, P) in non-general case + Z = LazyHcat(full_X, P) + BZ = LazyHcat(full_BX, BP) # data shared with (full_X, P) in non-general case else Z = full_X BZ = full_BX diff --git a/src/elements.jl b/src/elements.jl index d7a515b4bf..38b1b099dd 100644 --- a/src/elements.jl +++ b/src/elements.jl @@ -107,7 +107,7 @@ struct ElementCohenBergstresser <: Element V_sym # Map |G|^2 (in units of (2π / lattice_constant)^2) to form factors lattice_constant # Lattice constant (in Bohr) which is assumed end -charge_ionic(el::ElementCohenBergstresser) = 2 +charge_ionic(el::ElementCohenBergstresser) = 4 charge_nuclear(el::ElementCohenBergstresser) = el.Z AtomsBase.atomic_symbol(el::ElementCohenBergstresser) = el.symbol diff --git a/src/external/spglib.jl b/src/external/spglib.jl index 5ca2bcd474..f13054d5a3 100644 --- a/src/external/spglib.jl +++ b/src/external/spglib.jl @@ -56,6 +56,9 @@ function spglib_cell(lattice, atom_groups, positions, magnetic_moments) spg = spglib_atoms(atom_groups, positions, magnetic_moments) (; cell=Spglib.Cell(lattice, spg.positions, spg.numbers, spg.spins), spg.collinear) end +function spglib_cell(model::Model, magnetic_moments) + spglib_cell(model.lattice, model.atom_groups, model.positions, magnetic_moments) +end @timing function spglib_get_symmetry(lattice::AbstractMatrix{<:AbstractFloat}, atom_groups, @@ -180,6 +183,6 @@ end function spglib_spacegroup_number(model, magnetic_moments=[]; tol_symmetry=SYMMETRY_TOLERANCE) # Get spacegroup number according to International Tables for Crystallography (ITA) # TODO Time-reversal symmetry disabled? (not yet available in DFTK) - cell, _ = spglib_cell(model.lattice, model.atom_groups, model.positions, magnetic_moments) + cell, _ = spglib_cell(model, magnetic_moments) Spglib.get_spacegroup_number(cell, tol_symmetry) end diff --git a/src/external/vtkio.jl b/src/external/vtkio.jl index 8af52ee7ec..861c850ec5 100644 --- a/src/external/vtkio.jl +++ b/src/external/vtkio.jl @@ -16,7 +16,7 @@ function save_scfres_master(filename::AbstractString, scfres::NamedTuple, ::Val{ # Storing the bloch waves if save_ψ for ik in 1:length(basis.kpoints) - for iband in 1:size(scfres.ψ[1])[2] + for iband in 1:size(scfres.ψ[ik])[2] ψnk_real = ifft(basis, basis.kpoints[ik], scfres.ψ[ik][:, iband]) vtkfile["ψ_k$(ik)_band$(iband)_real"] = real.(ψnk_real) vtkfile["ψ_k$(ik)_band$(iband)_imag"] = imag.(ψnk_real) diff --git a/src/external/wannier90.jl b/src/external/wannier90.jl index e727345741..e288b53e29 100644 --- a/src/external/wannier90.jl +++ b/src/external/wannier90.jl @@ -42,16 +42,16 @@ function write_w90_win(fileprefix::String, basis::PlaneWaveBasis; println(fp, "!"^20 * " k_points\n") if bands_plot - kpathdata = high_symmetry_kpath(basis.model) - length(kpathdata.kpath) > 1 || @warn( + kpath = irrfbz_path(basis.model) + length(kpath.paths) > 1 || @warn( "Only first kpath branch considered in write_w90_win") - path = kpathdata.kpath[1] + path = kpath.paths[1] println(fp, "begin kpoint_path") for i in 1:length(path)-1 A, B = path[i:i+1] # write segment A -> B - @printf(fp, "%s %10.6f %10.6f %10.6f ", A, round.(kpathdata.klabels[A], digits=5)...) - @printf(fp, "%s %10.6f %10.6f %10.6f\n", B, round.(kpathdata.klabels[B], digits=5)...) + @printf(fp, "%s %10.6f %10.6f %10.6f ", A, round.(kpath.points[A], digits=5)...) + @printf(fp, "%s %10.6f %10.6f %10.6f\n", B, round.(kpath.points[B], digits=5)...) end println(fp, "end kpoint_path") println(fp, "bands_plot = true\n") @@ -247,7 +247,7 @@ generated in reduced coordinates. default_wannier_centres(n_wannier) = [rand(1, 3) for _ in 1:n_wannier] @timing function run_wannier90(scfres; - n_bands=size(scfres.ψ[1], 2) - scfres.n_ep_extra, + n_bands=scfres.n_bands_converge, n_wannier=n_bands, centers=default_wannier_centres(n_wannier), fileprefix=joinpath("wannier90", "wannier"), diff --git a/src/guess_density.jl b/src/guess_density.jl index 6aae91b0db..051ea93420 100644 --- a/src/guess_density.jl +++ b/src/guess_density.jl @@ -101,14 +101,18 @@ function gaussian_superposition(basis::PlaneWaveBasis{T}, gaussians) where {T} # # is formed from a superposition of atomic densities, each scaled by a prefactor for (iG, G) in enumerate(G_vectors(basis)) + # Ensure that we only set G-vectors that have a -G counterpart + if isnothing(index_G_vectors(basis, -G)) + ρ[iG] = zero(complex(T)) + continue + end + Gsq = sum(abs2, basis.model.recip_lattice * G) for (coeff, decay_length, r) in gaussians form_factor::T = exp(-Gsq * T(decay_length)^2) ρ[iG] += T(coeff) * form_factor * cis2pi(-dot(G, r)) end end - - # projection in the normalized plane wave basis irfft(basis, ρ / sqrt(basis.model.unit_cell_volume)) end diff --git a/src/interpolation.jl b/src/interpolation.jl new file mode 100644 index 0000000000..0201bc9048 --- /dev/null +++ b/src/interpolation.jl @@ -0,0 +1,89 @@ +import Interpolations +import Interpolations: interpolate, extrapolate, scale, BSpline, Quadratic, OnCell + +""" +Interpolate a function expressed in a basis `basis_in` to a basis `basis_out` +This interpolation uses a very basic real-space algorithm, and makes +a DWIM-y attempt to take into account the fact that basis_out can be a supercell of basis_in +""" +function interpolate_density(ρ_in, basis_in::PlaneWaveBasis, basis_out::PlaneWaveBasis) + ρ_out = interpolate_density(ρ_in, basis_in.fft_size, basis_out.fft_size, + basis_in.model.lattice, basis_out.model.lattice) +end + +# TODO Specialization for the common case lattice_out = lattice_in +function interpolate_density(ρ_in::AbstractArray, grid_in, grid_out, lattice_in, lattice_out=lattice_in) + T = real(eltype(ρ_in)) + @assert size(ρ_in) == grid_in + + # First, build supercell, array of 3 ints + supercell = zeros(Int, 3) + for i = 1:3 + if norm(lattice_in[:, i]) == 0 + @assert norm(lattice_out[:, i]) == 0 + supercell[i] = 1 + else + supercell[i] = round(Int, norm(lattice_out[:, i]) / norm(lattice_in[:, i])) + end + if norm(lattice_out[:, i] - supercell[i]*lattice_in[:, i]) > .3*norm(lattice_out[:, i]) + @warn "In direction $i, the output lattice is very different from the input lattice" + end + end + + # ρ_in represents a periodic function, on a grid 0, 1/N, ... (N-1)/N + grid_supercell = grid_in .* supercell + ρ_in_supercell = similar(ρ_in, (grid_supercell...)) + for i = 1:supercell[1] + for j = 1:supercell[2] + for k = 1:supercell[3] + ρ_in_supercell[ + 1 + (i-1)*grid_in[1] : i*grid_in[1], + 1 + (j-1)*grid_in[2] : j*grid_in[2], + 1 + (k-1)*grid_in[3] : k*grid_in[3]] = ρ_in + end + end + end + + # interpolate ρ_in_supercell from grid grid_supercell to grid_out + axes_in = (range(0, 1, length=grid_supercell[i]+1)[1:end-1] for i=1:3) + itp = interpolate(ρ_in_supercell, BSpline(Quadratic(Interpolations.Periodic(OnCell())))) + sitp = scale(itp, axes_in...) + ρ_interp = extrapolate(sitp, Periodic()) + ρ_out = similar(ρ_in, grid_out) + for i = 1:grid_out[1] + for j = 1:grid_out[2] + for k = 1:grid_out[3] + ρ_out[i, j, k] = ρ_interp((i-1)/grid_out[1], + (j-1)/grid_out[2], + (k-1)/grid_out[3]) + end + end + end + + ρ_out +end + +""" +Interpolate some data from one ``k``-point to another. The interpolation is fast, but not +necessarily exact. Intended only to construct guesses for iterative solvers. +""" +function interpolate_kpoint(data_in::AbstractVecOrMat, + basis_in::PlaneWaveBasis, kpoint_in::Kpoint, + basis_out::PlaneWaveBasis, kpoint_out::Kpoint) + # TODO merge with transfer_blochwave_kpt + if kpoint_in == kpoint_out + return copy(data_in) + end + @assert length(G_vectors(basis_in, kpoint_in)) == size(data_in, 1) + + n_bands = size(data_in, 2) + n_Gk_out = length(G_vectors(basis_out, kpoint_out)) + data_out = similar(data_in, n_Gk_out, n_bands) .= 0 + for iin in 1:size(data_in, 1) + idx_fft = kpoint_in.mapping[iin] + idx_fft in keys(kpoint_out.mapping_inv) || continue + iout = kpoint_out.mapping_inv[idx_fft] + data_out[iout, :] = data_in[iin, :] + end + ortho_qr(data_out) # Re-orthogonalize and renormalize +end diff --git a/src/occupation.jl b/src/occupation.jl index 2ac3e0d29b..f57488f776 100644 --- a/src/occupation.jl +++ b/src/occupation.jl @@ -2,6 +2,24 @@ import Roots + +""" +Find the occupation and Fermi level. +""" +function compute_occupation(basis::PlaneWaveBasis, eigenvalues; + temperature=basis.model.temperature, + smearing=basis.model.smearing) + if !isnothing(basis.model.εF) # fixed Fermi level + εF = basis.model.εF + else # fixed n_electrons + εF = compute_fermi_level(basis, eigenvalues; temperature) + end + occupation = compute_occupation(basis, eigenvalues, εF; temperature, smearing) + + (; occupation, εF) +end + + """Compute the occupations, given eigenenergies and a Fermi level""" function compute_occupation(basis::PlaneWaveBasis{T}, eigenvalues, εF; temperature=basis.model.temperature, @@ -15,23 +33,13 @@ function compute_occupation(basis::PlaneWaveBasis{T}, eigenvalues, εF; for εk in eigenvalues] end -""" -Find the occupation and Fermi level. -""" -function compute_occupation(basis::PlaneWaveBasis{T}, eigenvalues; - temperature=basis.model.temperature, - occupation_threshold) where {T} +function compute_fermi_level(basis::PlaneWaveBasis{T}, eigenvalues; temperature) where {T} n_electrons = basis.model.n_electrons + n_spin = basis.model.n_spin_components # Maximum occupation per state filled_occ = filled_occupation(basis.model) - if temperature == 0 && n_electrons % filled_occ != 0 - error("$n_electrons electron cannot be attained by filling states with " * - "occupation $filled_occ. Typically this indicates that you need to put " * - "a temperature or switch to a calculation with collinear spin polarization.") - end - # The goal is to find εF so that # n_i = filled_occ * f((εi-εF)/θ) with θ = temperature # sum_i n_i = n_electrons @@ -47,97 +55,55 @@ function compute_occupation(basis::PlaneWaveBasis{T}, eigenvalues; end # Get rough bounds to bracket εF - min_ε = minimum(minimum.(eigenvalues)) - 1 + min_ε = minimum(minimum, eigenvalues) - 1 min_ε = mpi_min(min_ε, basis.comm_kpts) - max_ε = maximum(maximum.(eigenvalues)) + 1 + max_ε = maximum(maximum, eigenvalues) + 1 max_ε = mpi_max(max_ε, basis.comm_kpts) - if temperature != 0 - @assert compute_n_elec(min_ε) < n_electrons < compute_n_elec(max_ε) - end - if compute_n_elec(max_ε) ≈ n_electrons - # This branch takes care of the case of insulators at zero - # temperature with as many bands as electrons; there it is - # possible that compute_n_elec(max_ε) ≈ n_electrons but - # compute_n_elec(max_ε) < n_electrons, so that bisection does - # not work - εF = max_ε + if iszero(temperature) + # Sanity check that we can indeed fill the appropriate number of states + if n_electrons % (n_spin * filled_occ) != 0 + error("$n_electrons electrons cannot be attained by filling states with " * + "occupation $filled_occ. Typically this indicates that you need to put " * + "a temperature or switch to a calculation with collinear spin polarization.") + end + + # For zero temperature, two cases arise: either there are as many bands + # as electrons, in which case we set εF to the highest energy level + # reached, or there are unoccupied conduction bands and we take + # εF as the midpoint between valence and conduction bands. + if compute_n_elec(max_ε) ≈ n_electrons + εF = max_ε + else + # The sanity check above ensures that n_fill is well defined + n_fill = div(n_electrons, n_spin * filled_occ, RoundUp) + # highest occupied energy level + HOMO = maximum([εk[n_fill] for εk in eigenvalues]) + HOMO = mpi_max(HOMO, basis.comm_kpts) + # lowest unoccupied energy level, be careful that not all k-points + # might have at least n_fill+1 energy levels so we have to take care + # of that by specifying init to minimum + LUMO = minimum(minimum.([εk[n_fill+1:end] for εk in eigenvalues]; init=T(Inf))) + LUMO = mpi_min(LUMO, basis.comm_kpts) + εF = (HOMO + LUMO) / 2 + end else - # Just use bisection here; note that with MP smearing there might - # be multiple possible Fermi levels. This could be sped up with more - # advanced methods (e.g. false position), but more care has to be - # taken with convergence criteria and the like + # For finite temperature, just use bisection; note that with MP smearing + # there might be multiple possible Fermi levels. This could be sped up + # with more advanced methods (e.g. false position), but more care has to + # be taken with convergence criteria and the like + @assert compute_n_elec(min_ε) < n_electrons < compute_n_elec(max_ε) εF = Roots.find_zero(εF -> compute_n_elec(εF) - n_electrons, (min_ε, max_ε), Roots.Bisection(), atol=eps(T)) end if !isapprox(compute_n_elec(εF), n_electrons) - # For insulators it can happen that bisection stops in a final interval (a, b) where - # `compute_n_elec(a) ≈ n_electrons` and `compute_n_elec(b) > n_electrons`, but where - # the returned `(a+b)/2` is rounded to `b`, such that `εF` gives a too - # large electron count. To make sure this is not the case, make εF a little smaller. - εF -= eps(εF) - end - - if !isapprox(compute_n_elec(εF), n_electrons) - if temperature == 0 + if iszero(temperature) error("Unable to find non-fractional occupations that have the " * "correct number of electrons. You should add a temperature.") else error("This should not happen, debug me!") end end - - occupation = compute_occupation(basis, eigenvalues, εF) - minocc = maximum(minimum, occupation) - if temperature > 0 && minocc > occupation_threshold - @warn "One k-point has a high minimum occupation $minocc. You should probably increase the number of bands." - end - - (; occupation, εF) -end - -""" -Find Fermi level and occupation for the given parameters, assuming a band gap -and zero temperature. This function is for DEBUG purposes only, and the -finite-temperature version with 0 temperature should be preferred. -""" -function compute_occupation_bandgap(basis, eigenvalues) - n_bands = length(eigenvalues[1]) - @assert all(e -> length(e) == n_bands, eigenvalues) - n_electrons = basis.model.n_electrons - T = eltype(basis) - @assert basis.model.temperature == 0 - - filled_occ = filled_occupation(basis.model) - n_fill = div(n_electrons, filled_occ, RoundUp) - @assert filled_occ * n_fill == n_electrons - @assert n_bands ≥ n_fill - - # We need to fill n_fill states with occupation filled_occ - # Find HOMO and LUMO - HOMO = -Inf # highest occupied energy state - LUMO = Inf # lowest unoccupied energy state - occupation = similar(basis.kpoints, Vector{T}) - for ik in 1:length(occupation) - occupation[ik] = zeros(T, n_bands) - occupation[ik][1:n_fill] .= filled_occ - HOMO = max(HOMO, eigenvalues[ik][n_fill]) - if n_fill < n_bands - LUMO = min(LUMO, eigenvalues[ik][n_fill + 1]) - end - end - LUMO = mpi_min(LUMO, basis.comm_kpts) - HOMO = mpi_max(HOMO, basis.comm_kpts) - @assert weighted_ksum(basis, sum.(occupation)) ≈ n_electrons - - # Put Fermi level between HOMO and LUMO, to ensure that HOMO < εF < LUMO - εF = (HOMO + LUMO) / 2 - if εF ≥ LUMO || εF ≤ HOMO - @warn("`compute_occupation_bandgap` assumes an insulator, but the " * - "system seems metallic. Try specifying a temperature and a smearing function.", - HOMO, LUMO, εF) - end - - (occupation=occupation, εF=εF) + εF end diff --git a/src/orbitals.jl b/src/orbitals.jl index 35104c3320..799d1b36f5 100644 --- a/src/orbitals.jl +++ b/src/orbitals.jl @@ -9,7 +9,7 @@ function select_occupied_orbitals(basis, ψ, occupation; threshold=0.0) # if we have an insulator, sanity check that the orbitals we kept are the # occupied ones - if threshold == 0.0 + if iszero(threshold) model = basis.model n_spin = model.n_spin_components n_bands = div(model.n_electrons, n_spin * filled_occupation(model), RoundUp) diff --git a/src/plotting.jl b/src/plotting.jl index 0f8a593340..09061ab7f6 100644 --- a/src/plotting.jl +++ b/src/plotting.jl @@ -21,38 +21,34 @@ function ScfPlotTrace(plt=Plots.plot(yaxis=:log); kwargs...) end -function plot_band_data(band_data; εF=nothing, - klabels=Dict{String, Vector{Float64}}(), unit=u"hartree", kwargs...) +function plot_band_data(kpath::KPathInterpolant, band_data; + εF=nothing, unit=u"hartree", kwargs...) eshift = isnothing(εF) ? 0.0 : εF - data = prepare_band_data(band_data, klabels=klabels) + data = data_for_plotting(kpath, band_data) # Constant to convert from AU to the desired unit to_unit = ustrip(auconvert(unit, 1.0)) - markerargs = () - if !(:markersize in keys(kwargs)) && !(:markershape in keys(kwargs)) - if length(krange_spin(band_data.basis, 1)) < 70 - markerargs = (markersize=2, markershape=:circle) + # Plot all bands, spins and errors + p = Plots.plot(xlabel="wave vector") + margs = length(kpath) < 70 ? (; markersize=2, markershape=:circle) : (; ) + for σ in 1:data.n_spin, iband = 1:data.n_bands, branch in data.kbranches + yerror = nothing + if hasproperty(data, :λerror) + yerror = data.λerror[:, iband, σ][branch] .* to_unit end + energies = (data.λ[:, iband, σ][branch] .- eshift) .* to_unit + Plots.plot!(p, data.kdistances[branch], energies; + label="", yerror, color=(:blue, :red)[σ], margs..., kwargs...) end - # For each branch, plot all bands, spins and errors - p = Plots.plot(xlabel="wave vector") - for branch in data.branches - for σ in 1:data.n_spin, iband = 1:data.n_bands - yerror = nothing - if hasproperty(branch, :λerror) - yerror = branch.λerror[:, iband, σ] .* to_unit - end - energies = (branch.λ[:, iband, σ] .- eshift) .* to_unit - color = (:blue, :red)[σ] - Plots.plot!(p, branch.kdistances, energies; color, label="", - yerror, markerargs..., kwargs...) - end + # Delimiter for branches + for branch in data.kbranches[1:end-1] + Plots.vline!(p, [data.kdistances[last(branch)]], color=:black, label="") end # X-range: 0 to last kdistance value - Plots.xlims!(p, (0, data.branches[end].kdistances[end])) + Plots.xlims!(p, (0, data.kdistances[end])) Plots.xticks!(p, data.ticks.distances, data.ticks.labels) ylims = [-0.147, 0.147] diff --git a/src/postprocess/band_structure.jl b/src/postprocess/band_structure.jl index d8c950337a..10b8d07118 100644 --- a/src/postprocess/band_structure.jl +++ b/src/postprocess/band_structure.jl @@ -1,84 +1,83 @@ import Brillouin -import Brillouin.KPaths: Bravais +import Brillouin.KPaths: KPath, KPathInterpolant, irrfbz_path @doc raw""" -Extract the high-symmetry ``k``-point path corresponding to the passed model -using `Brillouin.jl`. Uses the conventions described in the reference work by +Extract the high-symmetry ``k``-point path corresponding to the passed `model` +using `Brillouin`. Uses the conventions described in the reference work by Cracknell, Davies, Miller, and Love (CDML). Of note, this has minor differences to the ``k``-path reference ([Y. Himuma et. al. Comput. Mater. Sci. **128**, 140 (2017)](https://doi.org/10.1016/j.commatsci.2016.10.015)) underlying the path-choices of `Brillouin.jl`, specifically for oA and mC Bravais types. -The `kline_density` is given in number of ``k``-points per inverse bohrs (i.e. -overall in units of length). -Issues a warning in case the passed lattice does not match the expected primitive. +If the cell is a supercell of a smaller primitive cell, the standard ``k``-path of the +associated primitive cell is returned. So, the high-symmetry ``k`` points are those of the +primitive cell Brillouin zone, not those of the supercell Brillouin zone. + +The `dim` argument allows to artificially truncate the dimension of the employed model, +e.g. allowing to plot a 2D bandstructure of a 3D model (useful for example for plotting +band structures of sheets with `dim=2`). + +Due to lacking support in `Spglib.jl` for two-dimensional lattices it is (a) assumed that +`model.lattice` is a *conventional* lattice and (b) required to pass the space group +number using the `sgnum` keyword argument. """ -function high_symmetry_kpath(model; kline_density=40) - kline_density = austrip(kline_density) - - if model.n_dim == 1 # Return fast for 1D model - # TODO Is this special-casing of 1D is not needed for Brillouin.jl any more - # - # Just use irrfbz_path(1, DirectBasis{1}([1.0])) - # (see https://github.com/JuliaMolSim/DFTK.jl/pull/496/files#r725205860) - # - # Length of the kpath is recip_lattice[1, 1] in 1D - n_points = max(2, 1 + ceil(Int, kline_density * model.recip_lattice[1, 1])) - kcoords = [@SVector[coord, 0, 0] for coord in range(-1//2, 1//2, length=n_points)] - klabels = Dict("Γ" => zeros(3), "-½" => [-0.5, 0.0, 0.0], "½" => [0.5, 0, 0]) - return (kcoords=kcoords, klabels=klabels, - kpath=[["-½", "½"]], kbranches=[1:length(kcoords)]) +function irrfbz_path(model; dim::Integer=model.n_dim, sgnum=nothing, magnetic_moments=[]) + @assert dim ≤ model.n_dim + for i in dim:3, j in dim:3 + if i != j && !iszero(model.lattice[i, j]) + error("Reducing the dimension for band structure plotting only allowed " * + "if the dropped dimensions are orthogonal to the remaining ones.") + end + end + if !isnothing(sgnum) && dim ∈ (1, 3) + @warn("sgnum keyword argument unused in `irrfbz_path` unused " * + "unless a 2-dimensional lattice is encountered.") end - # - Brillouin.jl expects the input direct lattice to be in the conventional lattice - # in the convention of the International Table of Crystallography Vol A (ITA). - # - spglib uses this convention for the returned conventional lattice, - # so it can be directly used as input to Brillouin.jl - # - The output k-Points and reciprocal lattices will be in the CDML convention. - conv_latt = spglib_standardize_cell(model; primitive=false,correct_symmetry=false).lattice - sgnum = spglib_spacegroup_number(model) # Get ITA space-group number - direct_basis = Bravais.DirectBasis(collect(eachcol(conv_latt))) - primitive_latt = Bravais.primitivize(direct_basis, Bravais.centering(sgnum, 3)) - - primitive_latt ≈ collect(eachcol(model.lattice)) || @warn( - "DFTK's model.lattice and Brillouin's primitive lattice do not agree. " * - "The kpath selected to plot the band structure might not be most appropriate." - ) - - kp = Brillouin.irrfbz_path(sgnum, direct_basis) - kinter = Brillouin.interpolate(kp, density=kline_density) - - # TODO Need to take care of time-reversal symmetry here! - # See https://github.com/JuliaMolSim/DFTK.jl/pull/496/files#r725203554 - - # Need to double the points whenever a new path starts - # (for temporary compatibility with pymatgen) - # TODO Remove this later - kcoords = empty(first(kinter.kpaths)) - for kbranch in kinter.kpaths - idcs = findall(k -> any(sum(abs2, k - kcomp) < 1e-5 - for kcomp in values(kp.points)), kbranch) - @assert length(idcs) ≥ 2 - idcs = idcs[2:end-1] # Don't duplicate first and last - idcs = sort(append!(idcs, 1:length(kbranch))) - append!(kcoords, kbranch[idcs]) + # Brillouin.jl expects the input direct lattice to be in the conventional lattice + # in the convention of the International Table of Crystallography Vol A (ITA). + # + # The output of Brillouin.jl are k-Points and reciprocal lattice vectors + # in the CDML convention. + if dim == 1 + # Only one space group; avoid spglib here + kpath = Brillouin.irrfbz_path(1, [[model.lattice[1, 1]]], Val(1)) + elseif dim == 2 + if isnothing(sgnum) + error("sgnum keyword argument (specifying the ITA space group number) " * + "is required for band structure plots in 2D lattices.") + end + # TODO We assume to have the conventional lattice here. + lattice_2d = [model.lattice[1:2, 1], model.lattice[1:2, 2]] + kpath = Brillouin.irrfbz_path(sgnum, lattice_2d, Val(2)) + elseif dim == 3 + # Brillouin.jl has an interface to Spglib.jl to directly reduce the passed + # lattice to the ITA conventional lattice and so the Spglib cell can be + # directly used as an input. + cell, _ = spglib_cell(model, magnetic_moments) + kpath = Brillouin.irrfbz_path(cell) end - T = eltype(kcoords[1]) - klabels = Dict{String,Vector{T}}(string(key) => val for (key, val) in kp.points) - kpath = [[string(el) for el in path] for path in kp.paths] - (; kcoords, klabels, kpath) + # TODO In case of absence of time-reversal symmetry we need to explicitly + # add the inverted kpath here! + # See https://github.com/JuliaMolSim/DFTK.jl/pull/496/files#r725203554 + + kpath end +"""Return kpoint coordinates in reduced coordinates""" +function kpath_get_kcoords(kpath::KPathInterpolant{D}) where {D} + map(k -> vcat(k, zeros_like(k, 3 - D)), kpath) +end +function kpath_get_branch(kpath::KPathInterpolant{D}, ibranch::Integer) where {D} + map(k -> vcat(k, zeros_like(k, 3 - D)), kpath.kpaths[ibranch]) +end -@timing function compute_bands(basis, kcoords; +@timing function compute_bands(basis::PlaneWaveBasis, kcoords::AbstractVector; n_bands=default_n_bands_bandstructure(basis.model), ρ=nothing, eigensolver=lobpcg_hyper, tol=1e-3, show_progress=true, kwargs...) - # Create basis with new kpoints, without any symmetry operations. - kweights = ones(length(kcoords)) ./ length(kcoords) - bs_basis = PlaneWaveBasis(basis, kcoords, kweights) - + # kcoords are the kpoint coordinates in fractional coordinates if isnothing(ρ) if any(t isa TermNonlinear for t in basis.terms) error("If a non-linear term is present in the model the converged density is required " * @@ -88,60 +87,80 @@ end ρ = guess_density(basis) end + # Create basis with new kpoints, without any symmetry operations. + kweights = ones(length(kcoords)) ./ length(kcoords) + bs_basis = PlaneWaveBasis(basis, kcoords, kweights) + ham = Hamiltonian(bs_basis; ρ) - band_data = diagonalize_all_kblocks(eigensolver, ham, n_bands + 3; - n_conv_check=n_bands, - tol=tol, show_progress=show_progress, kwargs...) - if !band_data.converged + eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands + 3; + n_conv_check=n_bands, tol, show_progress, kwargs...) + if !eigres.converged @warn "Eigensolver not converged" iterations=band_data.iterations end - merge((basis=bs_basis, ), select_eigenpairs_all_kblocks(band_data, 1:n_bands)) + merge((; basis=bs_basis), select_eigenpairs_all_kblocks(eigres, 1:n_bands)) +end +function compute_bands(basis::PlaneWaveBasis, kpath::KPathInterpolant; kwargs...) + compute_bands(basis, kpath_get_kcoords(kpath); kwargs...) end -function split_into_branches(kcoords, data::Dict, klabels::Dict) +function kdistances_and_ticks(kcoords, klabels::Dict, kbranches) # kcoords in cartesian coordinates, klabels uses cartesian coordinates function getlabel(kcoord; tol=1e-4) findfirst(c -> norm(c - kcoord) < tol, klabels) end - branches = Any[(kindices = [0], kdistances=[0.0], ), ] - for (ik, kcoord) in enumerate(kcoords) - previous_kcoord = ik == 1 ? kcoords[1] : kcoords[ik - 1] - if !isnothing(getlabel(kcoord)) && !isnothing(getlabel(previous_kcoord)) - # New branch encountered - previous_distance = branches[end].kdistances[end] - push!(branches, (kindices=[ik], kdistances=[previous_distance])) + kdistances = eltype(kcoords[1])[] + tick_distances = eltype(kcoords[1])[] + tick_labels = String[] + for (ibranch, kbranch) in enumerate(kbranches) + kdistances_branch = cumsum(append!([0.], [norm(kcoords[ik - 1] - kcoords[ik]) + for ik in kbranch[2:end]])) + if ibranch == 1 + append!(kdistances, kdistances_branch) else - # Keep adding to current branch - distance = branches[end].kdistances[end] + norm(kcoord - kcoords[ik - 1]) - push!(branches[end].kdistances, distance) - push!(branches[end].kindices, ik) + append!(kdistances, kdistances_branch .+ kdistances[end][end]) + end + for ik in kbranch + kcoord = kcoords[ik] + if getlabel(kcoord) !== nothing + if ibranch != 1 && ik == kbranch[1] + # New branch encountered. Do not add a new tick point but update label. + tick_labels[end] *= " | " * String(getlabel(kcoord)) + else + push!(tick_labels, String(getlabel(kcoord))) + push!(tick_distances, kdistances[ik]) + end + end end end - - map(branches[2:end]) do branch - branch_data = Dict(key => data[key][branch.kindices, :, :] for key in keys(data)) - ret = (klabels=(getlabel(kcoords[branch.kindices[1]]), - getlabel(kcoords[branch.kindices[end]])), - kdistances=branch.kdistances, - kindices=branch.kindices) - merge(ret, (; branch_data...)) - end + ticks = (distances=tick_distances, labels=tick_labels) + (; kdistances, ticks) end -function prepare_band_data(band_data; datakeys=[:λ, :λerror], - klabels=Dict{String, Vector{Float64}}()) - basis = band_data.basis +function data_for_plotting(kpath::KPathInterpolant, band_data; datakeys=[:λ, :λerror]) + basis = band_data.basis n_spin = basis.model.n_spin_components n_kcoord = length(basis.kpoints) ÷ n_spin n_bands = nothing + # XXX Convert KPathInterpolant => kbranches, klabels + kbranches = [1:length(kpath.kpaths[1])] + for n in length.(kpath.kpaths)[2:end] + push!(kbranches, kbranches[end].stop+1:kbranches[end].stop+n) + end + klabels = Dict{Symbol, Vec3{eltype(kpath[1])}}() + for (ibranch, labels) in enumerate(kpath.labels) + for (k, v) in pairs(labels) + # Convert to Cartesian and add to labels + klabels[v] = basis.model.recip_lattice * kpath_get_branch(kpath, ibranch)[k] + end + end + # Convert coordinates to Cartesian kcoords_cart = [basis.model.recip_lattice * basis.kpoints[ik].coordinate for ik in krange_spin(basis, 1)] - klabels_cart = Dict(lal => basis.model.recip_lattice * vec for (lal, vec) in klabels) # Split data into branches data = Dict{Symbol, Any}() @@ -155,26 +174,9 @@ function prepare_band_data(band_data; datakeys=[:λ, :λerror], data[key] = data_per_kσ end @assert !isnothing(n_bands) - branches = split_into_branches(kcoords_cart, data, klabels_cart) - - tick_labels = String[branches[1].klabels[1]] - tick_distances = Float64[branches[1].kdistances[1]] - for (i, br) in enumerate(branches) - # Ignore branches with a single k-point - branches[i].klabels[1] == branches[i].klabels[2] && continue - - label = branches[i].klabels[2] - distance = branches[i].kdistances[end] - if i != length(branches) && branches[i+1].klabels[1] != label - # Next branch is not continuous from the current - label = label * " | " * branches[i+1].klabels[1] - end - push!(tick_labels, label) - push!(tick_distances, distance) - end - (branches=branches, ticks=(distances=tick_distances, labels=tick_labels), - n_bands=n_bands, n_kcoord=n_kcoord, n_spin=n_spin, basis=basis) + kdistances, ticks = kdistances_and_ticks(kcoords_cart, klabels, kbranches) + (; ticks, kdistances, kbranches, n_bands, n_kcoord, n_spin, data...) end @@ -215,22 +217,24 @@ by default. Another standard choices is `unit=u"eV"` (electron volts). The `kline_density` is given in number of ``k``-points per inverse bohrs (i.e. overall in units of length). """ -function plot_bandstructure(basis::PlaneWaveBasis; +function plot_bandstructure(basis::PlaneWaveBasis, kpath::KPath=irrfbz_path(basis.model); εF=nothing, kline_density=40u"bohr", - unit=u"hartree", kwargs_plot=(), kwargs...) + unit=u"hartree", kwargs_plot=(; ), + kwargs...) mpi_nprocs() > 1 && error("Band structures with MPI not supported yet") if !isdefined(DFTK, :PLOTS_LOADED) error("Plots not loaded. Run 'using Plots' before calling plot_bandstructure.") end # Band structure calculation along high-symmetry path - kcoords, klabels, kpath = high_symmetry_kpath(basis.model; kline_density) + kinter = Brillouin.interpolate(kpath, density=austrip(kline_density)) println("Computing bands along kpath:") - println(" ", join(join.(kpath, " -> "), " and ")) - band_data = compute_bands(basis, kcoords; kwargs...) - plot_band_data(band_data; εF, klabels, unit, kwargs_plot...) + sortlabels = map(bl -> last.(sort(collect(pairs(bl)))), kinter.labels) + println(" ", join(join.(sortlabels, " -> "), " and ")) + band_data = compute_bands(basis, kinter; kwargs...) + plot_band_data(kinter, band_data; εF, unit, kwargs_plot...) end -function plot_bandstructure(scfres::NamedTuple; +function plot_bandstructure(scfres::NamedTuple, kpath::KPath=irrfbz_path(scfres.basis.model); n_bands=default_n_bands_bandstructure(scfres), kwargs...) - plot_bandstructure(scfres.basis; n_bands, ρ=scfres.ρ, εF=scfres.εF, kwargs...) + plot_bandstructure(scfres.basis, kpath; n_bands, ρ=scfres.ρ, εF=scfres.εF, kwargs...) end diff --git a/src/pseudo/NormConservingPsp.jl b/src/pseudo/NormConservingPsp.jl index 3343f1ba99..c292895f0c 100644 --- a/src/pseudo/NormConservingPsp.jl +++ b/src/pseudo/NormConservingPsp.jl @@ -68,7 +68,8 @@ Notice: The returned result is the *energy per unit cell* and not the energy per To obtain the latter, the caller needs to divide by the unit cell volume. """ eval_psp_energy_correction(T, psp::NormConservingPsp, n_electrons) = zero(T) -# by default, no correction +# by default, no correction, see PspHgh implementation and tests +# for details on what to implement eval_psp_energy_correction(psp::NormConservingPsp, n_electrons) = eval_psp_energy_correction(Float64, psp, n_electrons) diff --git a/src/pseudo/PspHgh.jl b/src/pseudo/PspHgh.jl index 10dc160cfd..03032200d5 100644 --- a/src/pseudo/PspHgh.jl +++ b/src/pseudo/PspHgh.jl @@ -224,7 +224,7 @@ end function eval_psp_energy_correction(T, psp::PspHgh, n_electrons) # By construction we need to compute the DC component of the difference # of the Coulomb potential (-Z/G^2 in Fourier space) and the pseudopotential - # i.e. -Z/(ΔG)^2 - eval_psp_local_fourier(psp, ΔG) for ΔG → 0. This is: + # i.e. -4πZ/(ΔG)^2 - eval_psp_local_fourier(psp, ΔG) for ΔG → 0. This is: cloc_coeffs = T[1, 3, 15, 105] difference_DC = (T(psp.Zion) * T(psp.rloc)^2 / 2 + sqrt(T(π)/2) * T(psp.rloc)^3 * T(sum(cloc_coeffs .* psp.cloc))) diff --git a/src/response/cg.jl b/src/response/cg.jl new file mode 100644 index 0000000000..2b4c310d6c --- /dev/null +++ b/src/response/cg.jl @@ -0,0 +1,67 @@ +using LinearMaps + +function default_cg_print(info) + @printf("%3d\t%1.2e\n", info.n_iter, info.residual_norm) +end + +""" +Implementation of the conjugate gradient method which allows for preconditioning +and projection operations along iterations. +""" +function cg!(x::AbstractVector{T}, A::LinearMap{T}, b::AbstractVector{T}; + precon=I, proj=identity, callback=identity, + tol=1e-10, maxiter=100, miniter=1) where {T} + + # initialisation + # r = b - Ax is the residual + r = copy(b) + # c is an intermediate variable to store A*p and precon\r + c = zero(b) + + # save one matrix-vector product + if !iszero(x) + mul!(c, A, x) + r .-= c + end + ldiv!(c, precon, r) + γ = dot(r, c) + # p is the descent direction + p = copy(c) + n_iter = 0 + residual_norm = zero(real(T)) + + # convergence history + converged = false + + # preconditioned conjugate gradient + while n_iter < maxiter + n_iter += 1 + mul!(c, A, p) + α = γ / dot(p, c) + + # update iterate and residual while ensuring they stay in Ran(proj) + x .= proj(x .+ α .* p) + r .= proj(r .- α .* c) + residual_norm = norm(r) + + # output + info = (; A, b, n_iter, x, r, residual_norm, converged, stage=:iterate) + callback(info) + if (n_iter > miniter) && residual_norm <= tol + converged = true + break + end + + # apply preconditioner and prepare next iteration + ldiv!(c, precon, r) + γ_prev, γ = γ, dot(r, c) + β = γ / γ_prev + p .= proj(c .+ β .* p) + end + info = (; x, converged, tol, residual_norm, iterations=n_iter, maxiter, stage=:finalize) + callback(info) + info +end +cg!(x::AbstractVector, A::AbstractMatrix, b::AbstractVector; kwargs...) = cg!(x, LinearMap(A), b; kwargs...) +cg(A::LinearMap, b::AbstractVector; kwargs...) = cg!(zero(b), A, b; kwargs...) +cg(A::AbstractMatrix, b::AbstractVector; kwargs...) = cg(LinearMap(A), b; kwargs...) diff --git a/src/response/chi0.jl b/src/response/chi0.jl index e4a812d689..39e01451c5 100644 --- a/src/response/chi0.jl +++ b/src/response/chi0.jl @@ -1,5 +1,4 @@ using LinearMaps -using IterativeSolvers using ProgressMeter @doc raw""" @@ -16,8 +15,7 @@ In this case the matrix has effectively 4 blocks, which are: \end{array}\right) ``` """ -function compute_χ0(ham; temperature=ham.basis.model.temperature, - occupation_threshold=default_occupation_threshold()) +function compute_χ0(ham; temperature=ham.basis.model.temperature) # We're after χ0(r,r') such that δρ = ∫ χ0(r,r') δV(r') dr' # where (up to normalizations) # ρ = ∑_nk f(εnk - εF) |ψnk|^2 @@ -54,7 +52,7 @@ function compute_χ0(ham; temperature=ham.basis.model.temperature, EVs = [eigen(Hermitian(Array(Hk))) for Hk in ham.blocks] Es = [EV.values for EV in EVs] Vs = [EV.vectors for EV in EVs] - occ, εF = compute_occupation(basis, Es; temperature, occupation_threshold) + occ, εF = compute_occupation(basis, Es; temperature) χ0 = zeros(eltype(basis), n_spin * n_fft, n_spin * n_fft) for (ik, kpt) in enumerate(basis.kpoints) @@ -111,7 +109,7 @@ precondprep!(P::FunctionPreconditioner, ::Any) = P # included). function sternheimer_solver(Hk, ψk, εnk, rhs, n; callback=info->nothing, ψk_extra=zeros(size(ψk,1), 0), εk_extra=zeros(0), - abstol=1e-9, reltol=0, verbose=false) + Hψk_extra=zeros(size(ψk,1), 0), tol=1e-9) basis = Hk.basis kpoint = Hk.kpoint @@ -163,13 +161,12 @@ function sternheimer_solver(Hk, ψk, εnk, rhs, n; callback=info->nothing, # is defined above and b is the projection of -rhs onto Ran(Q). # b = -Q(rhs) - bb = R(b - H(ψk_extra * (ψk_exHψk_ex \ ψk_extra'b))) + bb = R(b - Hψk_extra * (ψk_exHψk_ex \ ψk_extra'b)) function RAR(ϕ) Rϕ = R(ϕ) - # A denotes here the Schur complement of (1-P) (H-εn) (1-P) + # Schur complement of (1-P) (H-εn) (1-P) # with the splitting Ran(1-P) = Ran(P_extra) ⊕ Ran(R) - ARϕ = Rϕ - ψk_extra * (ψk_exHψk_ex \ ψk_extra'H(Rϕ)) - R(H(ARϕ)) + R(H(Rϕ) - Hψk_extra * (ψk_exHψk_ex \ Hψk_extra'Rϕ)) end precon = PreconditionerTPA(basis, kpoint) precondprep!(precon, ψk[:, n]) @@ -177,9 +174,12 @@ function sternheimer_solver(Hk, ψk, εnk, rhs, n; callback=info->nothing, x .= R(precon \ R(y)) end J = LinearMap{eltype(ψk)}(RAR, size(Hk, 1)) - δψknᴿ, ch = cg(J, bb; Pl=FunctionPreconditioner(R_ldiv!), abstol, reltol, - verbose, log=true) - info = (; basis=basis, kpoint=kpoint, ch=ch, n=n) + res = cg(J, bb; precon=FunctionPreconditioner(R_ldiv!), tol, proj=R) + !res.converged && @warn("Sternheimer CG not converged", + iterations=res.iterations, tol=res.tol, + residual_norm=res.residual_norm) + δψknᴿ = res.x + info = (; basis, kpoint, res, n) callback(info) # 2) solve for αkn now that we know δψknᴿ @@ -267,28 +267,25 @@ end # First compute δεF δεF = zero(T) δocc = [zero(occ_occ[ik]) for ik = 1:Nk] # = fn' * (δεn - δεF) + smearing = model.smearing if temperature > 0 # First compute δocc without self-consistent Fermi δεF D = zero(T) for ik = 1:Nk, (n, εnk) in enumerate(ε_occ[ik]) enred = (εnk - εF) / temperature δεnk = real(dot(ψ_occ[ik][:, n], δHψ[ik][:, n])) - fpnk = (filled_occ - * Smearing.occupation_derivative(model.smearing, enred) - / temperature) + fpnk = filled_occ * Smearing.occupation_derivative(smearing, enred) / temperature δocc[ik][n] = δεnk * fpnk D += fpnk * basis.kweights[ik] end # compute δεF D = mpi_sum(D, basis.comm_kpts) # equal to minus the total DOS δocc_tot = mpi_sum(sum(basis.kweights .* sum.(δocc)), basis.comm_kpts) - δεF = δocc_tot / D + δεF = !isnothing(model.εF) ? zero(δεF) : δocc_tot / D # no δεF when Fermi level is fixed # recompute δocc for ik = 1:Nk, (n, εnk) in enumerate(ε_occ[ik]) enred = (εnk - εF) / temperature - fpnk = (filled_occ - * Smearing.occupation_derivative(model.smearing, enred) - / temperature) + fpnk = filled_occ * Smearing.occupation_derivative(smearing, enred) / temperature δocc[ik][n] -= fpnk * δεF end end @@ -298,16 +295,17 @@ end for ik = 1:Nk ψk = ψ_occ[ik] δψk = δψ[ik] + Hψk_extra = ham.blocks[ik] * ψ_extra[ik] εk = ε_occ[ik] for n = 1:length(εk) - fnk = filled_occ * Smearing.occupation(model.smearing, (εk[n]-εF) / temperature) + fnk = filled_occ * Smearing.occupation(smearing, (εk[n]-εF) / temperature) # explicit contributions (nonzero only for temperature > 0) for m = 1:length(εk) - fmk = filled_occ * Smearing.occupation(model.smearing, (εk[m]-εF) / temperature) + fmk = filled_occ * Smearing.occupation(smearing, (εk[m]-εF) / temperature) ddiff = Smearing.occupation_divided_difference - ratio = filled_occ * ddiff(model.smearing, εk[m], εk[n], εF, temperature) + ratio = filled_occ * ddiff(smearing, εk[m], εk[n], εF, temperature) αmn = compute_αmn(fmk, fnk, ratio) # fnk * αmn + fmk * αnm = ratio δψk[:, n] .+= ψk[:, m] .* αmn .* (dot(ψk[:, m], δHψ[ik][:, n]) * (n != m)) end @@ -315,14 +313,14 @@ end # Sternheimer contribution δψk[:, n] .+= sternheimer_solver(ham.blocks[ik], ψk, εk[n], δHψ[ik][:, n], n; ψk_extra=ψ_extra[ik], εk_extra=ε_extra[ik], - kwargs_sternheimer...) + Hψk_extra, kwargs_sternheimer...) end end # pad δoccupation δoccupation = zero.(occ) for (ik, maskk) in enumerate(mask_occ) - δoccupation[ik][maskk] .+= δocc[ik] + δoccupation[ik][maskk] .= δocc[ik] end # keeping zeros for extra bands to keep the output δψ with the same size @@ -338,7 +336,6 @@ function apply_χ0(ham, ψ, occupation, εF, eigenvalues, δV; kwargs_sternheimer...) basis = ham.basis - model = basis.model # Make δV respect the basis symmetry group, since we won't be able # to compute perturbations that don't anyway diff --git a/src/response/hessian.jl b/src/response/hessian.jl index f9c39866b1..8f192ce241 100644 --- a/src/response/hessian.jl +++ b/src/response/hessian.jl @@ -64,7 +64,7 @@ end Return δψ where (Ω+K) δψ = rhs """ function solve_ΩplusK(basis::PlaneWaveBasis{T}, ψ, rhs, occupation; - tol=1e-10, verbose=false) where {T} + tol=1e-10) where {T} @assert mpi_nprocs() == 1 # Distributed implementation not yet available filled_occ = filled_occupation(basis.model) # for now, all orbitals have to be fully occupied -> need to strip them beforehand @@ -108,10 +108,14 @@ function solve_ΩplusK(basis::PlaneWaveBasis{T}, ψ, rhs, occupation; J = LinearMap{T}(ΩpK, size(rhs_pack, 1)) # solve (Ω+K) δψ = rhs on the tangent space with CG - δψ, history = cg(J, rhs_pack, Pl=FunctionPreconditioner(f_ldiv!), - reltol=0, abstol=tol, verbose=verbose, log=true) - - (; δψ=unpack(δψ), history) + function proj(x) + δψ = unpack(x) + proj_tangent!(δψ, ψ) + pack(δψ) + end + res = cg(J, rhs_pack; precon=FunctionPreconditioner(f_ldiv!), proj, tol) + (; δψ=unpack(res.x), res.converged, res.tol, res.residual_norm, + res.iterations) end @@ -138,9 +142,9 @@ function solve_ΩplusK_split(ham::Hamiltonian, ρ::AbstractArray{T}, ψ, occupat @assert size(rhs[1]) == size(ψ[1]) # Assume the same number of bands in ψ and rhs # compute δρ0 (ignoring interactions) - δψ0, δoccupation0, δεF0 = apply_χ0_4P(ham, ψ, occupation, εF, eigenvalues, -rhs; - reltol=0, abstol=tol_sternheimer, - occupation_threshold, kwargs...) # = -χ04P * rhs + δψ0, δoccupation0 = apply_χ0_4P(ham, ψ, occupation, εF, eigenvalues, -rhs; + tol=tol_sternheimer, occupation_threshold, + kwargs...) # = -χ04P * rhs δρ0 = compute_δρ(basis, ψ, δψ0, occupation, δoccupation0) # compute total δρ @@ -153,8 +157,7 @@ function solve_ΩplusK_split(ham::Hamiltonian, ρ::AbstractArray{T}, ψ, occupat # Would be nice to play with abstol / reltol etc. to avoid over-solving # for the initial GMRES steps. χ0δV = apply_χ0(ham, ψ, occupation, εF, eigenvalues, δV; - occupation_threshold, abstol=tol_sternheimer, reltol=0, - kwargs...) + occupation_threshold, tol=tol_sternheimer, kwargs...) pack(δρ - χ0δV) end J = LinearMap{T}(eps_fun, prod(size(δρ0))) @@ -176,8 +179,8 @@ function solve_ΩplusK_split(ham::Hamiltonian, ρ::AbstractArray{T}, ψ, occupat end δψ, δoccupation, δεF = apply_χ0_4P(ham, ψ, occupation, εF, eigenvalues, δHψ; - occupation_threshold, abstol=tol_sternheimer, - reltol=0, kwargs...) + occupation_threshold, tol=tol_sternheimer, + kwargs...) (; δψ, δρ, δHψ, δVind, δeigenvalues, δoccupation, δεF, history) end @@ -185,10 +188,8 @@ end function solve_ΩplusK_split(basis::PlaneWaveBasis, ψ, rhs, occupation; kwargs...) ρ = compute_density(basis, ψ, occupation) _, H = energy_hamiltonian(basis, ψ, occupation; ρ) - eigenvalues = [real.(eigvals(ψk'Hψk)) for (ψk, Hψk) in zip(ψ, H * ψ)] - occupation_threshold = kwargs[:occupation_threshold] - occupation, εF = compute_occupation(basis, eigenvalues; occupation_threshold) + occupation, εF = compute_occupation(basis, eigenvalues) solve_ΩplusK_split(H, ρ, ψ, occupation, εF, eigenvalues, rhs; kwargs...) end diff --git a/src/scf/direct_minimization.jl b/src/scf/direct_minimization.jl index 1ac8249630..b9ea97dddd 100644 --- a/src/scf/direct_minimization.jl +++ b/src/scf/direct_minimization.jl @@ -74,7 +74,8 @@ function direct_minimization(basis::PlaneWaveBasis{T}, ψ0; error("Direct minimization with MPI is not supported yet") end model = basis.model - @assert model.temperature == 0 # temperature is not yet supported + @assert iszero(model.temperature) # temperature is not yet supported + @assert isnothing(model.εF) # neither are computations with fixed Fermi level filled_occ = filled_occupation(model) n_spin = model.n_spin_components n_bands = div(model.n_electrons, n_spin * filled_occ, RoundUp) diff --git a/src/scf/mixing.jl b/src/scf/mixing.jl index 5a2d4a5326..26ac68f3f8 100644 --- a/src/scf/mixing.jl +++ b/src/scf/mixing.jl @@ -74,6 +74,7 @@ end δFspin_fourier = spin_density(δF_fourier) δρtot_fourier = δFtot_fourier .* G² ./ (kTF.^2 .+ G²) + enforce_real!(basis, δρtot_fourier) δρtot = irfft(basis, δρtot_fourier) # Copy DC component, otherwise it never gets updated @@ -83,6 +84,7 @@ end ρ_from_total_and_spin(δρtot, nothing) else δρspin_fourier = @. δFspin_fourier - δFtot_fourier * (4π * ΔDOS_Ω) / (kTF^2 + G²) + enforce_real!(basis, δρspin_fourier) δρspin = irfft(basis, δρspin_fourier) ρ_from_total_and_spin(δρtot, δρspin) end diff --git a/src/scf/nbands_algorithm.jl b/src/scf/nbands_algorithm.jl new file mode 100644 index 0000000000..f75880d204 --- /dev/null +++ b/src/scf/nbands_algorithm.jl @@ -0,0 +1,87 @@ +""" +NbandsAlgorithm subtypes determine how many bands to compute and converge +in each SCF step. +""" +abstract type NbandsAlgorithm end + +function default_n_bands(model) + n_spin = model.n_spin_components + min_n_bands = div(model.n_electrons, n_spin * filled_occupation(model), RoundUp) + n_extra = iszero(model.temperature) ? 0 : ceil(Int, 0.15 * min_n_bands) + min_n_bands + n_extra +end +default_occupation_threshold() = 1e-6 + + +""" +In each SCF step converge exactly `n_bands_converge`, computing along the way exactly +`n_bands_compute` (usually a few more to ease convergence in systems with small gaps). +""" +@kwdef struct FixedBands <: NbandsAlgorithm + n_bands_converge::Int # Number of bands to converge + n_bands_compute::Int = n_bands_converge + 3 # bands to compute (not always converged) + # Threshold for orbital to be counted as occupied + occupation_threshold::Float64 = default_occupation_threshold() +end +function FixedBands(model::Model) + n_bands_converge = default_n_bands(model) + n_bands_converge += iszero(model.temperature) ? 0 : ceil(Int, 0.05 * n_bands_converge) + FixedBands(; n_bands_converge, n_bands_compute=n_bands_converge + 3) +end +FixedBands(basis::PlaneWaveBasis; kwargs...) = FixedBands(basis.model; kwargs...) +function determine_n_bands(bands::FixedBands, occupation, eigenvalues, ψ) + (; n_bands_converge=bands.n_bands_converge, bands.n_bands_compute) +end + + +""" +Dynamically adapt number of bands to be converged to ensure that the orbitals of lowest +occupation are occupied to at most `occupation_threshold`. To obtain rapid convergence +of the eigensolver a gap between the eigenvalues of the last occupied orbital and the last +computed (but not converged) orbital of `gap_min` is ensured. +""" +@kwdef struct AdaptiveBands <: NbandsAlgorithm + n_bands_converge::Int # Minimal number of bands to converge + n_bands_compute::Int # Minimal number of bands to compute + occupation_threshold::Float64 = default_occupation_threshold() + gap_min::Float64 = 1e-3 # Minimal gap between converged and computed bands +end +function AdaptiveBands(model::Model; n_bands_converge=default_n_bands(model), kwargs...) + n_extra = iszero(model.temperature) ? 3 : max(4, ceil(Int, 0.05 * n_bands_converge)) + AdaptiveBands(; n_bands_converge, n_bands_compute=n_bands_converge + n_extra, kwargs...) +end +AdaptiveBands(basis::PlaneWaveBasis; kwargs...) = AdaptiveBands(basis.model; kwargs...) + +function determine_n_bands(bands::AdaptiveBands, occupation::Nothing, eigenvalues, ψ) + if isnothing(ψ) + n_bands_compute = bands.n_bands_compute + else + n_bands_compute = max(bands.n_bands_compute, maximum(ψk -> size(ψk, 2), ψ)) + end + # Boost number of bands to converge to have more information around in the next step + # and to thus make a better decision on the number of bands we actually care about. + n_bands_converge = floor(Int, (bands.n_bands_converge + bands.n_bands_compute) / 2) + (; n_bands_converge, n_bands_compute) +end +function determine_n_bands(bands::AdaptiveBands, occupation::AbstractVector, + eigenvalues::AbstractVector, ψ::AbstractVector) + # TODO Could return different bands per k-Points + + # Determine number of bands to be actually converged + n_bands_occ = maximum(occupation) do occk + something(findlast(fnk -> fnk ≥ bands.occupation_threshold, occk), length(occk) + 1) + end + n_bands_converge = max(bands.n_bands_converge, n_bands_occ) + + # Determine number of bands to be computed + n_bands_compute_ε = maximum(eigenvalues) do εk + something(findlast(εnk -> εnk ≥ εk[n_bands_converge] + bands.gap_min, εk), + length(εk) + 1) + end + n_bands_compute = max(bands.n_bands_compute, n_bands_compute_ε, n_bands_converge + 3) + if !isnothing(ψ) + n_bands_compute = max(n_bands_compute, maximum(ψk -> size(ψk, 2), ψ)) + end + (; n_bands_converge, n_bands_compute) +end + diff --git a/src/scf/newton.jl b/src/scf/newton.jl index 89eb5e942f..abf6662f0f 100644 --- a/src/scf/newton.jl +++ b/src/scf/newton.jl @@ -72,22 +72,22 @@ end """ - newton(basis::PlaneWaveBasis{T}; ψ0=nothing, - tol=1e-6, tol_=1e-10, maxiter=20, verbose=false, - callback=NewtonDefaultCallback(), - is_converged=NewtonConvergenceDensity(tol)) + newton(basis::PlaneWaveBasis{T}, ψ0; + tol=1e-6, tol_cg=tol / 100, maxiter=20, callback=ScfDefaultCallback(), + is_converged=ScfConvergenceDensity(tol)) Newton algorithm. Be careful that the starting point needs to be not too far from the solution. """ function newton(basis::PlaneWaveBasis{T}, ψ0; - tol=1e-6, tol_cg=tol / 100, maxiter=20, verbose=false, + tol=1e-6, tol_cg=tol / 100, maxiter=20, callback=ScfDefaultCallback(), is_converged=ScfConvergenceDensity(tol)) where {T} # setting parameters model = basis.model - @assert model.temperature == 0 # temperature is not yet supported + @assert iszero(model.temperature) # temperature is not yet supported + @assert isnothing(model.εF) # neither are computations with fixed Fermi level # check that there are no virtual orbitals filled_occ = filled_occupation(model) @@ -115,7 +115,7 @@ function newton(basis::PlaneWaveBasis{T}, ψ0; # compute Newton step and next iteration res = compute_projected_gradient(basis, ψ, occupation) # solve (Ω+K) δψ = -res so that the Newton step is ψ <- ψ + δψ - δψ = solve_ΩplusK(basis, ψ, -res, occupation; tol=tol_cg, verbose).δψ + δψ = solve_ΩplusK(basis, ψ, -res, occupation; tol=tol_cg).δψ ψ = [ortho_qr(ψ[ik] + δψ[ik]) for ik in 1:Nk] ρ_next = compute_density(basis, ψ, occupation) diff --git a/src/scf/potential_mixing.jl b/src/scf/potential_mixing.jl index cfb41fc0a5..a76026e025 100644 --- a/src/scf/potential_mixing.jl +++ b/src/scf/potential_mixing.jl @@ -224,14 +224,13 @@ trial_damping(damping::FixedDamping, args...) = damping.α @timing function scf_potential_mixing( basis::PlaneWaveBasis; damping=FixedDamping(0.8), - n_bands=default_n_bands(basis.model), + nbandsalg::NbandsAlgorithm=AdaptiveBands(basis), ρ=guess_density(basis), V=nothing, ψ=nothing, tol=1e-6, maxiter=100, eigensolver=lobpcg_hyper, - n_ep_extra=3, diag_miniter=1, determine_diagtol=ScfDiagtol(), mixing=SimpleMixing(), @@ -240,7 +239,6 @@ trial_damping(damping::FixedDamping, args...) = damping.α acceleration=AndersonAcceleration(;m=10), accept_step=ScfAcceptStepAll(), max_backtracks=3, # Maximal number of backtracking line searches - occupation_threshold=default_occupation_threshold(), ) # TODO Test other mixings and lift this @assert ( mixing isa SimpleMixing @@ -248,34 +246,31 @@ trial_damping(damping::FixedDamping, args...) = damping.α || mixing isa KerkerDosMixing) damping isa Number && (damping = FixedDamping(damping)) - if ψ !== nothing + if !isnothing(ψ) @assert length(ψ) == length(basis.kpoints) - for ik in 1:length(basis.kpoints) - @assert size(ψ[ik], 2) == n_bands + n_ep_extra - end end # Initial guess for V (if none given) energies, ham = energy_hamiltonian(basis, nothing, nothing; ρ=ρ) isnothing(V) && (V = total_local_potential(ham)) - function EVρ(Vin; diagtol=tol / 10, ψ=nothing) + function EVρ(Vin; diagtol=tol / 10, ψ=nothing, eigenvalues=nothing, occupation=nothing) ham_V = hamiltonian_with_total_potential(ham, Vin) - res_V = next_density(ham_V; n_bands=n_bands, ψ=ψ, n_ep_extra=n_ep_extra, - miniter=diag_miniter, tol=diagtol, eigensolver=eigensolver, - occupation_threshold) + + res_V = next_density(ham_V, nbandsalg; eigensolver, ψ, eigenvalues, + occupation, miniter=diag_miniter, tol=diagtol) new_E, new_ham = energy_hamiltonian(basis, res_V.ψ, res_V.occupation; ρ=res_V.ρout, eigenvalues=res_V.eigenvalues, εF=res_V.εF) - (basis=basis, ham=new_ham, energies=new_E, occupation_threshold=occupation_threshold, - Vin=Vin, Vout=total_local_potential(new_ham), res_V...) + (; basis, ham=new_ham, energies=new_E, + Vin, Vout=total_local_potential(new_ham), res_V...) end n_iter = 1 converged = false α_trial = trial_damping(damping) diagtol = determine_diagtol((ρin=ρ, Vin=V, n_iter=n_iter)) - info = EVρ(V; diagtol=diagtol, ψ=ψ) + info = EVρ(V; diagtol, ψ) Pinv_δV = mix_potential(mixing, basis, info.Vout - info.Vin; n_iter, info...) info = merge(info, (α=NaN, diagonalization=[info.diagonalization], ρin=ρ, n_iter=n_iter, Pinv_δV=Pinv_δV)) @@ -297,7 +292,7 @@ trial_damping(damping::FixedDamping, args...) = damping.α δV = (acceleration(info.Vin, α_trial, info.Pinv_δV) - info.Vin) / α_trial # Determine damping and take next step - guess = ψ + guess = info.ψ α = α_trial successful = false # Successful line search (final step is considered good) n_backtrack = 1 @@ -308,7 +303,7 @@ trial_damping(damping::FixedDamping, args...) = damping.α mpi_master() && @debug "Iteration $n_iter linesearch step $n_backtrack α=$α diagtol=$diagtol" Vnext = info.Vin .+ α .* δV - info_next = EVρ(Vnext; ψ=guess, diagtol=diagtol) + info_next = EVρ(Vnext; ψ=guess, diagtol, info.eigenvalues, info.occupation) Pinv_δV_next = mix_potential(mixing, basis, info_next.Vout - info_next.Vin; n_iter, info_next...) push!(diagonalization, info_next.diagonalization) @@ -331,7 +326,7 @@ trial_damping(damping::FixedDamping, args...) = damping.α end # Adjust to guess fitting α best: - guess = α_next > α / 2 ? info_next.ψ : ψ + guess = α_next > α / 2 ? info_next.ψ : info.ψ α = α_next end @@ -347,7 +342,7 @@ trial_damping(damping::FixedDamping, args...) = damping.α ham = hamiltonian_with_total_potential(ham, info.Vout) info = (ham=ham, basis=basis, energies=info.energies, converged=converged, ρ=info.ρout, eigenvalues=info.eigenvalues, occupation=info.occupation, - εF=info.εF, n_iter=n_iter, n_ep_extra=n_ep_extra, ψ=info.ψ, + εF=info.εF, n_iter=n_iter, ψ=info.ψ, info.n_bands_converge, diagonalization=info.diagonalization, stage=:finalize, algorithm="SCF", occupation_threshold=info.occupation_threshold) callback(info) diff --git a/src/scf/self_consistent_field.jl b/src/scf/self_consistent_field.jl index e841a2e7a1..545d4beefd 100644 --- a/src/scf/self_consistent_field.jl +++ b/src/scf/self_consistent_field.jl @@ -6,74 +6,91 @@ include("scf_callbacks.jl") verbose = false end -function default_n_bands(model) - n_spin = model.n_spin_components - min_n_bands = div(model.n_electrons, n_spin * filled_occupation(model), RoundUp) - n_extra = model.temperature == 0 ? 0 : max(4, ceil(Int, 0.2 * n_spin * min_n_bands)) - min_n_bands + n_extra -end -default_occupation_threshold() = 1e-6 - """ -Obtain new density ρ by diagonalizing `ham`. +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; - n_bands=default_n_bands(ham.basis.model), - ψ=nothing, n_ep_extra=3, - eigensolver=lobpcg_hyper, - occupation_threshold, - kwargs...) - if ψ !== nothing +function next_density(ham::Hamiltonian, + nbandsalg::NbandsAlgorithm=AdaptiveBands(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) - for ik in 1:length(ham.basis.kpoints) - @assert size(ψ[ik], 2) == n_bands + n_ep_extra - end + n_bands_compute = max(n_bands_compute, maximum(ψk -> size(ψk, 2), ψ)) + increased_n_bands = n_bands_compute > size(ψ[1], 2) end - # Diagonalize - eigres = diagonalize_all_kblocks(eigensolver, ham, n_bands + n_ep_extra; ψguess=ψ, - n_conv_check=n_bands, kwargs...) + # 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) - # Update density from new ψ - occupation, εF = compute_occupation(ham.basis, eigres.λ; occupation_threshold) - ρout = compute_density(ham.basis, eigres.X, occupation) + # Check maximal occupation of the unconverged bands is sensible. + occupation, εF = compute_occupation(ham.basis, eigres.λ) + 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(basis)`) " * + "or request more converged bands than $n_bands_converge (e.g. " * + "`nbandsalg=AdaptiveBands(basis; n_bands_converge=$(n_bands_converge + 3)`)") + end - (ψ=eigres.X, eigenvalues=eigres.λ, occupation=occupation, εF=εF, - ρout=ρout, diagonalization=eigres) + ρout = compute_density(ham.basis, eigres.X, occupation) + (ψ=eigres.X, eigenvalues=eigres.λ, occupation, εF, ρout, diagonalization=eigres, + n_bands_converge, nbandsalg.occupation_threshold) end """ -Solve the Kohn-Sham equations with a SCF algorithm, starting at ρ. +Solve the Kohn-Sham equations with a SCF algorithm, starting at `ρ`. + +- `nbandsalg`: By default DFTK uses `nbandsalg=AdaptiveBands(basis)`, 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). """ -@timing function self_consistent_field(basis::PlaneWaveBasis; - n_bands=default_n_bands(basis.model), +@timing function self_consistent_field(basis::PlaneWaveBasis{T}; + n_bands=nothing, # TODO For backwards compatibility. + n_ep_extra=nothing, # TODO For backwards compatibility. ρ=guess_density(basis), ψ=nothing, tol=1e-6, maxiter=100, solver=scf_nlsolve_solver(), eigensolver=lobpcg_hyper, - n_ep_extra=3, determine_diagtol=ScfDiagtol(), damping=0.8, # Damping parameter mixing=LdosMixing(), is_converged=ScfConvergenceEnergy(tol), + nbandsalg::NbandsAlgorithm=AdaptiveBands(basis), callback=ScfDefaultCallback(; show_damping=false), compute_consistent_energies=true, - occupation_threshold=default_occupation_threshold(), # 1e-10 response=ResponseOptions(), # Dummy here, only for AD - ) - T = eltype(basis) - model = basis.model + ) where {T} + if !isnothing(n_bands) || !isnothing(n_ep_extra) + # TODO Backwards compatibility ... emulates exactly how bands worked before + Base.depwarn("The options n_bands and n_ep_extra of self_consistent_field " * + "are deprecated. Use `nbandsalg` instead to influence number of " * + "bands to compute.", :self_consistent_field) + n_bands_converge = something(n_bands, FixedBands(basis.model).n_bands_converge) + nbandsalg = FixedBands(; n_bands_converge, + n_bands_compute=n_bands_converge + something(n_ep_extra, 3)) + end # All these variables will get updated by fixpoint_map - if ψ !== nothing + if !isnothing(ψ) @assert length(ψ) == length(basis.kpoints) - for ik in 1:length(basis.kpoints) - @assert size(ψ[ik], 2) == n_bands + n_ep_extra - end end occupation = nothing eigenvalues = nothing @@ -89,35 +106,25 @@ Solve the Kohn-Sham equations with a SCF algorithm, starting at ρ. # TODO support other mixing types function fixpoint_map(ρin) converged && return ρin # No more iterations if convergence flagged - n_iter += 1 - # Build next Hamiltonian, diagonalize it, get ρout - if n_iter == 1 # first iteration - _, ham = energy_hamiltonian(basis, nothing, nothing; - ρ=ρin, eigenvalues=nothing, εF=nothing) - else - # 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) - end + # 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; n_bands, ψ, eigensolver, - miniter=1, tol=determine_diagtol(info), - n_ep_extra, occupation_threshold) + nextstate = next_density(ham, nbandsalg; 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, n_ep_extra, occupation_threshold, + ρ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=eigenvalues, εF=εF) + energies, _ = energy_hamiltonian(basis, ψ, occupation; ρ=ρout, eigenvalues, εF) end info = merge(info, (energies=energies, )) @@ -133,25 +140,23 @@ Solve the Kohn-Sham equations with a SCF algorithm, starting at ρ. end # Tolerance and maxiter are only dummy here: Convergence is flagged by is_converged - # inside the fixpoint_map. Also we do not use the return value of fpres but rather the - # one that got updated by fixpoint_map - fpres = solver(fixpoint_map, ρout, maxiter; tol=eps(T)) + # inside the fixpoint_map. + solver(fixpoint_map, ρout, maxiter; tol=eps(T)) - # We do not use the return value of fpres 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=eigenvalues, εF=εF) + # 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, occupation_threshold, - ρ=ρout, α=damping, eigenvalues, occupation, εF, - n_iter, n_ep_extra, ψ, info.diagonalization, - stage=:finalize, algorithm="SCF", norm_Δρ) + 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 diff --git a/src/supercell.jl b/src/supercell.jl index be15f145a9..e6bf889322 100644 --- a/src/supercell.jl +++ b/src/supercell.jl @@ -25,11 +25,11 @@ an input basis ``kgrid``. All other parameters are modified so that the respecti systems associated to both basis are equivalent. """ function cell_to_supercell(basis::PlaneWaveBasis) - @assert(iszero(basis.kshift)) + iszero(basis.kshift) || error("Only kshift of 0 implemented.") model = basis.model # Compute supercell model and basis parameters - supercell_size = Int64.(basis.kgrid) # Renaming for clarity + supercell_size = basis.kgrid # Renaming for clarity supercell = create_supercell(model.lattice, model.atoms, model.positions, supercell_size) supercell_fft_size = basis.fft_size .* supercell_size # Assemble new model and new basis @@ -39,8 +39,8 @@ function cell_to_supercell(basis::PlaneWaveBasis) basis.variational, [zeros(Float64, 3)], # kcoords [one(Float64)], # kweights - [1,1,1], # kgrid = Γ point only - zeros(Int64, 3), # kshift + ones(3), # kgrid = Γ point only + basis.kshift, # kshift false, # single point symmetry basis.comm_kpts, ) @@ -50,8 +50,8 @@ end Maps all ``k+G`` vectors of an given basis as ``G`` vectors of the supercell basis, in reduced coordinates. """ -function Gplusk_vectors_in_supercell(basis::PlaneWaveBasis, - basis_supercell::PlaneWaveBasis, kpt::Kpoint) +function Gplusk_vectors_in_supercell(basis::PlaneWaveBasis, basis_supercell::PlaneWaveBasis, + kpt::Kpoint) inv_recip_superlattice = compute_inverse_lattice(basis_supercell.model.recip_lattice) map(Gpk -> round.(Int64, inv_recip_superlattice*Gpk), Gplusk_vectors_cart(basis, kpt)) end @@ -63,12 +63,12 @@ The output ``ψ_supercell`` have a single component at ``Γ``-point, such that ``ψ_supercell[Γ][:,k+n]`` contains ``ψ[k][:,n]``, within normalization on the supercell. """ function cell_to_supercell(ψ, basis::PlaneWaveBasis{T}, - basis_supercell::PlaneWaveBasis{T}) where {T<:Real} + basis_supercell::PlaneWaveBasis{T}) where {T <: Real} # Ensure that the basis is unfolded. (prod(basis.kgrid) != length(basis.kpoints)) && (error("basis must be unfolded")) - num_kpG = sum(size.(ψ,1)) - num_bands = size(ψ[1],2) + num_kpG = sum(size.(ψ, 1)) + num_bands = size(ψ[1], 2) # Maps k+G vector of initial basis to a G vector of basis_supercell cell_supercell_mapping(kpt) = index_G_vectors.(basis_supercell, diff --git a/src/symmetry.jl b/src/symmetry.jl index 8f25ab41cd..ec170ce5f5 100644 --- a/src/symmetry.jl +++ b/src/symmetry.jl @@ -48,16 +48,17 @@ function symmetry_operations(lattice, atoms, positions, magnetic_moments=[]; [SymOp(W, w) for (W, w) in zip(Ws, ws)] end +# Approximate in; can be performance-critical, so we optimize in case of rationals +is_approx_in_(x::AbstractArray{<:Rational}, X) = any(isequal(x), X) +is_approx_in_(x::AbstractArray{T}, X) where {T} = any(y -> isapprox(x, y; atol=sqrt(eps(T))), X) + """ Filter out the symmetry operations that don't respect the symmetries of the discrete BZ grid """ function symmetries_preserving_kgrid(symmetries, kcoords) kcoords_normalized = normalize_kpoint_coordinate.(kcoords) - T = eltype(kcoords[1]) - atol = T <: Rational ? 0 : sqrt(eps(T)) - is_approx_in(x, X) = any(y -> isapprox(x, y; atol), X) function preserves_grid(symop) - all(is_approx_in(normalize_kpoint_coordinate(symop.S * k), kcoords_normalized) + all(is_approx_in_(normalize_kpoint_coordinate(symop.S * k), kcoords_normalized) for k in kcoords_normalized) end filter(preserves_grid, symmetries) @@ -243,7 +244,9 @@ function symmetrize_forces(model::Model, forces; symmetries) # (but careful that our symmetries are r -> Wr+w, not R(r+f)) other_at = W \ (position - w) i_other_at = findfirst(a -> is_approx_integer(a - other_at), positions_group) - symmetrized_forces[idx] += W * forces[group[i_other_at]] + # (A.27) is in cartesian coordinates, and since Wcart is orthogonal, + # Fsymcart = Wcart * Fcart <=> Fsymred = inv(Wred') Fred + symmetrized_forces[idx] += inv(W') * forces[group[i_other_at]] end end symmetrized_forces / length(symmetries) @@ -338,6 +341,6 @@ end Ensure its real-space equivalent of passed Fourier-space representation is entirely real by removing wavevectors `G` that don't have a `-G` counterpart in the basis. """ -function force_real!(basis, fourier_coeffs) +@timing function enforce_real!(basis, fourier_coeffs) lowpass_for_symmetry!(fourier_coeffs, basis; symmetries=[SymOp(-Mat3(I), Vec3(0, 0, 0))]) end diff --git a/src/terms/Hamiltonian.jl b/src/terms/Hamiltonian.jl index 8e67b218be..a287ae7c07 100644 --- a/src/terms/Hamiltonian.jl +++ b/src/terms/Hamiltonian.jl @@ -117,6 +117,7 @@ end H::DftHamiltonianBlock, ψ::AbstractArray) n_bands = size(ψ, 2) + iszero(n_bands) && return Hψ # Nothing to do if ψ empty have_divAgrad = !isnothing(H.divAgrad_op) # Notice that we use unnormalized plans for extra speed diff --git a/src/terms/anyonic.jl b/src/terms/anyonic.jl index 84967f4216..6708fc7e84 100644 --- a/src/terms/anyonic.jl +++ b/src/terms/anyonic.jl @@ -99,7 +99,8 @@ function TermAnyonic(basis::PlaneWaveBasis{T}, hbar, β) where {T} TermAnyonic(hbar, β, ρref, Aref) end -function ene_ops(term::TermAnyonic, basis::PlaneWaveBasis{T}, ψ, occ; ρ, kwargs...) where {T} +function ene_ops(term::TermAnyonic, basis::PlaneWaveBasis{T}, ψ, occupation; + ρ, kwargs...) where {T} hbar = term.hbar β = term.β @assert ψ !== nothing # the hamiltonian depends explicitly on ψ @@ -131,7 +132,7 @@ function ene_ops(term::TermAnyonic, basis::PlaneWaveBasis{T}, ψ, occ; ρ, kwarg β^2 .* (abs2.(Areal[1]) .+ abs2.(Areal[2])))] # Now compute effective local potential - 2β x^⟂/|x|² ∗ (βAρ + J) - J = compute_current(basis, ψ, occ) + J = compute_current(basis, ψ, occupation) eff_current = [hbar .* J[α] .+ β .* ρ .* Areal[α] for α = 1:2] eff_current_fourier = [fft(basis, eff_current[α]) for α = 1:2] @@ -156,7 +157,7 @@ function ene_ops(term::TermAnyonic, basis::PlaneWaveBasis{T}, ψ, occ; ρ, kwarg ψnk = @views ψ[1][:, iband] # TODO optimize this for op in ops_energy - E += occ[1][iband] * real(dot(ψnk, op * ψnk)) + E += occupation[1][iband] * real(dot(ψnk, op * ψnk)) end end diff --git a/src/terms/entropy.jl b/src/terms/entropy.jl index 8a29a33065..5a4e7d5df8 100644 --- a/src/terms/entropy.jl +++ b/src/terms/entropy.jl @@ -8,13 +8,16 @@ struct Entropy end (::Entropy)(basis) = TermEntropy() struct TermEntropy <: Term end -function ene_ops(term::TermEntropy, basis::PlaneWaveBasis{T}, ψ, occ; kwargs...) where {T} +function ene_ops(term::TermEntropy, basis::PlaneWaveBasis{T}, ψ, occupation; + kwargs...) where {T} ops = [NoopOperator(basis, kpt) for kpt in basis.kpoints] smearing = basis.model.smearing temperature = basis.model.temperature iszero(temperature) && return (E=zero(T), ops=ops) - isnothing(ψ) && return (E=T(Inf), ops=ops) + if isnothing(ψ) || isnothing(occupation) + return (E=T(Inf), ops=ops) + end !(:εF in keys(kwargs)) && return (E=T(Inf), ops=ops) !(:eigenvalues in keys(kwargs)) && return (E=T(Inf), ops=ops) diff --git a/src/terms/ewald.jl b/src/terms/ewald.jl index ea0022a8d7..b3c26c6b45 100644 --- a/src/terms/ewald.jl +++ b/src/terms/ewald.jl @@ -15,12 +15,12 @@ function TermEwald(basis::PlaneWaveBasis{T}) where {T} TermEwald(T(energy_ewald(basis.model))) end -function ene_ops(term::TermEwald, basis::PlaneWaveBasis, ψ, occ; kwargs...) +function ene_ops(term::TermEwald, basis::PlaneWaveBasis, ψ, occupation; kwargs...) (E=term.energy, ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end @timing "forces: Ewald" function compute_forces(term::TermEwald, basis::PlaneWaveBasis{T}, - ψ, occ; kwargs...) where {T} + ψ, occupation; kwargs...) where {T} # TODO this could be precomputed forces = zero(basis.model.positions) energy_ewald(basis.model; forces) @@ -29,12 +29,7 @@ end function energy_ewald(model::Model{T}; kwargs...) where {T} isempty(model.atoms) && return zero(T) - - # DFTK currently assumes that the compensating charge in the electronic and nuclear - # terms is equal and of opposite sign. See also the PSP correction term, where - # n_electrons is used synonymously for sum of charges charges = T.(charge_ionic.(model.atoms)) - @assert sum(charges) == model.n_electrons energy_ewald(model.lattice, charges, model.positions; kwargs...) end diff --git a/src/terms/hartree.jl b/src/terms/hartree.jl index 69315d4c71..22ad775894 100644 --- a/src/terms/hartree.jl +++ b/src/terms/hartree.jl @@ -32,19 +32,14 @@ function TermHartree(basis::PlaneWaveBasis{T}, scaling_factor) where {T} # Solving the Poisson equation ΔV = -4π ρ in Fourier space # is multiplying elementwise by 4π / |G|^2. poisson_green_coeffs = 4T(π) ./ [sum(abs2, G) for G in G_vectors_cart(basis)] - if !isempty(model.atoms) - # Assume positive charge from nuclei is exactly compensated by the electrons - sum_charges = sum(charge_ionic, model.atoms) - @assert sum_charges == model.n_electrons - end poisson_green_coeffs[1] = 0 # Compensating charge background => Zero DC - force_real!(basis, poisson_green_coeffs) # Symmetrize Fourier coeffs to have real iFFT + enforce_real!(basis, poisson_green_coeffs) # Symmetrize Fourier coeffs to have real iFFT TermHartree(T(scaling_factor), T(scaling_factor) .* poisson_green_coeffs) end @timing "ene_ops: hartree" function ene_ops(term::TermHartree, basis::PlaneWaveBasis{T}, - ψ, occ; ρ, kwargs...) where {T} + ψ, occupation; ρ, kwargs...) where {T} ρtot_fourier = fft(basis, total_density(ρ)) pot_fourier = term.poisson_green_coeffs .* ρtot_fourier pot_real = irfft(basis, pot_fourier) diff --git a/src/terms/kinetic.jl b/src/terms/kinetic.jl index b825f05e47..9a83877bec 100644 --- a/src/terms/kinetic.jl +++ b/src/terms/kinetic.jl @@ -25,16 +25,18 @@ function TermKinetic(basis::PlaneWaveBasis{T}, scaling_factor, blowup) where {T} end @timing "ene_ops: kinetic" function ene_ops(term::TermKinetic, basis::PlaneWaveBasis{T}, - ψ, occ; kwargs...) where {T} + ψ, occupation; kwargs...) where {T} ops = [FourierMultiplication(basis, kpoint, term.kinetic_energies[ik]) for (ik, kpoint) in enumerate(basis.kpoints)] - isnothing(ψ) && return (E=T(Inf), ops=ops) + if isnothing(ψ) || isnothing(occupation) + return (E=T(Inf), ops=ops) + end E = zero(T) - for (ik, k) in enumerate(basis.kpoints) - for iband = 1:size(ψ[ik], 2) - ψnk = @views ψ[ik][:, iband] - E += (basis.kweights[ik] * occ[ik][iband] + for (ik, ψk) in enumerate(ψ) + for iband = 1:size(ψk, 2) + ψnk = @views ψk[:, iband] + E += (basis.kweights[ik] * occupation[ik][iband] * real(dot(ψnk, Diagonal(term.kinetic_energies[ik]), ψnk))) end end @@ -52,7 +54,7 @@ struct BlowupIdentity end """ -Blow-up function as proposed in [REF paper Cancès, Hassan, Vidal to be submitted] +Blow-up function as proposed in https://hal.archives-ouvertes.fr/hal-03794000 The blow-up order of the function is fixed to ensure C^2 regularity of the energies bands away from crossings and Lipschitz continuity at crossings. """ diff --git a/src/terms/local.jl b/src/terms/local.jl index aee77677c8..3aa1fb1285 100644 --- a/src/terms/local.jl +++ b/src/terms/local.jl @@ -7,7 +7,8 @@ abstract type TermLocalPotential <: Term end @timing "ene_ops: local" function ene_ops(term::TermLocalPotential, - basis::PlaneWaveBasis{T}, ψ, occ; kwargs...) where {T} + basis::PlaneWaveBasis{T}, ψ, occupation; + kwargs...) where {T} potview(data, spin) = ndims(data) == 4 ? (@view data[:, :, :, spin]) : data ops = [RealSpaceMultiplication(basis, kpt, potview(term.potential_values, kpt.spin)) for kpt in basis.kpoints] @@ -51,7 +52,7 @@ function (external::ExternalFromFourier)(basis::PlaneWaveBasis{T}) where {T} pot_fourier = map(G_vectors_cart(basis)) do G convert_dual(complex(T), external.potential(G) / sqrt(unit_cell_volume)) end - force_real!(basis, pot_fourier) # Symmetrize Fourier coeffs to have real iFFT + enforce_real!(basis, pot_fourier) # Symmetrize Fourier coeffs to have real iFFT TermExternal(irfft(basis, pot_fourier)) end @@ -69,7 +70,6 @@ Atomic local potential defined by `model.atoms`. struct AtomicLocal end function (::AtomicLocal)(basis::PlaneWaveBasis{T}) where {T} model = basis.model - # pot_fourier is expanded in a basis of e_{G-G'} # Since V is a sum of radial functions located at atomic # positions, this involves a form factor (`local_potential_fourier`) @@ -83,7 +83,7 @@ function (::AtomicLocal)(basis::PlaneWaveBasis{T}) where {T} end pot / sqrt(model.unit_cell_volume) end - force_real!(basis, pot_fourier) # Symmetrize Fourier coeffs to have real iFFT + enforce_real!(basis, pot_fourier) # Symmetrize Fourier coeffs to have real iFFT pot_real = irfft(basis, pot_fourier) TermAtomicLocal(pot_real) diff --git a/src/terms/local_nonlinearity.jl b/src/terms/local_nonlinearity.jl index 5ee9d069e9..ca7f05e7af 100644 --- a/src/terms/local_nonlinearity.jl +++ b/src/terms/local_nonlinearity.jl @@ -9,7 +9,8 @@ struct TermLocalNonlinearity{TF} <: TermNonlinear end (L::LocalNonlinearity)(::AbstractBasis) = TermLocalNonlinearity(L.f) -function ene_ops(term::TermLocalNonlinearity, basis::PlaneWaveBasis{T}, ψ, occ; ρ, kwargs...) where {T} +function ene_ops(term::TermLocalNonlinearity, basis::PlaneWaveBasis{T}, ψ, occupation; + ρ, kwargs...) where {T} fp(ρ) = ForwardDiff.derivative(term.f, ρ) E = sum(term.f.(ρ)) * basis.dvol potential = convert_dual.(T, fp.(ρ)) diff --git a/src/terms/magnetic.jl b/src/terms/magnetic.jl index e93bccaea3..3dd43fc336 100644 --- a/src/terms/magnetic.jl +++ b/src/terms/magnetic.jl @@ -30,17 +30,20 @@ function TermMagnetic(basis::PlaneWaveBasis{T}, Afunction::Function) where {T} TermMagnetic(Apotential) end -function ene_ops(term::TermMagnetic, basis::PlaneWaveBasis{T}, ψ, occ; kwargs...) where {T} +function ene_ops(term::TermMagnetic, basis::PlaneWaveBasis{T}, ψ, occupation; + kwargs...) where {T} ops = [MagneticFieldOperator(basis, kpoint, term.Apotential) for (ik, kpoint) in enumerate(basis.kpoints)] - isnothing(ψ) && return (E=T(Inf), ops=ops) + if isnothing(ψ) || isnothing(occupation) + return (E=T(Inf), ops=ops) + end E = zero(T) for (ik, k) in enumerate(basis.kpoints) for iband = 1:size(ψ[1], 2) ψnk = @views ψ[ik][:, iband] # TODO optimize this - E += basis.kweights[ik] * occ[ik][iband] * real(dot(ψnk, ops[ik] * ψnk)) + E += basis.kweights[ik] * occupation[ik][iband] * real(dot(ψnk, ops[ik] * ψnk)) end end E = mpi_sum(E, basis.comm_kpts) diff --git a/src/terms/nonlocal.jl b/src/terms/nonlocal.jl index 7b7ff331d6..85fb873f15 100644 --- a/src/terms/nonlocal.jl +++ b/src/terms/nonlocal.jl @@ -27,14 +27,16 @@ end @timing "ene_ops: nonlocal" function ene_ops(term::TermAtomicNonlocal, basis::PlaneWaveBasis{T}, - ψ, occ; kwargs...) where {T} - isnothing(ψ) && return (E=T(Inf), ops=term.ops) + ψ, occupation; kwargs...) where {T} + if isnothing(ψ) || isnothing(occupation) + return (E=T(Inf), ops=term.ops) + end E = zero(T) - for (ik, kpt) in enumerate(basis.kpoints) - Pψ = term.ops[ik].P' * ψ[ik] # nproj x nband - band_enes = dropdims(sum(real.(conj.(Pψ) .* (term.ops[ik].D * Pψ)), dims=1), dims=1) - E += basis.kweights[ik] * sum(band_enes .* occ[ik]) + for (ik, ψk) in enumerate(ψ) + Pψk = term.ops[ik].P' * ψk # nproj x nband + band_enes = dropdims(sum(real.(conj.(Pψk) .* (term.ops[ik].D * Pψk)), dims=1), dims=1) + E += basis.kweights[ik] * sum(band_enes .* occupation[ik]) end E = mpi_sum(E, basis.comm_kpts) @@ -43,7 +45,7 @@ end @timing "forces: nonlocal" function compute_forces(::TermAtomicNonlocal, basis::PlaneWaveBasis{TT}, - ψ, occ; kwargs...) where {TT} + ψ, occupation; kwargs...) where {TT} T = promote_type(TT, real(eltype(ψ[1]))) model = basis.model unit_cell_volume = model.unit_cell_volume @@ -73,7 +75,7 @@ end dPdR = [-2T(π)*im*q[α] for q in qs] .* P ψk = ψ[ik] dHψk = P * (C * (dPdR' * ψk)) - -sum(occ[ik][iband] * basis.kweights[ik] * + -sum(occupation[ik][iband] * basis.kweights[ik] * 2real(dot(ψk[:, iband], dHψk[:, iband])) for iband=1:size(ψk, 2)) end # α diff --git a/src/terms/pairwise.jl b/src/terms/pairwise.jl index 12f1173cc8..0797247e07 100644 --- a/src/terms/pairwise.jl +++ b/src/terms/pairwise.jl @@ -39,12 +39,12 @@ struct TermPairwisePotential{TV, Tparams, T} <:Term energy::T end -function ene_ops(term::TermPairwisePotential, basis::PlaneWaveBasis, ψ, occ; kwargs...) +function ene_ops(term::TermPairwisePotential, basis::PlaneWaveBasis, ψ, occupation; kwargs...) (E=term.energy, ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end @timing "forces: Pairwise" function compute_forces(term::TermPairwisePotential, - basis::PlaneWaveBasis{T}, ψ, occ; + basis::PlaneWaveBasis{T}, ψ, occupation; kwargs...) where {T} forces = zero(basis.model.positions) energy_pairwise(basis.model, term.V, term.params; term.max_radius, forces) diff --git a/src/terms/psp_correction.jl b/src/terms/psp_correction.jl index fa6b20f049..2c0e30903b 100644 --- a/src/terms/psp_correction.jl +++ b/src/terms/psp_correction.jl @@ -15,7 +15,7 @@ function TermPspCorrection(basis::PlaneWaveBasis) TermPspCorrection(energy_psp_correction(model)) end -function ene_ops(term::TermPspCorrection, basis::PlaneWaveBasis, ψ, occ; kwargs...) +function ene_ops(term::TermPspCorrection, basis::PlaneWaveBasis, ψ, occupation; kwargs...) (E=term.energy, ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end diff --git a/src/terms/terms.jl b/src/terms/terms.jl index c6e32e0816..58d6b74411 100644 --- a/src/terms/terms.jl +++ b/src/terms/terms.jl @@ -4,7 +4,8 @@ include("operators.jl") # - A Term is something that, given a state, returns a named tuple (E, hams) with an energy # and a list of RealFourierOperator (for each kpoint). # - Each term must overload -# `ene_ops(term, basis, ψ, occ; kwargs...)` -> (E::Real, ops::Vector{RealFourierOperator}). +# `ene_ops(term, basis, ψ, occupation; kwargs...)` +# -> (E::Real, ops::Vector{RealFourierOperator}). # - Note that terms are allowed to hold on to references to ψ (eg Fock term), # so ψ should not mutated after ene_ops @@ -25,7 +26,7 @@ abstract type TermNonlinear <: Term end A term with a constant zero energy. """ struct TermNoop <: Term end -function ene_ops(term::TermNoop, basis::PlaneWaveBasis{T}, ψ, occ; kwargs...) where {T} +function ene_ops(term::TermNoop, basis::PlaneWaveBasis{T}, ψ, occupation; kwargs...) where {T} (E=zero(eltype(T)), ops=[NoopOperator(basis, kpt) for kpt in basis.kpoints]) end @@ -57,8 +58,8 @@ breaks_symmetries(::Magnetic) = true include("anyonic.jl") breaks_symmetries(::Anyonic) = true -# forces computes either nothing or an array forces[at][α] -compute_forces(::Term, ::AbstractBasis, ψ, occ; kwargs...) = nothing # by default, no force +# forces computes either nothing or an array forces[at][α] (by default no forces) +compute_forces(::Term, ::AbstractBasis, ψ, occupation; kwargs...) = nothing @doc raw""" compute_kernel(basis::PlaneWaveBasis; kwargs...) diff --git a/src/terms/xc.jl b/src/terms/xc.jl index f8af1e11f9..bccbf9ba2f 100644 --- a/src/terms/xc.jl +++ b/src/terms/xc.jl @@ -44,7 +44,7 @@ struct TermXc{T} <: TermNonlinear where {T} end @views @timing "ene_ops: xc" function ene_ops(term::TermXc, basis::PlaneWaveBasis{T}, - ψ, occ; ρ, τ=nothing, kwargs...) where {T} + ψ, occupation; ρ, τ=nothing, kwargs...) where {T} @assert !isempty(term.functionals) model = basis.model @@ -53,10 +53,10 @@ end # Compute kinetic energy density, if needed. if isnothing(τ) && any(needs_τ, term.functionals) - if isnothing(ψ) || isnothing(occ) + if isnothing(ψ) || isnothing(occupation) τ = zero(ρ) else - τ = compute_kinetic_energy_density(basis, ψ, occ) + τ = compute_kinetic_energy_density(basis, ψ, occupation) end end diff --git a/src/interpolation_transfer.jl b/src/transfer.jl similarity index 56% rename from src/interpolation_transfer.jl rename to src/transfer.jl index 87da904c99..a384343a58 100644 --- a/src/interpolation_transfer.jl +++ b/src/transfer.jl @@ -1,95 +1,5 @@ -import Interpolations -import Interpolations: interpolate, extrapolate, scale, BSpline, Quadratic, OnCell using SparseArrays -""" -Interpolate a function expressed in a basis `basis_in` to a basis `basis_out` -This interpolation uses a very basic real-space algorithm, and makes -a DWIM-y attempt to take into account the fact that basis_out can be a supercell of basis_in -""" -function interpolate_density(ρ_in, basis_in::PlaneWaveBasis, basis_out::PlaneWaveBasis) - ρ_out = interpolate_density(ρ_in, basis_in.fft_size, basis_out.fft_size, - basis_in.model.lattice, basis_out.model.lattice) -end - -# TODO Specialization for the common case lattice_out = lattice_in -function interpolate_density(ρ_in::AbstractArray, grid_in, grid_out, lattice_in, lattice_out=lattice_in) - T = real(eltype(ρ_in)) - @assert size(ρ_in) == grid_in - - # First, build supercell, array of 3 ints - supercell = zeros(Int, 3) - for i = 1:3 - if norm(lattice_in[:, i]) == 0 - @assert norm(lattice_out[:, i]) == 0 - supercell[i] = 1 - else - supercell[i] = round(Int, norm(lattice_out[:, i]) / norm(lattice_in[:, i])) - end - if norm(lattice_out[:, i] - supercell[i]*lattice_in[:, i]) > .3*norm(lattice_out[:, i]) - @warn "In direction $i, the output lattice is very different from the input lattice" - end - end - - # ρ_in represents a periodic function, on a grid 0, 1/N, ... (N-1)/N - grid_supercell = grid_in .* supercell - ρ_in_supercell = similar(ρ_in, (grid_supercell...)) - for i = 1:supercell[1] - for j = 1:supercell[2] - for k = 1:supercell[3] - ρ_in_supercell[ - 1 + (i-1)*grid_in[1] : i*grid_in[1], - 1 + (j-1)*grid_in[2] : j*grid_in[2], - 1 + (k-1)*grid_in[3] : k*grid_in[3]] = ρ_in - end - end - end - - # interpolate ρ_in_supercell from grid grid_supercell to grid_out - axes_in = (range(0, 1, length=grid_supercell[i]+1)[1:end-1] for i=1:3) - itp = interpolate(ρ_in_supercell, BSpline(Quadratic(Interpolations.Periodic(OnCell())))) - sitp = scale(itp, axes_in...) - ρ_interp = extrapolate(sitp, Periodic()) - ρ_out = similar(ρ_in, grid_out) - for i = 1:grid_out[1] - for j = 1:grid_out[2] - for k = 1:grid_out[3] - ρ_out[i, j, k] = ρ_interp((i-1)/grid_out[1], - (j-1)/grid_out[2], - (k-1)/grid_out[3]) - end - end - end - - ρ_out -end - -""" -Interpolate some data from one ``k``-point to another. The interpolation is fast, but not -necessarily exact or even normalized. Intended only to construct guesses for iterative -solvers -""" -function interpolate_kpoint(data_in::AbstractVecOrMat, - basis_in::PlaneWaveBasis, kpoint_in::Kpoint, - basis_out::PlaneWaveBasis, kpoint_out::Kpoint) - # TODO merge with transfer_blochwave_kpt - if kpoint_in == kpoint_out - return copy(data_in) - end - @assert length(G_vectors(basis_in, kpoint_in)) == size(data_in, 1) - - n_bands = size(data_in, 2) - n_Gk_out = length(G_vectors(basis_out, kpoint_out)) - data_out = similar(data_in, n_Gk_out, n_bands) .= 0 - for iin in 1:size(data_in, 1) - idx_fft = kpoint_in.mapping[iin] - idx_fft in keys(kpoint_out.mapping_inv) || continue - iout = kpoint_out.mapping_inv[idx_fft] - data_out[iout, :] = data_in[iin, :] - end - data_out -end - """ Compute the index mapping between two bases. Returns two arrays `idcs_in` and `idcs_out` such that `ψkout[idcs_out] = ψkin[idcs_in]` does @@ -126,7 +36,6 @@ function transfer_mapping(basis_in::PlaneWaveBasis{T}, kpt_in::Kpoint, idcs_in, idcs_out end - """ Return a sparse matrix that maps quantities given on `basis_in` and `kpt_in` to quantities on `basis_out` and `kpt_out`. @@ -137,7 +46,6 @@ function compute_transfer_matrix(basis_in::PlaneWaveBasis{T}, kpt_in::Kpoint, sparse(idcs_out, idcs_in, true) end - """ Return a list of sparse matrices (one per ``k``-point) that map quantities given in the `basis_in` basis to quantities given in the `basis_out` basis. @@ -151,9 +59,8 @@ function compute_transfer_matrix(basis_in::PlaneWaveBasis{T}, basis_out::PlaneWa for (kpt_in, kpt_out) in zip(basis_in.kpoints, basis_out.kpoints)] end - """ -Transfer an array ψk defined on basis_in ``k``-point kpt_in to basis_out ``k``-point kpt_out. +Transfer an array `ψk` defined on basis_in ``k``-point kpt_in to basis_out ``k``-point kpt_out. """ function transfer_blochwave_kpt(ψk_in, basis_in::PlaneWaveBasis{T}, kpt_in::Kpoint, basis_out::PlaneWaveBasis{T}, kpt_out::Kpoint) where {T} @@ -169,6 +76,58 @@ function transfer_blochwave_kpt(ψk_in, basis_in::PlaneWaveBasis{T}, kpt_in::Kpo ψk_out end +""" +Transfer an array `ψk_in` expanded on `kpt_in`, and produce ``ψ(r) e^{i ΔG·r}`` expanded on +`kpt_out`. It is mostly useful for phonons. +Beware: `ψk_out` can lose information if the shift `ΔG` is large or if the `G_vectors` +differ between `k`-points. +""" +function transfer_blochwave_kpt(ψk_in, basis::PlaneWaveBasis, kpt_in, kpt_out, ΔG) + ψk_out = zeros(eltype(ψk_in), length(G_vectors(basis, kpt_out)), size(ψk_in, 2)) + for (iG, G) in enumerate(G_vectors(basis, kpt_in)) + # e^i(kpt_in + G)r = e^i(kpt_out + G')r, where + # kpt_out + G' = kpt_in + G = kpt_out + ΔG + G, and + # G' = G + ΔG + idx_Gp_in_kpoint = index_G_vectors(basis, kpt_out, G - ΔG) + if !isnothing(idx_Gp_in_kpoint) + ψk_out[idx_Gp_in_kpoint, :] = ψk_in[iG, :] + end + end + ψk_out +end + +""" +Find the equivalent index of the coordinate `kcoord` ∈ ℝ³ in a list `kcoords` ∈ [-½, ½)³. +`ΔG` is the vector of ℤ³ such that `kcoords[index] = kcoord + ΔG`. +""" +function find_equivalent_kpt(kcoords::Vector{Vec3{T}}, kcoord; tol=100*eps(T)) where {T} + kcoord_normalized = mod.(Vector(kcoord), 1) # coordinate in [0, 1)³ + kcoord_normalized[kcoord_normalized .≥ 0.5 - tol] .-= 1 # coordinate in [-½, ½) + + indices = findall(e -> is_approx_integer(e - kcoord_normalized; tol), kcoords) + index = only(indices) + + ΔG = kcoord_normalized - kcoord + + # ΔG should be an integer. + @assert all(is_approx_integer.(ΔG; tol)) + ΔG = round.(Int, ΔG) + + return (; index=index, ΔG) +end + +""" +Return the Fourier coefficients for `ψk · e^{i q·r}` in the basis of `kpt_out`, where `ψk` +is defined on a basis `kpt_in`. +""" +function multiply_by_expiqr(basis, kpt_in, q, ψk; transfer_fn=transfer_blochwave_kpt) + kcoords = getfield.(basis.kpoints, :coordinate) + shifted_kcoord = kpt_in.coordinate .+ q # coordinate of ``k``-point in ℝ + (index, ΔG) = find_equivalent_kpt(kcoords, shifted_kcoord) + kpt_out = basis.kpoints[index] + return transfer_fn(ψk, basis, kpt_in, kpt_out, ΔG) +end + """ Transfer Bloch wave between two basis sets. Limited feature set. """ diff --git a/src/workarounds/cuda_arrays.jl b/src/workarounds/cuda_arrays.jl new file mode 100644 index 0000000000..0ed1e2deb6 --- /dev/null +++ b/src/workarounds/cuda_arrays.jl @@ -0,0 +1,12 @@ +using LinearAlgebra + +# https://github.com/JuliaGPU/CUDA.jl/issues/1572 +function LinearAlgebra.eigen(A::Hermitian{T,AT}) where {T<:Complex,AT<:CUDA.CuArray} + vals, vects = CUDA.CUSOLVER.heevd!('V', 'U', A.data) + (vectors = vects, values = vals) +end + +function LinearAlgebra.eigen(A::Hermitian{T,AT}) where {T<:Real,AT<:CUDA.CuArray} + vals, vects = CUDA.CUSOLVER.syevd!('V', 'U', A.data) + (vectors = vects, values = vals) +end diff --git a/src/workarounds/forwarddiff_rules.jl b/src/workarounds/forwarddiff_rules.jl index b88766cbbc..5a21fea069 100644 --- a/src/workarounds/forwarddiff_rules.jl +++ b/src/workarounds/forwarddiff_rules.jl @@ -244,11 +244,11 @@ function self_consistent_field(basis_dual::PlaneWaveBasis{T}; energies, ham = energy_hamiltonian(basis_dual, ψ, occupation; ρ, eigenvalues, εF) # This has to be changed whenever the scfres structure changes - (; ham, basis=basis_dual, energies, ρ, eigenvalues, occupation, εF, ψ, + (; ham, basis=basis_dual, energies, ρ, eigenvalues, occupation, εF, ψ, # non-differentiable metadata: response=getfield.(δresults, :history), - scfres.converged, scfres.occupation_threshold, scfres.α, scfres.n_iter, - scfres.n_ep_extra, scfres.diagonalization, scfres.stage, + scfres.converged, scfres.occupation_threshold, scfres.α, scfres.n_iter, + scfres.n_bands_converge, scfres.diagonalization, scfres.stage, scfres.algorithm, scfres.norm_Δρ) end diff --git a/src/workarounds/gpu_arrays.jl b/src/workarounds/gpu_arrays.jl new file mode 100644 index 0000000000..5c9fc9a478 --- /dev/null +++ b/src/workarounds/gpu_arrays.jl @@ -0,0 +1,5 @@ +using LinearAlgebra +using GPUArraysCore + +# https://github.com/JuliaGPU/CUDA.jl/issues/1565 +LinearAlgebra.dot(x::AbstractGPUArray, D::Diagonal, y::AbstractGPUArray) = x' * (D * y) diff --git a/test/Model.jl b/test/Model.jl index c3059cce76..decdf04c69 100644 --- a/test/Model.jl +++ b/test/Model.jl @@ -34,3 +34,15 @@ include("testcases.jl") @test DFTK.comatrix_cart_to_red(model, DFTK.comatrix_red_to_cart(model, Bred)) ≈ Bred @test DFTK.matrix_cart_to_red(model, DFTK.matrix_red_to_cart(model, Ared)) ≈ Ared end + +@testset "Violation of charge neutrality" begin + # This is fine as no Coulomb electrostatics + Model(silicon.lattice; εF=0.1) + Model(silicon.lattice; n_electrons=1) + + # Violation of charge neutrality should throw for models with atoms. + @test_throws ErrorException model_LDA(silicon.lattice, silicon.atoms, + silicon.positions; εF=0.1) + @test_throws ErrorException model_LDA(silicon.lattice, silicon.atoms, + silicon.positions; n_electrons=1) +end diff --git a/test/PlaneWaveBasis.jl b/test/PlaneWaveBasis.jl index 71eece0481..78a41337fd 100644 --- a/test/PlaneWaveBasis.jl +++ b/test/PlaneWaveBasis.jl @@ -6,7 +6,7 @@ using LinearAlgebra include("testcases.jl") function test_pw_cutoffs(testcase, Ecut, fft_size) - model = Model(testcase.lattice; testcase.n_electrons) + model = Model(testcase.lattice) basis = PlaneWaveBasis(model; Ecut, fft_size, kgrid=(2, 5, 5), kshift=[1, 0, 0]/2) for (ik, kpt) in enumerate(basis.kpoints) @@ -53,7 +53,7 @@ end end @testset "PlaneWaveBasis: Check cubic basis and cubic index" begin - model = Model(silicon.lattice; silicon.n_electrons) + model = Model(silicon.lattice) basis = PlaneWaveBasis(model; Ecut=3, fft_size=(15, 15, 15), kgrid=(1, 1, 1)) g_all = collect(G_vectors(basis)) @@ -109,23 +109,25 @@ end model = Model(silicon.lattice, silicon.atoms, silicon.positions) basis = PlaneWaveBasis(model, Ecut, silicon.kcoords, silicon.kweights; fft_size) + # `isapprox` and not `==` because of https://github.com/JuliaLang/julia/issues/46849 + atol = 20eps(eltype(basis)) + @test length(G_vectors(fft_size)) == prod(fft_size) @test length(r_vectors(basis)) == prod(fft_size) - @test all(G_vectors(basis) .== G_vectors(fft_size)) - @test all(G_vectors_cart(basis) .== map(G -> model.recip_lattice * G, - G_vectors(fft_size))) - @test all(r_vectors_cart(basis) .== map(r -> model.lattice * r, - r_vectors(basis))) + @test G_vectors(basis) ≈ G_vectors(fft_size) atol=atol + @test G_vectors_cart(basis) ≈ map(G -> model.recip_lattice * G, + G_vectors(fft_size)) atol=atol + @test r_vectors_cart(basis) ≈ map(r -> model.lattice * r, r_vectors(basis)) atol=atol for kpt in basis.kpoints @test length(G_vectors(basis, kpt)) == length(kpt.mapping) - @test all(G_vectors_cart(basis, kpt) .== map(G -> model.recip_lattice * G, - G_vectors(basis, kpt))) - @test all(Gplusk_vectors(basis, kpt) .== map(G -> G + kpt.coordinate, - G_vectors(basis, kpt))) - @test all(Gplusk_vectors_cart(basis, kpt) .== map(q -> model.recip_lattice * q, - Gplusk_vectors(basis, kpt))) + @test G_vectors_cart(basis, kpt) ≈ map(G -> model.recip_lattice * G, + G_vectors(basis, kpt)) atol=atol + @test Gplusk_vectors(basis, kpt) ≈ map(G -> G + kpt.coordinate, + G_vectors(basis, kpt)) atol=atol + @test Gplusk_vectors_cart(basis, kpt) ≈ map(q -> model.recip_lattice * q, + Gplusk_vectors(basis, kpt)) atol=atol end end diff --git a/test/PspHgh.jl b/test/PspHgh.jl index c7024cb65e..da1f5ffb77 100644 --- a/test/PspHgh.jl +++ b/test/PspHgh.jl @@ -2,6 +2,7 @@ using Test using DFTK: load_psp, eval_psp_projector_fourier, eval_psp_local_fourier using DFTK: eval_psp_projector_real, psp_local_polynomial, eval_psp_local_real using DFTK: psp_projector_polynomial, qcut_psp_projector, qcut_psp_local +using DFTK: eval_psp_energy_correction using SpecialFunctions: besselj using QuadGK @@ -144,7 +145,7 @@ end end @testset "Potentials are consistent in real and Fourier space" begin - reg_param = 0.001 # divergent integral, needs regularization + reg_param = 1e-3 # divergent integral, needs regularization function integrand(psp, q, r) 4π * eval_psp_local_real(psp, r) * exp(-reg_param * r) * sin(q*r) / q * r end @@ -157,3 +158,32 @@ end end end end + +@testset "PSP energy correction is consistent with real-space potential" begin + reg_param = 1e-6 # divergent integral, needs regularization + q_small = 1e-6 # We are interested in q→0 term + function integrand(psp, n_electrons, r) + # Difference of potential of point-like atom (what is assumed in Ewald) + # versus actual structure of the pseudo potential + coulomb = -psp.Zion / r + diff = n_electrons * (eval_psp_local_real(psp, r) - coulomb) + 4π * diff * exp(-reg_param * r) * sin(q_small*r) / q_small * r + end + + n_electrons = 20 + for pspfile in ["Au-q11", "Ba-q10"] + psp = load_psp("hgh/lda/" * pspfile) + reference = quadgk(r -> integrand(psp, n_electrons, r), 0, Inf)[1] + @test reference ≈ eval_psp_energy_correction(psp, n_electrons) atol=1e-2 + end +end + +@testset "PSP energy correction is consistent with fourier-space potential" begin + q_small = 1e-3 # We are interested in q→0 term + for pspfile in ["Au-q11", "Ba-q10"] + psp = load_psp("hgh/lda/" * pspfile) + coulomb = -4π * psp.Zion / q_small^2 + reference = eval_psp_local_fourier(psp, q_small) - coulomb + @test reference ≈ eval_psp_energy_correction(psp, 1) atol=1e-3 + end +end diff --git a/test/cg.jl b/test/cg.jl new file mode 100644 index 0000000000..3997512226 --- /dev/null +++ b/test/cg.jl @@ -0,0 +1,30 @@ +using DFTK +using Test +using IterativeSolvers +using LinearAlgebra: norm + +function test_cg(T) + @testset "Test conjugate gradient method for $T" begin + n = 10 + A = rand(Complex{T}, n, n) + A = A' * A + I + b = rand(Complex{T}, n) + tol = 1e-10 * (T == Float64) + 1e-5 * (T == Float32) + + res = DFTK.cg(A, b; tol, maxiter=2n) + + # test convergence + @test norm(A*res.x - b) ≤ tol + @test res.converged + + # test type stability + f(b) = DFTK.cg(A, b; tol, maxiter=2n).x + g(b) = DFTK.cg(A, b; tol, maxiter=2n).residual_norm + @test res.x ≈ @inferred f(b) + @test tol ≥ @inferred g(b) + end +end + +for T in (Float32, Float64) + test_cg(T) +end diff --git a/test/chi0.jl b/test/chi0.jl index df05189900..af7494a8e1 100644 --- a/test/chi0.jl +++ b/test/chi0.jl @@ -6,18 +6,17 @@ using LinearAlgebra: norm include("testcases.jl") -function test_chi0(testcase; symmetries=false, temperature=0, - spin_polarization=:none, eigensolver=lobpcg_hyper, Ecut=10, - kgrid=[3, 1, 1], fft_size=[15, 1, 15], compute_full_χ0=false) +function test_chi0(testcase; symmetries=false, temperature=0, spin_polarization=:none, + eigensolver=lobpcg_hyper, Ecut=10, kgrid=[3, 1, 1], fft_size=[15, 1, 15], + compute_full_χ0=false, εF=nothing) tol = 1e-11 ε = 1e-6 testtol = 2e-6 - n_ep_extra = 3 - occupation_threshold = DFTK.default_occupation_threshold() - collinear = spin_polarization == :collinear - is_metal = !isnothing(testcase.temperature) + collinear = spin_polarization == :collinear + is_metal = !isnothing(testcase.temperature) + is_εF_fixed = !isnothing(εF) eigsol = eigensolver == lobpcg_hyper label = [ is_metal ? " metal" : "insulator", @@ -25,21 +24,22 @@ function test_chi0(testcase; symmetries=false, temperature=0, symmetries ? " symm" : "no symm", temperature > 0 ? "temp" : " 0K", collinear ? "coll" : "none", + is_εF_fixed ? " εF" : "none", ] @testset "Computing χ0 ($(join(label, ", ")))" begin - spec = ElementPsp(testcase.atnum, psp=load_psp(testcase.psp)) magnetic_moments = collinear ? [0.3, 0.7] : [] - model_kwargs = (; temperature, symmetries, magnetic_moments, spin_polarization) + model_kwargs = (; symmetries, magnetic_moments, spin_polarization, temperature, εF, + disable_electrostatics_check=true) basis_kwargs = (; kgrid, fft_size, Ecut) model = model_LDA(testcase.lattice, testcase.atoms, testcase.positions; model_kwargs...) basis = PlaneWaveBasis(model; basis_kwargs...) ρ0 = guess_density(basis, magnetic_moments) - energies, ham0 = energy_hamiltonian(basis, nothing, nothing; ρ=ρ0) - res = DFTK.next_density(ham0; tol, n_ep_extra, eigensolver, occupation_threshold) - occ, εF = DFTK.compute_occupation(basis, res.eigenvalues; occupation_threshold) - scfres = (ham=ham0, res..., n_ep_extra, occupation_threshold) + _, ham0 = energy_hamiltonian(basis, nothing, nothing; ρ=ρ0) + nbandsalg = is_εF_fixed ? FixedBands(; n_bands_converge=6) : AdaptiveBands(model) + res = DFTK.next_density(ham0, nbandsalg; tol, eigensolver) + scfres = (ham=ham0, res...) # create external small perturbation εδV n_spin = model.n_spin_components @@ -57,9 +57,8 @@ function test_chi0(testcase; symmetries=false, temperature=0, model = model_LDA(testcase.lattice, testcase.atoms, testcase.positions; model_kwargs..., extra_terms=[term_builder]) basis = PlaneWaveBasis(model; basis_kwargs...) - energies, ham = energy_hamiltonian(basis, nothing, nothing; ρ=ρ0) - res = DFTK.next_density(ham; tol, n_ep_extra, eigensolver, - occupation_threshold) + _, ham = energy_hamiltonian(basis, nothing, nothing; ρ=ρ0) + res = DFTK.next_density(ham, nbandsalg; tol, eigensolver) res.ρout end @@ -72,17 +71,28 @@ function test_chi0(testcase; symmetries=false, temperature=0, diff_applied_χ0 = apply_χ0(scfres, δV) @test norm(diff_findiff - diff_applied_χ0) < testtol + # Test apply_χ0 without extra bands + ψ_occ, occ_occ = DFTK.select_occupied_orbitals(basis, + scfres.ψ, + scfres.occupation; + threshold=scfres.occupation_threshold) + ε_occ = [scfres.eigenvalues[ik][1:size(ψk, 2)] for (ik, ψk) in enumerate(ψ_occ)] + + diff_applied_χ0_noextra = apply_χ0(scfres.ham, ψ_occ, occ_occ, scfres.εF, + ε_occ, δV; scfres.occupation_threshold) + @test norm(diff_applied_χ0_noextra - diff_applied_χ0) < testtol + # just to cover it here if temperature > 0 - D = compute_dos(εF, basis, res.eigenvalues) - LDOS = compute_ldos(εF, basis, res.eigenvalues, res.ψ) + D = compute_dos(res.εF, basis, res.eigenvalues) + LDOS = compute_ldos(res.εF, basis, res.eigenvalues, res.ψ) end if !symmetries # Test compute_χ0 against finite differences # (only works in reasonable time for small Ecut) if compute_full_χ0 - χ0 = compute_χ0(ham0; occupation_threshold) + χ0 = compute_χ0(ham0) diff_computed_χ0 = reshape(χ0 * vec(δV), basis.fft_size..., n_spin) @test norm(diff_findiff - diff_computed_χ0) < testtol end @@ -109,10 +119,10 @@ end end end - # additional test for compute_χ0 + # Additional test for compute_χ0 for spin_polarization in (:none, :collinear) - test_chi0(silicon; symmetries=false, spin_polarization, - eigensolver=diag_full, Ecut=3, fft_size=[10, 1, 10], - compute_full_χ0=true) + test_chi0(silicon; symmetries=false, spin_polarization, eigensolver=diag_full, + Ecut=3, fft_size=[10, 1, 10], compute_full_χ0=true) + test_chi0(magnesium; spin_polarization, temperature=0.01, εF=0.3) end end diff --git a/test/compute_bands.jl b/test/compute_bands.jl index 18d9138ae5..edf33a5e09 100644 --- a/test/compute_bands.jl +++ b/test/compute_bands.jl @@ -1,5 +1,6 @@ using Test using DFTK +import Brillouin: interpolate include("testcases.jl") @@ -23,7 +24,6 @@ if mpi_nprocs() == 1 # not easy to distribute [0.423076923077, 0.000000000000, 0.423076923077], [0.461538461538, 0.000000000000, 0.461538461538], [0.500000000000, 0.000000000000, 0.500000000000], - [0.500000000000, 0.000000000000, 0.500000000000], [0.531250000000, 0.062500000000, 0.531250000000], [0.562500000000, 0.125000000000, 0.562500000000], [0.593750000000, 0.187500000000, 0.593750000000], @@ -43,7 +43,6 @@ if mpi_nprocs() == 1 # not easy to distribute [0.053571428571, 0.053571428571, 0.107142857143], [0.026785714286, 0.026785714286, 0.053571428571], [0.000000000000, 0.000000000000, 0.000000000000], - [0.000000000000, 0.000000000000, 0.000000000000], [0.041666666667, 0.041666666667, 0.041666666667], [0.083333333333, 0.083333333333, 0.083333333333], [0.125000000000, 0.125000000000, 0.125000000000], @@ -56,7 +55,6 @@ if mpi_nprocs() == 1 # not easy to distribute [0.416666666667, 0.416666666667, 0.416666666667], [0.458333333333, 0.458333333333, 0.458333333333], [0.500000000000, 0.500000000000, 0.500000000000], - [0.500000000000, 0.500000000000, 0.500000000000], [0.500000000000, 0.472222222222, 0.527777777778], [0.500000000000, 0.444444444444, 0.555555555556], [0.500000000000, 0.416666666667, 0.583333333333], @@ -66,7 +64,6 @@ if mpi_nprocs() == 1 # not easy to distribute [0.500000000000, 0.305555555556, 0.694444444444], [0.500000000000, 0.277777777778, 0.722222222222], [0.500000000000, 0.250000000000, 0.750000000000], - [0.500000000000, 0.250000000000, 0.750000000000], [0.500000000000, 0.208333333333, 0.708333333333], [0.500000000000, 0.166666666667, 0.666666666667], [0.500000000000, 0.125000000000, 0.625000000000], @@ -76,41 +73,45 @@ if mpi_nprocs() == 1 # not easy to distribute ] ref_klabels = Dict( - "U"=>[0.625, 0.25, 0.625], - "W"=>[0.5, 0.25, 0.75], - "X"=>[0.5, 0.0, 0.5], - "Γ"=>[0.0, 0.0, 0.0], - "L"=>[0.5, 0.5, 0.5], - "K"=>[0.375, 0.375, 0.75] + :U => [0.625, 0.25, 0.625], + :W => [0.5, 0.25, 0.75], + :X => [0.5, 0.0, 0.5], + :Γ => [0.0, 0.0, 0.0], + :L => [0.5, 0.5, 0.5], + :K => [0.375, 0.375, 0.75] ) model = model_LDA(testcase.lattice, testcase.atoms, testcase.positions) - kcoords, klabels, kpath = high_symmetry_kpath(model; kline_density=22.7) - - @test length(ref_kcoords) == length(kcoords) - for ik in 1:length(ref_kcoords) - @test ref_kcoords[ik] ≈ kcoords[ik] atol=1e-11 - end + kpath = irrfbz_path(model) - @test length(klabels) == length(ref_klabels) + @test length(kpath.points) == length(ref_klabels) for key in keys(ref_klabels) - @test klabels[key] ≈ ref_klabels[key] atol=1e-15 + @test kpath.points[key] ≈ ref_klabels[key] atol=1e-15 end + @test kpath.paths[1] == [:Γ, :X, :U] + @test kpath.paths[2] == [:K, :Γ, :L, :W, :X] - @test kpath[1] == ["Γ", "X", "U"] - @test kpath[2] == ["K", "Γ", "L", "W", "X"] + # Interpolate the path and check + kinter = interpolate(kpath, density=22.7) + @test ref_kcoords ≈ kinter atol=1e-11 + @test length.(kinter.kpaths) == [18, 42] end @testset "High-symmetry kpath construction for 1D system" begin lattice = diagm([8.0, 0, 0]) - model = Model(lattice; n_electrons=1, terms=[Kinetic()]) - kcoords, klabels, kpath = high_symmetry_kpath(model; kline_density=20) - - @test length(kcoords) == 17 - @test kcoords[1] ≈ [-1/2, 0, 0] - @test kcoords[9] ≈ [ 0, 0, 0] - @test kcoords[17] ≈ [ 1/2, 0, 0] - @test length(kpath) == 1 + model = Model(lattice; terms=[Kinetic()]) + kpath = irrfbz_path(model) + + @test length(kpath.paths) == 1 + @test length(kpath.points) == 2 + @test kpath.paths == [[:Γ, :X]] + @test kpath.points[:Γ] == [0.0] + @test kpath.points[:X] == [0.5] + + kinter = interpolate(kpath, density=20) + @test length(kinter) == 8 + @test kinter[1] == [0.0] + @test kinter[8] == [0.5] end @testset "Compute bands for silicon" begin @@ -118,18 +119,18 @@ end Ecut = 7 n_bands = 8 - model = model_LDA(testcase.lattice, testcase.atoms, testcase.positions) - basis = PlaneWaveBasis(model, Ecut, testcase.kcoords, testcase.kweights) - - # Build Hamiltonian just from SAD guess - ρ0 = guess_density(basis) - ham = Hamiltonian(basis; ρ=ρ0) + model = model_LDA(testcase.lattice, testcase.atoms, testcase.positions) + kinter = interpolate(irrfbz_path(model), density=3) + kweights = ones(length(kinter)) ./ length(kinter) + basis = PlaneWaveBasis(model, Ecut, kinter, kweights) # Check that plain diagonalization and compute_bands agree - eigres = diagonalize_all_kblocks(lobpcg_hyper, ham, n_bands + 3, n_conv_check=n_bands, - tol=1e-5) + ρ = guess_density(basis) + ham = Hamiltonian(basis; ρ) + band_data = compute_bands(basis, kinter; ρ, n_bands) - band_data = compute_bands(basis, [k.coordinate for k in basis.kpoints]; ρ=ρ0, n_bands) + eigres = diagonalize_all_kblocks(lobpcg_hyper, ham, n_bands + 3, + n_conv_check=n_bands, tol=1e-5) for ik in 1:length(basis.kpoints) @test eigres.λ[ik][1:n_bands] ≈ band_data.λ[ik] atol=1e-5 end @@ -137,84 +138,74 @@ end @testset "prepare_band_data" begin testcase = silicon - model = model_LDA(testcase.lattice, testcase.atoms, testcase.positions) - - # k coordinates simulating two band branches, Γ => X => W and U => X - kcoords = [ - [0.000, 0.000, 0.000], - [0.250, 0.000, 0.250], - [0.500, 0.000, 0.500], - # - [0.500, 0.000, 0.500], - [0.500, 0.125, 0.625], - [0.500, 0.250, 0.750], - # - [0.625, 0.250, 0.625], - [0.575, 0.150, 0.575], - [0.500, 0.000, 0.500], - ] - kweights = ones(9) ./ 9 - basis = PlaneWaveBasis(model, 5, kcoords, kweights) - klabels = Dict("Γ" => [0, 0, 0], "X" => [0.5, 0.0, 0.5], - "W" => [0.5, 0.25, 0.75], "U" => [0.625, 0.25, 0.625]) + model = model_LDA(testcase.lattice, testcase.atoms, testcase.positions) + kpath = irrfbz_path(model) + kinter = interpolate(irrfbz_path(model), density=3) + kweights = ones(length(kinter)) ./ length(kinter) + basis = PlaneWaveBasis(model, 5, kinter, kweights) # Setup some dummy data - λ = [10ik .+ collect(1:4) for ik = 1:length(kcoords)] # Simulate 4 computed bands - λerror = [λ[ik]./100 for ik = 1:length(kcoords)] # ... and 4 errors - - ret = DFTK.prepare_band_data((basis=basis, λ=λ, λerror=λerror), klabels=klabels) + λ = [10ik .+ collect(1:4) for ik = 1:length(kinter)] + λerror = [λ[ik]./100 for ik = 1:length(kinter)] + band_data = (; basis, λ, λerror) + ret = DFTK.data_for_plotting(kinter, band_data) @test ret.n_spin == 1 - @test ret.n_kcoord == 9 + @test ret.n_kcoord == 8 @test ret.n_bands == 4 - @test ret.branches[1].kindices == [1, 2, 3] - @test ret.branches[2].kindices == [4, 5, 6] - @test ret.branches[3].kindices == [7, 8, 9] - - @test ret.branches[1].klabels == ("Γ", "X") - @test ret.branches[2].klabels == ("X", "W") - @test ret.branches[3].klabels == ("U", "X") - for iband in 1:4 - @test ret.branches[1].λ[:, iband, 1] == [10ik .+ iband for ik in 1:3] - @test ret.branches[2].λ[:, iband, 1] == [10ik .+ iband for ik in 4:6] - @test ret.branches[3].λ[:, iband, 1] == [10ik .+ iband for ik in 7:9] - - for ibr in 1:3 - @test ret.branches[ibr].λerror[:, iband, 1] == ret.branches[ibr].λ[:, iband, 1] ./ 100 - end + @test ret.λ[:, iband, 1] == [10ik .+ iband for ik in 1:8] + @test ret.λerror[:, iband, 1] == ret.λ[:, iband, 1] ./ 100 end B = model.recip_lattice - ref_kdist = zeros(3, 3) # row idx is k-point, col idx is branch, - ikpt = 1 - for ibr in 1:3 - ibr != 1 && (ref_kdist[1, ibr] = ref_kdist[end, ibr-1]) - ikpt += 1 - for ik in 2:3 - ref_kdist[ik, ibr] = ( - ref_kdist[ik-1, ibr] + norm(B * (kcoords[ikpt-1] - kcoords[ikpt])) - ) - ikpt += 1 + ref_kdist = [0.0] + for ik in 2:8 + if ik != 4 + push!(ref_kdist, ref_kdist[end] + norm(B * (kinter[ik-1] - kinter[ik]))) + else + # At ik = 6, the branch changes so kdistance does not increase. + push!(ref_kdist, ref_kdist[end]) end end - for ibr in 1:3 - @test ret.branches[ibr].kdistances == ref_kdist[:, ibr] - end - - @test ret.ticks.labels == ["Γ", "X", "W | U", "X"] - @test ret.ticks.distances == [0.0, ref_kdist[end, 1], ref_kdist[end, 2], ref_kdist[end, 3]] + @test ret.kdistances ≈ ref_kdist atol=1e-14 + @test ret.ticks.labels == ["Γ", "X", "U | K", "Γ", "L", "W", "X"] + @test ret.ticks.distances ≈ ref_kdist[[1, 2, 3, 5, 6, 7, 8]] atol=1e-14 + @test ret.kbranches == [1:3, 4:8] end @testset "is_metal" begin testcase = silicon model = model_LDA(testcase.lattice, testcase.atoms, testcase.positions) - basis = PlaneWaveBasis(model, 5, testcase.kcoords, testcase.kweights) λ = [[1, 2, 3, 4], [1, 1.5, 3.5, 4.2], [1, 1.1, 3.2, 4.3], [1, 2, 3.3, 4.1]] - @test !DFTK.is_metal((λ=λ, basis=basis), 2.5) - @test DFTK.is_metal((λ=λ, basis=basis), 3.2) + @test !DFTK.is_metal((; λ, basis), 2.5) + @test DFTK.is_metal((; λ, basis), 3.2) +end + +@testset "High-symmetry kpath for nonstandard lattice" begin + lattice_std = [0 1 1; 1 0 1; 1 1 0] .* 5.13 + model_std = model_LDA(lattice_std, silicon.atoms, silicon.positions) + + # Non-standard lattice parameters that describe the same system as model_standard. + lattice_nst = copy(lattice_std) + lattice_nst[:, 3] .+= lattice_nst[:, 1] .* 3 + position_nst = [[-2, 1, 1]/8, -[-2, 1, 1]/8] + model_nst = model_LDA(lattice_nst, silicon.atoms, position_nst) + + kpath_std = irrfbz_path(model_std) + kpath_nst = irrfbz_path(model_nst) + @test Set(keys(kpath_std.points)) == Set(keys(kpath_nst.points)) + @test kpath_std.paths == kpath_nst.paths + + # Check the k points are the same in Cartesian coordinates. + kinter_std = interpolate(kpath_std; density=20) + kinter_nst = interpolate(kpath_nst; density=20) + for (k_std, k_nst) in zip(kinter_std, kinter_nst) + @test( model_std.recip_lattice * k_std + ≈ model_nst.recip_lattice * k_nst) + end end end diff --git a/test/compute_density.jl b/test/compute_density.jl index ec12e49298..8bf86b8366 100644 --- a/test/compute_density.jl +++ b/test/compute_density.jl @@ -17,7 +17,6 @@ if mpi_nprocs() == 1 # not easy to distribute kwargs = (temperature=testcase.temperature, smearing=DFTK.Smearing.FermiDirac()) n_bands = div(testcase.n_electrons, 2, RoundUp) + 4 end - occupation_threshold = 1e-7 model = model_DFT(testcase.lattice, testcase.atoms, testcase.positions, :lda_xc_teter93; symmetries, kwargs...) @@ -25,14 +24,14 @@ if mpi_nprocs() == 1 # not easy to distribute ham = Hamiltonian(basis; ρ=guess_density(basis)) res = diagonalize_all_kblocks(lobpcg_hyper, ham, n_bands; tol) - occ, εF = DFTK.compute_occupation(basis, res.λ; occupation_threshold) + occ, εF = DFTK.compute_occupation(basis, res.λ) ρnew = compute_density(basis, res.X, occ) for it in 1:n_rounds ham = Hamiltonian(basis; ρ=ρnew) res = diagonalize_all_kblocks(lobpcg_hyper, ham, n_bands; tol=tol, ψguess=res.X) - occ, εF = DFTK.compute_occupation(basis, res.λ; occupation_threshold) + occ, εF = DFTK.compute_occupation(basis, res.λ) ρnew = compute_density(basis, res.X, occ) end diff --git a/test/compute_fft_size.jl b/test/compute_fft_size.jl index 195b0a8b45..6fc9797658 100644 --- a/test/compute_fft_size.jl +++ b/test/compute_fft_size.jl @@ -5,7 +5,7 @@ using DFTK include("testcases.jl") @testset "Test compute_fft_size on Silicon" begin - model = Model(silicon.lattice; n_electrons=1) + model = Model(silicon.lattice) @test compute_fft_size(model, 3, supersampling=2) == (15, 15, 15) @test compute_fft_size(model, 4, supersampling=2) == (15, 15, 15) @test compute_fft_size(model, 5, supersampling=2) == (18, 18, 18) @@ -17,7 +17,7 @@ end @testset "Test compute_fft_size on skewed lattice" begin lattice = Diagonal([1, 1e-12, 1e-12]) - model = Model(lattice; n_electrons=1) + model = Model(lattice) @test compute_fft_size(model, 15, supersampling=2) == ( 5, 1, 1) @test compute_fft_size(model, 300, supersampling=2) == (18, 1, 1) end diff --git a/test/diag_compare.jl b/test/diag_compare.jl index a67b1ab3ae..70877e1901 100644 --- a/test/diag_compare.jl +++ b/test/diag_compare.jl @@ -3,20 +3,20 @@ using DFTK @testset "Comparison of diagonalisaton procedures" begin function test_solver(reference, eigensolver, prec_type) - nev = length(reference.λ[1]) - println("Running $eigensolver with $prec_type ...") - res = diagonalize_all_kblocks(eigensolver, reference.ham, nev, prec_type=prec_type) - @test res.λ ≈ reference.λ + @testset "$eigensolver with $prec_type" begin + nev = length(reference.λ[1]) + res = diagonalize_all_kblocks(eigensolver, reference.ham, nev; prec_type) + @test res.λ ≈ reference.λ + end end - Ecut = 100 lattice = Float64[5 0 0; 0 0 0; 0 0 0] - model = Model(lattice; n_electrons=4, terms=[Kinetic()]) - basis = PlaneWaveBasis(model; Ecut, kgrid=(1, 1, 1)) + model = Model(lattice; terms=[Kinetic()]) + basis = PlaneWaveBasis(model; Ecut=100, kgrid=(1, 1, 1)) ham = Hamiltonian(basis) reference = merge(diagonalize_all_kblocks(diag_full, ham, 6), (ham=ham,)) - test_solver(reference, diag_full, PreconditionerTPA) - test_solver(reference, diag_full, PreconditionerNone) + test_solver(reference, diag_full, PreconditionerTPA) + test_solver(reference, diag_full, PreconditionerNone) test_solver(reference, lobpcg_hyper, PreconditionerTPA) end diff --git a/test/elements.jl b/test/elements.jl index 2a615c6b5a..bf5108c782 100644 --- a/test/elements.jl +++ b/test/elements.jl @@ -60,9 +60,9 @@ end @test atomic_symbol(element) == :Si @test charge_nuclear(element) == 14 - @test charge_ionic(element) == 2 - @test n_elec_valence(element) == 2 - @test n_elec_core(element) == 12 + @test charge_ionic(element) == 4 + @test n_elec_valence(element) == 4 + @test n_elec_core(element) == 10 @test local_potential_fourier(element, 0.0) == 0.0 q3 = sqrt(3) * 2π / element.lattice_constant diff --git a/test/energy_cutoff_smearing.jl b/test/energy_cutoff_smearing.jl index 11d285663b..58ecedc9f9 100644 --- a/test/energy_cutoff_smearing.jl +++ b/test/energy_cutoff_smearing.jl @@ -11,7 +11,7 @@ if mpi_nprocs() == 1 model = model_LDA(silicon.lattice, silicon.atoms, silicon.positions) basis = PlaneWaveBasis(model, 5, silicon.kcoords, silicon.kweights) - scfres = self_consistent_field(basis; n_bands=8, callback=info->nothing) + scfres = self_consistent_field(basis; callback=identity) # Kpath around one discontinuity of the first band of silicon (between X and U points) k_start = [0.5274, 0.0548, 0.5274] diff --git a/test/external/wannier90.jl b/test/external/wannier90.jl index 4335afadb8..eb2dd7afdd 100644 --- a/test/external/wannier90.jl +++ b/test/external/wannier90.jl @@ -7,7 +7,8 @@ if !Sys.iswindows() && mpi_nprocs() == 1 using wannier90_jll model = model_LDA(silicon.lattice, silicon.atoms, silicon.positions) basis = PlaneWaveBasis(model; Ecut=5, kgrid=[4, 4, 4], kshift=[1, 1, 1]/2) - scfres = self_consistent_field(basis, tol=1e-12, n_bands=12) + nbandsalg = AdaptiveBands(model; n_bands_converge=12) + scfres = self_consistent_field(basis; nbandsalg, tol=1e-12) fileprefix = "wannier90_outputs/Si" run_wannier90(scfres; fileprefix, diff --git a/test/forces.jl b/test/forces.jl index 1a3a4a47b2..94dfeb339a 100644 --- a/test/forces.jl +++ b/test/forces.jl @@ -44,13 +44,10 @@ end @testset "Forces on silicon with spin and temperature" begin function silicon_energy_forces(positions; smearing=Smearing.FermiDirac()) - model = model_DFT(silicon.lattice, silicon.atoms, positions, :lda_xc_teter93; - temperature=0.03, smearing, spin_polarization=:collinear) - basis = PlaneWaveBasis(model; Ecut=4, kgrid=[4, 1, 2], kshift=[1/2, 0, 0]) - - n_bands = 10 - is_converged = DFTK.ScfConvergenceDensity(5e-10) - scfres = self_consistent_field(basis; n_bands, is_converged) + model = model_DFT(silicon.lattice, silicon.atoms, positions, :lda_xc_teter93; + temperature=0.03, smearing, spin_polarization=:collinear) + basis = PlaneWaveBasis(model; Ecut=4, kgrid=[4, 1, 2], kshift=[1/2, 0, 0]) + scfres = self_consistent_field(basis; is_converged=DFTK.ScfConvergenceDensity(5e-10)) scfres.energies.total, compute_forces(scfres) end diff --git a/test/forwarddiff.jl b/test/forwarddiff.jl index 5b7db7e8c6..6209577410 100644 --- a/test/forwarddiff.jl +++ b/test/forwarddiff.jl @@ -72,10 +72,11 @@ end is_converged = DFTK.ScfConvergenceDensity(1e-10) scfres = self_consistent_field(basis; is_converged, mixing=KerkerMixing(), + nbandsalg=FixedBands(; n_bands_converge=10), damping=0.6, response=ResponseOptions(verbose=true)) ComponentArray( - eigenvalues=hcat([ev[1:end-3] for ev in scfres.eigenvalues]...), + eigenvalues=hcat([ev[1:10] for ev in scfres.eigenvalues]...), ρ=scfres.ρ, energies=collect(values(scfres.energies)), εF=scfres.εF, @@ -112,7 +113,6 @@ end (compute_force(ε) - compute_force(-ε)) / 2ε end derivative_fd = ForwardDiff.derivative(compute_force, 0.0) - @show derivative_ε derivative_fd @test norm(derivative_ε - derivative_fd) < 1e-4 end diff --git a/test/fourier_transforms.jl b/test/fourier_transforms.jl index c52bd23028..e0a037d711 100644 --- a/test/fourier_transforms.jl +++ b/test/fourier_transforms.jl @@ -4,7 +4,7 @@ using DFTK: PlaneWaveBasis, ifft!, fft!, ifft, fft include("testcases.jl") @testset "FFT and IFFT are an identity" begin - model = Model(silicon.lattice; silicon.n_electrons) + model = Model(silicon.lattice) pw = PlaneWaveBasis(model; Ecut=4.0, fft_size=(8, 8, 8)) @testset "Transformation on the cubic basis set" begin diff --git a/test/hamiltonian_consistency.jl b/test/hamiltonian_consistency.jl index 8c7845920f..869e549028 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, n_dim = 3 - count(iszero, eachcol(lattice)) Si = n_dim == 3 ? ElementPsp(14, psp=load_psp(silicon.psp)) : ElementCoulomb(:Si) atoms = [Si, Si] - model = Model(lattice, atoms, silicon.positions; n_electrons=silicon.n_electrons, + model = Model(lattice, atoms, silicon.positions; terms=[term], spin_polarization, symmetries=true) basis = PlaneWaveBasis(model; Ecut, kgrid, kshift) diff --git a/test/helium_all_electron.jl b/test/helium_all_electron.jl index 668203155f..838b4fd992 100644 --- a/test/helium_all_electron.jl +++ b/test/helium_all_electron.jl @@ -10,12 +10,11 @@ using LinearAlgebra model = model_DFT(lattice, atoms, positions, [], n_electrons=2) basis = PlaneWaveBasis(model; Ecut, kgrid=(1, 1, 1)) - is_converged = DFTK.ScfConvergenceDensity(tol) - scfres = self_consistent_field(basis, is_converged=is_converged) + scfres = self_consistent_field(basis; is_converged=DFTK.ScfConvergenceDensity(tol)) scfres.energies.total, DFTK.compute_forces(scfres) end E, forces = energy_forces(Ecut=5, tol=1e-10) - @test E ≈ -1.5869009433016852 - @test norm(forces) < 1e-9 + @test E ≈ -1.5869009433016852 atol=1e-12 + @test norm(forces) < 1e-8 end diff --git a/test/hessian.jl b/test/hessian.jl index ac03a68858..ccd668697c 100644 --- a/test/hessian.jl +++ b/test/hessian.jl @@ -1,7 +1,7 @@ using Test using DFTK import DFTK: solve_ΩplusK, apply_Ω, apply_K, solve_ΩplusK_split -import DFTK: filled_occupation, compute_projected_gradient, compute_occupation +import DFTK: compute_projected_gradient import DFTK: select_occupied_orbitals include("testcases.jl") @@ -79,14 +79,14 @@ include("testcases.jl") end - @testset "ΩplusK_split, temp" begin + @testset "ΩplusK_split, temperature" begin Ecut = 5 fft_size = [9, 9, 9] model = model_DFT(magnesium.lattice, magnesium.atoms, magnesium.positions, [:lda_xc_teter93]; temperature=magnesium.temperature) basis = PlaneWaveBasis(model, Ecut, magnesium.kcoords, magnesium.kweights; fft_size) - scfres = self_consistent_field(basis; n_bands=7, tol=1e-12, - occupation_threshold=1e-10) + nbandsalg = AdaptiveBands(basis; occupation_threshold=1e-10) + scfres = self_consistent_field(basis; tol=1e-12, nbandsalg) ψ = scfres.ψ rhs = compute_projected_gradient(basis, scfres.ψ, scfres.occupation) diff --git a/test/interval_arithmetic.jl b/test/interval_arithmetic.jl index 5584335297..7cba5785ed 100644 --- a/test/interval_arithmetic.jl +++ b/test/interval_arithmetic.jl @@ -45,14 +45,12 @@ end model = model_LDA(Matrix{Interval{Float64}}(testcase.lattice), testcase.atoms, testcase.positions) - basis = PlaneWaveBasis(model; Ecut=10, kgrid=(1, 1, 1)) + basis = PlaneWaveBasis(model; Ecut=10, kgrid=(2, 1, 1)) eigenvalues = [[-0.17268859, 0.26999098, 0.2699912, 0.2699914, 0.35897297, 0.3589743], [-0.08567941, 0.00889772, 0.2246137, 0.2246138, 0.31941655, 0.3870046]] - occupations, εF = DFTK.compute_occupation(basis, Vector{Interval{Float64}}.(eigenvalues); - occupation_threshold=1e-7) + occupations, εF = DFTK.compute_occupation(basis, Vector{Interval{Float64}}.(eigenvalues)) - - @test mid.(εF) ≈ 0.2246137 atol=1e-6 - @test mid.(sum(sum, occupations)) ≈ 8.0 + @test mid(εF) ≈ (eigenvalues[1][4] + eigenvalues[2][5]) / 2 atol=1e-6 + @test mid(sum(DFTK.weighted_ksum(basis, occupations))) ≈ 8.0 end diff --git a/test/iron_lda.jl b/test/iron_lda.jl index 68c778854a..be828e9e6c 100644 --- a/test/iron_lda.jl +++ b/test/iron_lda.jl @@ -43,5 +43,5 @@ function run_iron_lda(T; kwargs...) end @testset "Iron LDA (Float64)" begin - run_iron_lda(Float64, test_tol=5e-6, scf_tol=1e-10) + run_iron_lda(Float64, test_tol=5e-6, scf_tol=1e-11) end diff --git a/test/lobpcg.jl b/test/lobpcg.jl index 90cfe1b8d6..aadbd1603f 100644 --- a/test/lobpcg.jl +++ b/test/lobpcg.jl @@ -122,3 +122,23 @@ end @test res1.λ[ik] ≈ res2.λ[ik] atol=1e-6 end end + +@testset "LOBPCG Internal data structures" begin + a1 = rand(10, 5) + a2 = rand(10, 2) + a3 = rand(10, 7) + b1 = rand(10, 6) + b2 = rand(10, 2) + A = hcat(a1,a2,a3) + B = hcat(b1,b2) + Ablock = DFTK.LazyHcat(a1, a2, a3) + Bblock = DFTK.LazyHcat(b1, b2) + @test Ablock'*Bblock ≈ A'*B + @test Ablock'*B ≈ A'*B + + C = rand(14, 4) + @test Ablock*C ≈ A*C + + D = rand(10, 4) + @test mul!(D,Ablock, C, 1, 0) ≈ A*C +end diff --git a/test/occupation.jl b/test/occupation.jl index f6a7ac499b..3e12186d22 100644 --- a/test/occupation.jl +++ b/test/occupation.jl @@ -34,7 +34,6 @@ if mpi_nprocs() == 1 # can't be bothered to convert the tests Ecut = 5 n_bands = 10 fft_size = [15, 15, 15] - occupation_threshold = 1e-7 # Emulate an insulator ... prepare energy levels energies = [zeros(n_bands) for k in silicon.kcoords] @@ -49,9 +48,9 @@ if mpi_nprocs() == 1 # can't be bothered to convert the tests # Occupation for zero temperature model = Model(silicon.lattice, silicon.atoms, silicon.positions; temperature=0.0, - smearing=nothing, terms=[Kinetic()]) + terms=[Kinetic()]) basis = PlaneWaveBasis(model, Ecut, silicon.kcoords, silicon.kweights; fft_size) - occupation0, εF0 = DFTK.compute_occupation_bandgap(basis, energies) + occupation0, εF0 = DFTK.compute_occupation(basis, energies) @test εHOMO < εF0 < εLUMO @test DFTK.weighted_ksum(basis, sum.(occupation0)) ≈ model.n_electrons @@ -62,7 +61,7 @@ if mpi_nprocs() == 1 # can't be bothered to convert the tests temperature, smearing, terms=[Kinetic()]) basis = PlaneWaveBasis(model, Ecut, silicon.kcoords, silicon.kweights; fft_size) occs, _ = with_logger(NullLogger()) do - DFTK.compute_occupation(basis, energies; occupation_threshold) + DFTK.compute_occupation(basis, energies) end @test sum(basis.kweights .* sum.(occs)) ≈ model.n_electrons end @@ -73,7 +72,7 @@ if mpi_nprocs() == 1 # can't be bothered to convert the tests model = Model(silicon.lattice, silicon.atoms, silicon.positions; temperature, smearing, terms=[Kinetic()]) basis = PlaneWaveBasis(model, Ecut, silicon.kcoords, silicon.kweights; fft_size) - occupation, _ = DFTK.compute_occupation(basis, energies; occupation_threshold) + occupation, _ = DFTK.compute_occupation(basis, energies) for ik in 1:n_k @test all(isapprox.(occupation[ik], occupation0[ik], atol=1e-2)) @@ -88,7 +87,6 @@ if mpi_nprocs() == 1 # can't be bothered to convert the tests Ecut = 5 fft_size = [15, 15, 15] kgrid = [2, 3, 4] - occupation_threshold = 1e-7 # Emulate a metal ... energies = [[-0.08063210585291, 0.11227915155236, 0.13057816014162, 0.57672256037074], @@ -107,7 +105,6 @@ if mpi_nprocs() == 1 # can't be bothered to convert the tests symmetries = DFTK.symmetry_operations(testcase.lattice, testcase.atoms, testcase.positions) kcoords, _ = bzmesh_ir_wedge(kgrid, symmetries) - n_bands = length(energies[1]) n_k = length(kcoords) @assert n_k == length(energies) @@ -126,7 +123,7 @@ if mpi_nprocs() == 1 # can't be bothered to convert the tests temperature, smearing, terms=[Kinetic()]) basis = PlaneWaveBasis(model; Ecut, kgrid, fft_size, kshift=[1, 0, 1]/2) occupation, εF = with_logger(NullLogger()) do - DFTK.compute_occupation(basis, energies; occupation_threshold) + DFTK.compute_occupation(basis, energies) end @test DFTK.weighted_ksum(basis, sum.(occupation)) ≈ model.n_electrons @@ -134,3 +131,30 @@ if mpi_nprocs() == 1 # can't be bothered to convert the tests end end end + +if mpi_nprocs() == 1 # can't be bothered to convert the tests +@testset "Fixed Fermi level" begin + testcase = magnesium + + function run_scf(; kwargs...) + atoms = fill(ElementGaussian(1.0, 0.5), length(testcase.positions)) + model = Model(testcase.lattice, atoms, testcase.positions; + temperature=0.01, disable_electrostatics_check=true, kwargs...) + basis = PlaneWaveBasis(model; Ecut=5, kgrid=(2, 2, 2)) + self_consistent_field(basis; nbandsalg=FixedBands(; n_bands_converge=8)) + end + scfres_ref = run_scf(; testcase.n_electrons) + εF_ref = scfres_ref.εF + n_electrons_ref = scfres_ref.basis.model.n_electrons + @test n_electrons_ref == testcase.n_electrons + + δεF = εF_ref / 4 + for εF in [εF_ref - δεF, εF_ref + δεF] + scfres = run_scf(; εF) + @test εF ≈ scfres.εF + n_electrons = DFTK.weighted_ksum(scfres.basis, sum.(scfres.occupation)) + εF > εF_ref && @test n_electrons > n_electrons_ref + εF < εF_ref && @test n_electrons < n_electrons_ref + end +end +end diff --git a/test/phonons.jl b/test/phonons.jl index b60a66500c..8f2910922f 100644 --- a/test/phonons.jl +++ b/test/phonons.jl @@ -4,7 +4,9 @@ using DFTK using LinearAlgebra using ForwardDiff using StaticArrays +using Random +if mpi_nprocs() == 1 # can't be bothered to convert the tests @testset "Phonons" begin # Convert back and forth between Vec3 and columnwise matrix @@ -29,6 +31,18 @@ function prepare_system(; n_scell=1) (; positions, lattice, directions, params, V) end +function prepare_3d_system(; n_scell=1) + positions = [[0.0, 0.0, 0.0]] + for i in 1:n_scell-1 + push!(positions, i * ones(3) / n_scell) + end + + a = 5. * length(positions) + lattice = a * rand(3, 3) + + (; positions, lattice) +end + # Compute phonons for a one-dimensional pairwise potential for a set of `q = 0` using # supercell method function test_supercell_q0(; n_scell=1, max_radius=1e3) @@ -83,6 +97,19 @@ function test_ph_disp(; n_scell=1, max_radius=1e3, n_points=2) ph_bands end +""" +Real-space equivalent of `transfer_blochwave_kpt`. +""" +function transfer_blochwave_kpt_real(ψk_in, basis::PlaneWaveBasis, kpt_in, kpt_out, ΔG) + ψk_out = zeros(eltype(ψk_in), length(kpt_out.G_vectors), size(ψk_in, 2)) + exp_ΔGr = DFTK.cis2pi.(-dot.(Ref(ΔG), r_vectors(basis))) + for n in 1:size(ψk_in, 2) + ψk_out[:, n] = fft(basis, kpt_out, exp_ΔGr .* ifft(basis, kpt_in, ψk_in[:, n])) + end + ψk_out +end + + @testset "Phonon consistency" begin max_radius = 1e3 tolerance = 1e-6 @@ -105,4 +132,41 @@ end end end +@testset "Test shifting function" begin + Random.seed!() + tolerance = 1e-12 + + case = prepare_3d_system() + + X = ElementGaussian(1.0, 0.5, :X) + atoms = [X for _ in case.positions] + n_atoms = length(case.positions) + + model = Model(case.lattice, atoms, case.positions; n_electrons=n_atoms, + symmetries=false, spin_polarization=:spinless) + kgrid = rand(2:20, 3) + k1, k2, k3 = kgrid + basis = PlaneWaveBasis(model; Ecut=100, kgrid=kgrid) + + # We consider a smooth periodic function with Fourier coefficients given if the basis + # e^(iG·x) + ψ = rand(ComplexF64, size(r_vectors(basis))) + + # Random `q` shift + q0 = rand(basis.kpoints).coordinate + ishift = [rand(-k1*2:k1*2), rand(-k2*2:k2*2), rand(-k3*2:k3*2)] + q = q0 .* ishift + for kpt in unique(rand(basis.kpoints, 4)) + ψk = fft(basis, kpt, ψ) + + ψk_out_four = DFTK.multiply_by_expiqr(basis, kpt, q, ψk) + ψk_out_real = DFTK.multiply_by_expiqr(basis, kpt, q, ψk; + transfer_fn=transfer_blochwave_kpt_real) + @testset "Testing kpoint $(kpt.coordinate) on kgrid $kgrid" begin + @test norm(ψk_out_four - ψk_out_real) < tolerance + end + end +end + +end end diff --git a/test/random_spindensity.jl b/test/random_spindensity.jl index 982dae3a91..53aa1afa7c 100644 --- a/test/random_spindensity.jl +++ b/test/random_spindensity.jl @@ -16,7 +16,8 @@ include("testcases.jl") ρspin = nothing end ρ = ρ_from_total_and_spin(ρtot, ρspin) - self_consistent_field(basis, tol=5e-6, ρ=ρ, n_bands=10); + self_consistent_field(basis; tol=5e-6, ρ, + nbandsalg=FixedBands(; n_bands_converge=10)); end scfres = run_silicon(:none) diff --git a/test/run_scf_and_compare.jl b/test/run_scf_and_compare.jl index 0f5387a22d..43db058d89 100644 --- a/test/run_scf_and_compare.jl +++ b/test/run_scf_and_compare.jl @@ -4,11 +4,12 @@ import DFTK: mpi_sum function run_scf_and_compare(T, basis, ref_evals, ref_etot; n_ignored=0, test_tol=1e-6, scf_tol=1e-6, test_etot=true, kwargs...) - n_kpt = length(ref_evals) - n_bands = length(ref_evals[1]) + n_kpt = length(ref_evals) + n_bands = length(ref_evals[1]) kpt_done = zeros(Bool, n_kpt) - scfres = self_consistent_field(basis; tol=scf_tol, n_bands=n_bands, kwargs...) + nbandsalg = AdaptiveBands(basis.model, n_bands_converge=n_bands) + scfres = self_consistent_field(basis; tol=scf_tol, nbandsalg, kwargs...) for (ik, ik_global) in enumerate(basis.krange_thisproc) @test eltype(scfres.eigenvalues[ik]) == T @test eltype(scfres.ψ[ik]) == Complex{T} diff --git a/test/runtests.jl b/test/runtests.jl index 79537ca693..091ef45e3e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -108,6 +108,7 @@ Random.seed!(0) include("variational.jl") include("compute_bands.jl") include("random_spindensity.jl") + include("cg.jl") include("chi0.jl") include("kernel.jl") include("serialisation.jl") diff --git a/test/scf_compare.jl b/test/scf_compare.jl index 7bb1735a20..5d99eb430d 100644 --- a/test/scf_compare.jl +++ b/test/scf_compare.jl @@ -6,7 +6,6 @@ include("testcases.jl") @testset "Compare different SCF algorithms (no spin, no temperature)" begin Ecut = 3 - n_bands = 6 fft_size = [9, 9, 9] tol = 1e-7 @@ -15,13 +14,13 @@ include("testcases.jl") # Run nlsolve without guess ρ0 = zeros(basis.fft_size..., 1) - ρ_nl = self_consistent_field(basis; ρ=ρ0, tol=tol).ρ + ρ_nl = self_consistent_field(basis; ρ=ρ0, tol).ρ # Run DM if mpi_nprocs() == 1 # Distributed implementation not yet available @testset "Direct minimization" begin - ρ_dm = direct_minimization(basis; g_tol=tol).ρ - @test maximum(abs.(ρ_dm - ρ_nl)) < sqrt(tol) / 10 + ρ_dm = direct_minimization(basis; tol).ρ + @test maximum(abs, ρ_dm - ρ_nl) < sqrt(tol) / 10 end end @@ -42,7 +41,7 @@ include("testcases.jl") for solver in (scf_nlsolve_solver(), scf_damping_solver(1.2), scf_anderson_solver(), scf_CROP_solver()) @testset "Testing $solver" begin - ρ_alg = self_consistent_field(basis; ρ=ρ0, solver=solver, tol=tol).ρ + ρ_alg = self_consistent_field(basis; ρ=ρ0, solver, tol).ρ @test maximum(abs.(ρ_alg - ρ_nl)) < sqrt(tol) / 10 end end @@ -68,7 +67,6 @@ end @testset "Compare different SCF algorithms (collinear spin, no temperature)" begin Ecut = 3 - n_bands = 6 fft_size = [9, 9, 9] tol = 1e-7 @@ -100,7 +98,6 @@ end @testset "Compare different SCF algorithms (no spin, temperature)" begin Ecut = 3 - n_bands = 6 fft_size = [9, 9, 9] tol = 1e-7 @@ -123,7 +120,6 @@ end @testset "Compare different SCF algorithms (collinear spin, temperature)" begin - n_bands = 8 fft_size = [13, 13, 13] tol = 1e-7 diff --git a/test/silicon_lda.jl b/test/silicon_lda.jl index 8d03b0e773..84d1c18eba 100644 --- a/test/silicon_lda.jl +++ b/test/silicon_lda.jl @@ -37,30 +37,28 @@ end @testset "Silicon LDA (small, Float64)" begin - run_silicon_lda(Float64, Ecut=7, test_tol=0.03, n_ignored=0, grid_size=17, scf_tol=1e-5, - n_ep_extra=0) + run_silicon_lda(Float64, Ecut=7, test_tol=0.03, n_ignored=0, grid_size=17, scf_tol=1e-5) end if !isdefined(Main, :FAST_TESTS) || !FAST_TESTS @testset "Silicon LDA (large, Float64)" begin - run_silicon_lda(Float64, Ecut=25, test_tol=1e-5, n_ignored=0, - grid_size=33, scf_tol=1e-7, n_ep_extra=0) + run_silicon_lda(Float64, Ecut=25, test_tol=1e-5, n_ignored=0, grid_size=33, + scf_tol=1e-7) end end @testset "Silicon LDA (small, Float32)" begin - run_silicon_lda(Float32, Ecut=7, test_tol=0.03, n_ignored=1, grid_size=19, scf_tol=1e-4, - n_ep_extra=1) + run_silicon_lda(Float32, Ecut=7, test_tol=0.03, n_ignored=1, grid_size=19, scf_tol=1e-4) end @testset "Silicon LDA (small, collinear spin)" begin run_silicon_lda(Float64, Ecut=7, test_tol=0.03, n_ignored=0, grid_size=17, - scf_tol=1e-5, n_ep_extra=0, spin_polarization=:collinear) + scf_tol=1e-5, spin_polarization=:collinear) end if !isdefined(Main, :FAST_TESTS) || !FAST_TESTS @testset "Silicon LDA (large, collinear spin)" begin run_silicon_lda(Float64, Ecut=25, test_tol=1e-5, n_ignored=0, - grid_size=33, scf_tol=1e-7, n_ep_extra=0, spin_polarization=:collinear) + grid_size=33, scf_tol=1e-7, spin_polarization=:collinear) end end diff --git a/test/supercell.jl b/test/supercell.jl index 89ac7f4f76..c12d7a28f2 100644 --- a/test/supercell.jl +++ b/test/supercell.jl @@ -2,8 +2,13 @@ using Test using DFTK include("testcases.jl") +if mpi_nprocs() == 1 # can't be bothered to convert the tests + @testset "Compare scf results in unit cell and supercell" begin - Ecut = 4; kgrid = [3,3,3]; tol=1e-12; kshift=zeros(3); + Ecut = 4 + kgrid = [3, 3, 3] + kshift = zeros(3) + tol = 1e-12 scf_tol = (; is_converged=DFTK.ScfConvergenceDensity(tol)) # Parameters Si = ElementPsp(silicon.atnum, psp=load_psp(silicon.psp)) @@ -16,12 +21,14 @@ include("testcases.jl") scfres_supercell = cell_to_supercell(scfres) # Compare energies - @test norm(scfres.energies.total*prod(kgrid) - + @test norm(scfres.energies.total * prod(kgrid) - scfres_supercell_manual.energies.total) < 1e-8 - @test scfres.energies.total*prod(kgrid) ≈ scfres_supercell.energies.total + @test scfres.energies.total * prod(kgrid) ≈ scfres_supercell.energies.total # Compare densities ρ_ref = DFTK.interpolate_density(dropdims(scfres.ρ, dims=4), basis, basis_supercell) @test norm(ρ_ref .- scfres_supercell.ρ) < 10*tol @test norm(ρ_ref .- scfres_supercell_manual.ρ) < 10*tol end + +end diff --git a/test/variational.jl b/test/variational.jl index a6b2826456..062272c7b3 100644 --- a/test/variational.jl +++ b/test/variational.jl @@ -8,7 +8,6 @@ function get_scf_energies(testcase, supersampling, functionals) Ecut=3 grid_size=15 scf_tol=1e-12 # Tolerance in total enengy - n_bands = 10 kcoords = [[.2, .3, .4]] # force symmetries to false because the symmetrization is weird at low ecuts