Skip to content

Commit c69de01

Browse files
authored
Merge pull request #1 from CedricTravelletti/main
Basic implementation of geometry optimization interface.
2 parents fef59b5 + 4f03340 commit c69de01

File tree

12 files changed

+321
-9
lines changed

12 files changed

+321
-9
lines changed

.github/workflows/CI.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ jobs:
1919
fail-fast: false
2020
matrix:
2121
version:
22-
- '1.0'
2322
- '1.9'
2423
- 'nightly'
2524
os:

Project.toml

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,36 @@ uuid = "673bf261-a53d-43b9-876f-d3c1fc8329c2"
33
authors = ["JuliaMolSim community"]
44
version = "0.0.1"
55

6+
[deps]
7+
AtomsBase = "a963bdd2-2df7-4f54-a1ee-49d51e6be12a"
8+
AtomsCalculators = "a3e0e189-c65a-42c1-833c-339540406eb1"
9+
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
10+
OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e"
11+
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
12+
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
13+
UnitfulAtomic = "a7773ee8-282e-5fa2-be4e-bd808c38a91a"
14+
615
[compat]
7-
julia = "1"
16+
AtomsBase = "0.3"
17+
AtomsCalculators = "0.1"
18+
Optimization = "3.20"
19+
OptimizationOptimJL = "0.1"
20+
StaticArrays = "1.8"
21+
TestItemRunner = "0.2"
22+
Unitful = "1.19"
23+
UnitfulAtomic = "1.0"
24+
julia = "1.9"
25+
DFTK = "0.6"
26+
ASEconvert = "0.1"
27+
EmpiricalPotentials = "0.0.1"
828

929
[extras]
1030
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
31+
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
32+
DFTK = "acf6eb54-70d9-11e9-0013-234b7a5f5337"
33+
ASEconvert = "3da9722f-58c2-4165-81be-b4d7253e8fd2"
34+
EmpiricalPotentials = "38527215-9240-4c91-a638-d4250620c9e2"
1135

1236
[targets]
13-
test = ["Test"]
37+
examples = ["DFTK", "ASEconvert", "EmpiricalPotentials"]
38+
test = ["Test", "TestItemRunner"]

examples/Al_supercell.jl

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#= Test Geometry Optimization on an aluminium supercell.
2+
=#
3+
using LinearAlgebra
4+
using DFTK
5+
using ASEconvert
6+
using LazyArtifacts
7+
using AtomsCalculators
8+
using Unitful
9+
using UnitfulAtomic
10+
using Random
11+
using OptimizationOptimJL
12+
13+
using GeometryOptimization
14+
15+
16+
function build_al_supercell(rep=1)
17+
pseudodojo_psp = artifact"pd_nc_sr_lda_standard_0.4.1_upf/Al.upf"
18+
a = 7.65339 # true lattice constant.
19+
lattice = a * Matrix(I, 3, 3)
20+
Al = ElementPsp(:Al; psp=load_psp(pseudodojo_psp))
21+
atoms = [Al, Al, Al, Al]
22+
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]]
23+
unit_cell = periodic_system(lattice, atoms, positions)
24+
25+
# Make supercell in ASE:
26+
# We convert our lattice to the conventions used in ASE, make the supercell
27+
# and then convert back ...
28+
supercell_ase = convert_ase(unit_cell) * pytuple((rep, 1, 1))
29+
supercell = pyconvert(AbstractSystem, supercell_ase)
30+
31+
# Unfortunately right now the conversion to ASE drops the pseudopotential information,
32+
# so we need to reattach it:
33+
supercell = attach_psp(supercell; Al=pseudodojo_psp)
34+
return supercell
35+
end;
36+
37+
al_supercell = build_al_supercell(1)
38+
39+
# Create a simple calculator for the model.
40+
model_kwargs = (; functionals = [:lda_x, :lda_c_pw], temperature = 1e-4)
41+
basis_kwargs = (; kgrid = [6, 6, 6], Ecut = 30.0)
42+
scf_kwargs = (; tol = 1e-6)
43+
calculator = DFTKCalculator(al_supercell; model_kwargs, basis_kwargs, scf_kwargs, verbose=true)
44+
45+
energy_true = AtomsCalculators.potential_energy(al_supercell, calculator)
46+
47+
# Starting position is a random perturbation of the equilibrium one.
48+
Random.seed!(1234)
49+
x0 = vcat(position(al_supercell)...)
50+
σ = 0.5u"angstrom"; x0_pert = x0 + σ * rand(Float64, size(x0))
51+
al_supercell = update_not_clamped_positions(al_supercell, x0_pert)
52+
energy_pert = AtomsCalculators.potential_energy(al_supercell, calculator)
53+
54+
println("Initial guess distance (norm) from true parameters $(norm(x0 - x0_pert)).")
55+
println("Initial regret $(energy_pert - energy_true).")
56+
57+
optim_options = (f_tol=1e-6, iterations=6, show_trace=true)
58+
59+
results = minimize_energy!(al_supercell, calculator; optim_options...)
60+
println(results)

examples/H2.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Printf
2+
using LinearAlgebra
3+
using DFTK
4+
using Unitful
5+
using UnitfulAtomic
6+
using OptimizationOptimJL
7+
8+
using GeometryOptimization
9+
10+
11+
a = 10. # Big box around the atoms.
12+
lattice = a * I(3)
13+
H = ElementPsp(:H; psp=load_psp("hgh/lda/h-q1"));
14+
atoms = [H, H];
15+
positions = [[0, 0, 0], [0, 0, .16]]
16+
system = periodic_system(lattice, atoms, positions)
17+
18+
# Set everything to optimizable.
19+
system = clamp_atoms(system, [1])
20+
21+
# Create a simple calculator for the model.
22+
model_kwargs = (; functionals = [:lda_x, :lda_c_pw])
23+
basis_kwargs = (; kgrid = [1, 1, 1], Ecut = 10.0)
24+
scf_kwargs = (; tol = 1e-7)
25+
calculator = DFTKCalculator(system; model_kwargs, basis_kwargs, scf_kwargs)
26+
27+
solver = OptimizationOptimJL.LBFGS()
28+
optim_options = (f_tol=1e-32, iterations=20, show_trace=true)
29+
30+
results = minimize_energy!(system, calculator; solver=solver, optim_options...)
31+
println(results)
32+
@printf "Bond length: %3f bohrs.\n" norm(results.minimizer[1:end])

examples/H2_LJ.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#= Test Geometry Optimization on an aluminium supercell.
2+
=#
3+
using LinearAlgebra
4+
using EmpiricalPotentials
5+
using Unitful
6+
using UnitfulAtomic
7+
using OptimizationOptimJL
8+
using AtomsBase
9+
10+
using GeometryOptimization
11+
12+
13+
bounding_box = 10.0u"angstrom" .* [[1, 0, 0.], [0., 1, 0], [0., 0, 1]]
14+
atoms = [:H => [0, 0, 0.0]u"bohr", :H => [0, 0, 1.9]u"bohr"]
15+
system = periodic_system(atoms, bounding_box)
16+
17+
lj = LennardJones(-1.17u"hartree", 0.743u"angstrom", 1, 1, 0.6u"nm")
18+
19+
solver = OptimizationOptimJL.LBFGS()
20+
optim_options = (f_tol=1e-6, iterations=100, show_trace=false)
21+
22+
results = minimize_energy!(system, lj; solver=solver, optim_options...)
23+
println("Bond length: $(norm(results.minimizer[1:3] - results.minimizer[4:end])).")
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using AtomsBase
2+
using AtomsCalculators
3+
using EmpiricalPotentials
4+
using ExtXYZ
5+
using Unitful
6+
using UnitfulAtomic
7+
using OptimizationOptimJL
8+
9+
using GeometryOptimization
10+
11+
fname = joinpath(pkgdir(EmpiricalPotentials), "data/", "TiAl-1024.xyz")
12+
data = ExtXYZ.load(fname) |> FastSystem
13+
14+
lj = LennardJones(-1.0u"meV", 3.1u"Å", 13, 13, 6.0u"Å")
15+
16+
# Convert to AbstractSystem, so we have the `particles` attribute.
17+
particles = map(data) do atom
18+
Atom(; pairs(atom)...)
19+
end
20+
system = AbstractSystem(data; particles)
21+
22+
solver = OptimizationOptimJL.LBFGS()
23+
optim_options = (f_tol=1e-8, g_tol=1e-8, iterations=10, show_trace=true)
24+
25+
results = minimize_energy!(system, lj; solver, optim_options...)
26+
println(results)

src/GeometryOptimization.jl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
module GeometryOptimization
22

3-
# Write your package code here.
3+
using StaticArrays
4+
using Optimization
5+
using OptimizationOptimJL
6+
using AtomsBase
7+
using AtomsCalculators
8+
using Unitful
9+
using UnitfulAtomic
10+
11+
include("atomsbase_interface.jl")
12+
include("optimization.jl")
413

514
end

src/atomsbase_interface.jl

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#
2+
# Interface between AtomsBase.jl and GeometryOptimization.jl that provides
3+
# utility functions for manipulating systems.
4+
#
5+
# IMPORTANT: Note that we always work in cartesian coordinates.
6+
#
7+
export update_positions, update_not_clamped_positions, clamp_atoms
8+
9+
10+
@doc raw"""
11+
Creates a new system based on ``system`` but with atoms positions updated
12+
to the ones provided.
13+
14+
"""
15+
function update_positions(system, positions::AbstractVector{<:AbstractVector{<:Unitful.Length}})
16+
particles = [Atom(atom; position) for (atom, position) in zip(system, positions)]
17+
AbstractSystem(system; particles)
18+
end
19+
20+
@doc raw"""
21+
Creates a new system based on ``system`` where the non clamped positions are
22+
updated to the ones provided (in the order in which they appear in the system).
23+
"""
24+
function update_not_clamped_positions(system, positions::AbstractVector{<:Unitful.Length})
25+
mask = not_clamped_mask(system)
26+
new_positions = deepcopy(position(system))
27+
new_positions[mask] = reinterpret(reshape, SVector{3, eltype(positions)},
28+
reshape(positions, 3, :))
29+
update_positions(system, new_positions)
30+
end
31+
32+
@doc raw"""
33+
Returns a mask for selecting the not clamped atoms in the system.
34+
35+
"""
36+
function not_clamped_mask(system)
37+
# If flag not set, the atom is considered not clamped.
38+
[haskey(a, :clamped) ? !a[:clamped] : true for a in system]
39+
end
40+
41+
function not_clamped_positions(system)
42+
mask = not_clamped_mask(system)
43+
Iterators.flatten(system[mask, :position])
44+
end
45+
46+
@doc raw"""
47+
Clamp given atoms in the system. Clamped atoms are fixed and their positions
48+
will not be optimized. The atoms to be clamped should be given as a list of
49+
indices corresponding to their positions in the system.
50+
51+
"""
52+
function clamp_atoms(system, clamped_indexes::Union{AbstractVector{<:Integer},Nothing})
53+
clamped = falses(length(system))
54+
clamped[clamped_indexes] .= true
55+
particles = [Atom(atom; clamped=m) for (atom, m) in zip(system, clamped)]
56+
AbstractSystem(system, particles=particles)
57+
end

src/optimization.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#=
2+
# Note that by default all particles in the system are assumed optimizable.
3+
# IMPORTANT: Note that we always work in cartesian coordinates.
4+
=#
5+
6+
export minimize_energy!
7+
8+
9+
"""
10+
By default we work in cartesian coordinaes.
11+
Note that internally, when optimizing the cartesian positions, atomic units
12+
are used.
13+
"""
14+
function Optimization.OptimizationFunction(system, calculator; kwargs...)
15+
mask = not_clamped_mask(system) # mask is assumed not to change during optim.
16+
17+
f = function(x::AbstractVector{<:Real}, p)
18+
new_system = update_not_clamped_positions(system, x * u"bohr")
19+
energy = AtomsCalculators.potential_energy(new_system, calculator; kwargs...)
20+
austrip(energy)
21+
end
22+
23+
g! = function(G::AbstractVector{<:Real}, x::AbstractVector{<:Real}, p)
24+
new_system = update_not_clamped_positions(system, x * u"bohr")
25+
energy = AtomsCalculators.potential_energy(new_system, calculator; kwargs...)
26+
27+
forces = AtomsCalculators.forces(new_system, calculator; kwargs...)
28+
# Translate the forces vectors on each particle to a single gradient for the optimization parameter.
29+
forces_concat = collect(Iterators.flatten(forces[mask]))
30+
31+
# NOTE: minus sign since forces are opposite to gradient.
32+
G .= - austrip.(forces_concat)
33+
end
34+
OptimizationFunction(f; grad=g!)
35+
end
36+
37+
function minimize_energy!(system, calculator; solver=Optim.LBFGS(), kwargs...)
38+
# Use current system parameters as starting positions.
39+
x0 = austrip.(not_clamped_positions(system)) # Optim modifies x0 in-place, so need a mutable type.
40+
f_opt = OptimizationFunction(system, calculator)
41+
problem = OptimizationProblem(f_opt, x0, nothing) # Last argument needed in Optimization.jl.
42+
solve(problem, solver; kwargs...)
43+
end

test/dummy_calculator.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Create a dummy AtomsCalculator to test the geometry optimization interfcae.
2+
@testsetup module TestCalculators
3+
using AtomsCalculators
4+
using Unitful
5+
using UnitfulAtomic
6+
7+
struct DummyCalculator end
8+
9+
AtomsCalculators.@generate_interface function AtomsCalculators.potential_energy(
10+
system, calculator::DummyCalculator; kwargs...)
11+
0.0u"eV"
12+
end
13+
14+
AtomsCalculators.@generate_interface function AtomsCalculators.forces(
15+
system, calculator::DummyCalculator; kwargs...)
16+
AtomsCalculators.zero_forces(system, calculator)
17+
end
18+
end

0 commit comments

Comments
 (0)