-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lattice Optimization #8
base: main
Are you sure you want to change the base?
Changes from 15 commits
9dd9801
c2b81f0
f699d36
36ad6c1
b72bf35
dacdeaf
572598d
4e41c60
4517e6b
e113328
442f505
3c3a844
24b2771
1f5cf85
a724e00
1a3d9c3
2079a6e
0947b2b
551ee5a
205b149
a0312d3
06679ee
e3a8b02
b2d33fe
fa82d3c
39c54d4
6d5edcf
6697e22
3370277
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
using DFTK: occupation | ||
#= Test Geometry Optimization on an aluminium supercell. | ||
=# | ||
using LinearAlgebra | ||
using DFTK | ||
using ASEconvert | ||
using LazyArtifacts | ||
using AtomsCalculators | ||
using Unitful | ||
using UnitfulAtomic | ||
using Random | ||
using OptimizationOptimJL | ||
|
||
using GeometryOptimization | ||
|
||
|
||
function build_al_supercell(rep=1) | ||
pseudodojo_psp = artifact"pd_nc_sr_lda_standard_0.4.1_upf/Al.upf" | ||
a = 7.65339 # true lattice constant. | ||
lattice = a * Matrix(I, 3, 3) | ||
Al = ElementPsp(:Al; psp=load_psp(pseudodojo_psp)) | ||
atoms = [Al, Al, Al, Al] | ||
positions = [[0.0, 0.0, 0.0], [0.0, 0.5, 0.5], [0.5, 0.0, 0.5], [0.5, 0.5, 0.0]] | ||
unit_cell = periodic_system(lattice, atoms, positions) | ||
|
||
# Make supercell in ASE: | ||
# We convert our lattice to the conventions used in ASE, make the supercell | ||
# and then convert back ... | ||
supercell_ase = convert_ase(unit_cell) * pytuple((rep, 1, 1)) | ||
supercell = pyconvert(AbstractSystem, supercell_ase) | ||
|
||
# Unfortunately right now the conversion to ASE drops the pseudopotential information, | ||
# so we need to reattach it: | ||
supercell = attach_psp(supercell; Al=pseudodojo_psp) | ||
return supercell | ||
end; | ||
|
||
al_supercell = build_al_supercell(1) | ||
|
||
# Create a simple calculator for the model. | ||
model_kwargs = (; functionals = [:lda_x, :lda_c_pw], temperature = 1e-4) | ||
basis_kwargs = (; kgrid = [6, 6, 6], Ecut = 30.0) | ||
scf_kwargs = (; tol = 1e-6) | ||
calculator = DFTKCalculator(; model_kwargs, basis_kwargs, scf_kwargs, verbose=true) | ||
|
||
energy_true = AtomsCalculators.potential_energy(al_supercell, calculator) | ||
|
||
# Starting position is a random perturbation of the equilibrium one. | ||
Random.seed!(1234) | ||
x0 = vcat(position(al_supercell)...) | ||
σ = 0.5u"angstrom"; x0_pert = x0 + σ * rand(Float64, size(x0)) | ||
al_supercell = update_not_clamped_positions(al_supercell, x0_pert) | ||
energy_pert = AtomsCalculators.potential_energy(al_supercell, calculator) | ||
|
||
println("Initial guess distance (norm) from true parameters $(norm(x0 - x0_pert)).") | ||
println("Initial regret $(energy_pert - energy_true).") | ||
|
||
optim_options = (f_tol=1e-6, iterations=6, show_trace=true) | ||
|
||
results = minimize_energy!(al_supercell, calculator; optim_options...) | ||
println(results) | ||
|
||
""" Returns the index of the highest occupied band. | ||
`atol` specifies the (fractional) occupations below which | ||
a band is considered unoccupied. | ||
""" | ||
function valence_band_index(occupations; atol=1e-36) | ||
filter = x -> isapprox(x, 0.; atol) | ||
maximum(maximum.(findall.(!filter, occupations))) | ||
end | ||
|
||
function band_gaps(scfres) | ||
vi = valence_band_index(occupations; atol) | ||
# If the system is metallic, by convenction band gap is zero. | ||
if DFTK.is_metal(scfres.eigenvalues, scfres.εF; tol=1e-4) | ||
return (; εMax_valence=0.0u"hartree", εMin_conduction=0.0u"hartree", | ||
direct_bandgap=0.0u"hartree", valence_band_index=vi) | ||
else | ||
εMax_valence = maximum([εk[vi] for εk in scfres.eigenvalues]) * u"hartree" | ||
εMin_conduction = minimum([εk[vi + 1] for εk in scfres.eigenvalues]) * u"hartree" | ||
direct_bandgap = minimum([εk[vi + 1] - εk[vi] for εk in scfres.eigenvalues]) * u"hartree" | ||
return (; εMax_valence, εMin_conduction, direct_bandgap, valence_band_index=vi) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
using LinearAlgebra | ||
using DFTK | ||
using ASEconvert | ||
using LazyArtifacts | ||
using AtomsBase | ||
using AtomsCalculators | ||
using Unitful | ||
using UnitfulAtomic | ||
using Random | ||
using OptimizationOptimJL | ||
using ComponentArrays | ||
|
||
using GeometryOptimization | ||
|
||
|
||
function build_al_supercell(rep=1) | ||
pseudodojo_psp = artifact"pd_nc_sr_lda_standard_0.4.1_upf/Al.upf" | ||
a = 7.65339 # true lattice constant. | ||
lattice = a * Matrix(I, 3, 3) | ||
Al = ElementPsp(:Al; psp=load_psp(pseudodojo_psp)) | ||
atoms = [Al, Al, Al, Al] | ||
positions = [[0.0, 0.0, 0.0], [0.0, 0.5, 0.5], [0.5, 0.0, 0.5], [0.5, 0.5, 0.0]] | ||
unit_cell = periodic_system(lattice, atoms, positions) | ||
|
||
# Make supercell in ASE: | ||
# We convert our lattice to the conventions used in ASE, make the supercell | ||
# and then convert back ... | ||
supercell_ase = convert_ase(unit_cell) * pytuple((rep, 1, 1)) | ||
supercell = pyconvert(AbstractSystem, supercell_ase) | ||
|
||
# Unfortunately right now the conversion to ASE drops the pseudopotential information, | ||
# so we need to reattach it: | ||
supercell = attach_psp(supercell; Al=pseudodojo_psp) | ||
return supercell | ||
end; | ||
|
||
al_supercell = build_al_supercell(1) | ||
|
||
# Create a simple calculator for the model. | ||
model_kwargs = (; functionals = [:lda_x, :lda_c_pw], temperature = 1e-2) | ||
basis_kwargs = (; kgrid = [2, 2, 2], Ecut = 10.0) | ||
scf_kwargs = (; tol = 1e-3) | ||
calculator = DFTKCalculator(; model_kwargs, basis_kwargs, scf_kwargs, verbose=true) | ||
|
||
optim_options = (f_tol=1e-6, iterations=6, show_trace=true) | ||
results = minimize_energy!(al_supercell, calculator; procedure="vc_relax", optim_options...) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# Verify correctness of lattice optimization on silicon. | ||
using DFTK | ||
using AtomsBase | ||
using AtomsCalculators | ||
using ComponentArrays | ||
using LazyArtifacts | ||
using Random | ||
using Unitful | ||
using UnitfulAtomic | ||
using OptimizationOptimJL | ||
using GeometryOptimization | ||
|
||
|
||
lattice = [0.0 5.131570667152971 5.131570667152971; | ||
5.131570667152971 0.0 5.131570667152971; | ||
5.131570667152971 5.131570667152971 0.0] | ||
hgh_lda_family = artifact"hgh_lda_hgh" | ||
psp_hgh = joinpath(hgh_lda_family, "si-q4.hgh") | ||
|
||
positions = [ones(3)/8, -ones(3)/8] | ||
atoms = fill(ElementPsp(:Si; psp=load_psp(psp_hgh)), 2) | ||
system = periodic_system(lattice, atoms, positions) | ||
|
||
# Create a simple calculator for the model. | ||
model_kwargs = (; functionals = [:lda_x, :lda_c_pw], temperature = 1e-5) | ||
basis_kwargs = (; kgrid = [5, 5, 5], Ecut = 30.0) | ||
scf_kwargs = (; tol = 1e-6) | ||
calculator = DFTKCalculator(; model_kwargs, basis_kwargs, scf_kwargs, verbose=true) | ||
|
||
# Perturb unit cell. | ||
Random.seed!(1234) | ||
σ = 0.2u"bohr" | ||
bounding_box_pert = [v + σ * rand(Float64, size(v)) for v in bounding_box(system)] | ||
system_pert = update_positions(system, position(system); bounding_box=bounding_box_pert) | ||
|
||
|
||
using LineSearches | ||
linesearch = BackTracking(c_1= 1e-4, ρ_hi= 0.8, ρ_lo= 0.1, order=2, maxstep=Inf) | ||
solver = OptimizationOptimJL.LBFGS(; linesearch) | ||
optim_options = (; solver, f_tol=1e-10, g_tol=1e-5, iterations=30, | ||
show_trace=true, store_trace = true, allow_f_increases=true) | ||
|
||
results = minimize_energy!(system_pert, calculator; procedure="vc_relax", optim_options...) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,26 +2,38 @@ | |
# Note that by default all particles in the system are assumed optimizable. | ||
# IMPORTANT: Note that we always work in cartesian coordinates. | ||
=# | ||
|
||
using DFTK | ||
export minimize_energy! | ||
|
||
|
||
function update_state(original_system, new_system, state) | ||
if bounding_box(original_system) != bounding_box(new_system) | ||
return DFTK.DFTKState() | ||
else | ||
return state | ||
end | ||
end | ||
|
||
""" | ||
By default we work in cartesian coordinaes. | ||
By default we work in cartesian coordinates. | ||
Note that internally, when optimizing the cartesian positions, atomic units | ||
are used. | ||
""" | ||
function Optimization.OptimizationFunction(system, calculator; kwargs...) | ||
function Optimization.OptimizationFunction(system, calculator; pressure=0.0, kwargs...) | ||
mask = not_clamped_mask(system) # mask is assumed not to change during optim. | ||
|
||
f = function(x::AbstractVector{<:Real}, p) | ||
# TODO: Note that this function will dispatch appropriately when called with | ||
# a Component vector. | ||
f = function(x, p) | ||
new_system = update_not_clamped_positions(system, x * u"bohr") | ||
energy = AtomsCalculators.potential_energy(new_system, calculator; kwargs...) | ||
state = update_state(system, new_system, calculator.state) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need a function to extract the state from the calcuator. Not all calculators may have state in which case the returned state would just be a dummy. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also I think I would use a marker struct on the |
||
energy = AtomsCalculators.potential_energy(new_system, calculator; state, kwargs...) | ||
austrip(energy) | ||
end | ||
|
||
g! = function(G::AbstractVector{<:Real}, x::AbstractVector{<:Real}, p) | ||
function g!(G, x, p) | ||
new_system = update_not_clamped_positions(system, x * u"bohr") | ||
# TODO: Determine if here we need a call to update_state. | ||
energy = AtomsCalculators.potential_energy(new_system, calculator; kwargs...) | ||
|
||
forces = AtomsCalculators.forces(new_system, calculator; kwargs...) | ||
|
@@ -31,13 +43,34 @@ function Optimization.OptimizationFunction(system, calculator; kwargs...) | |
# NOTE: minus sign since forces are opposite to gradient. | ||
G .= - austrip.(forces_concat) | ||
end | ||
function g!(G::ComponentVector, x::ComponentVector, p) | ||
deformation_tensor = I + voigt_to_full(austrip.(x.strain)) | ||
new_system = update_not_clamped_positions(system, x * u"bohr") | ||
|
||
state = update_state(system, new_system, calculator.state) | ||
forces = AtomsCalculators.forces(new_system, calculator; state, kwargs...) | ||
# Translate the forces vectors on each particle to a single gradient for the optimization parameter. | ||
forces_concat = collect(Iterators.flatten([deformation_tensor * f for f in forces[mask]])) | ||
|
||
# NOTE: minus sign since forces are opposite to gradient. | ||
G.atoms .= - austrip.(forces_concat) | ||
virial = AtomsCalculators.virial(new_system, calculator) | ||
G.strain .= - full_to_voigt(virial / deformation_tensor) | ||
end | ||
OptimizationFunction(f; grad=g!) | ||
end | ||
|
||
function minimize_energy!(system, calculator; solver=Optim.LBFGS(), kwargs...) | ||
function minimize_energy!(system, calculator; pressure=0.0, procedure="relax", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't do this On a practical level this means you should use a marker struct. By the way if you use marker structs, then you can also use these to automatically adapt your definition of |
||
solver=Optim.LBFGS(), kwargs...) | ||
# Use current system parameters as starting positions. | ||
x0 = austrip.(not_clamped_positions(system)) # Optim modifies x0 in-place, so need a mutable type. | ||
f_opt = OptimizationFunction(system, calculator) | ||
if procedure == "relax" | ||
x0 = austrip.(not_clamped_positions(system)) | ||
elseif procedure == "vc_relax" | ||
x0 = ComponentVector(atoms = austrip.(reduce(vcat, position(system))), strain = zeros(6)) | ||
else | ||
print("Error: unknown optimization procedure. Please use one of [`relax`, `vc_relax`].") | ||
end | ||
f_opt = OptimizationFunction(system, calculator; pressure) | ||
problem = OptimizationProblem(f_opt, x0, nothing) # Last argument needed in Optimization.jl. | ||
solve(problem, solver; kwargs...) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there DFTK-specific stuff in here, that should not be the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see you did this because there is no generic way to do this right now. This is a flaw in AtomsCalculators and something we should change.
My suggestion for what to do would be the following. In AtomsCalculators you introduce a
calculator_state(calculator)
function, which returns a state object and a methodupdate_state(algorithm, state, original_system, new_system)
, which does some magic to update the state. We supply two fallbacksin AtomsCalculators. In this way there is zero additional implementation cost for people who don't want to by into this, but for DFT methods it helps.
In DFTK we then add the code you put here with
and additionally
The rest I guess is clear: In here you just use
calculator_state
instead of accessingcalc.state
directly to make sure the dummy stuff works as well.Note that I added additionally an
algorithm
argument, which you should expose tominimize_energy!
. In this way the interpolation algorithm can be easily swapped.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Added PR to DFTK and AtomsCalculators implementing the changes.