Skip to content

Commit 2756c8f

Browse files
authored
Make Optimization.jl optional (#27)
1 parent 8a01158 commit 2756c8f

File tree

13 files changed

+410
-186
lines changed

13 files changed

+410
-186
lines changed

Project.toml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,36 @@
11
name = "GeometryOptimization"
22
uuid = "673bf261-a53d-43b9-876f-d3c1fc8329c2"
33
authors = ["JuliaMolSim community"]
4-
version = "0.1.2"
4+
version = "0.1.3"
55

66
[deps]
77
AtomsBase = "a963bdd2-2df7-4f54-a1ee-49d51e6be12a"
88
AtomsCalculators = "a3e0e189-c65a-42c1-833c-339540406eb1"
99
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
1010
LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255"
1111
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
12-
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
13-
OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e"
12+
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
1413
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
1514
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1615
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
1716
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
1817
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
1918
UnitfulAtomic = "a7773ee8-282e-5fa2-be4e-bd808c38a91a"
2019

20+
[weakdeps]
21+
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
22+
23+
[extensions]
24+
GeometryOptimizationOptimizationExt = "Optimization"
25+
2126
[compat]
2227
AtomsBase = "0.5"
2328
AtomsBuilder = "0.2.2"
2429
AtomsCalculators = "0.2.3"
2530
DocStringExtensions = "0.9"
2631
LineSearches = "7"
32+
Optim = "1.11.0"
2733
Optimization = "3"
28-
OptimizationOptimJL = "0.3"
2934
PrettyTables = "2"
3035
StaticArrays = "1"
3136
TestItemRunner = "0.2"
@@ -37,8 +42,9 @@ julia = "1.10"
3742
[extras]
3843
AtomsBuilder = "f5cc8831-eeb7-4288-8d9f-d6c1ddb77004"
3944
EmpiricalPotentials = "38527215-9240-4c91-a638-d4250620c9e2"
45+
OptimizationNLopt = "4e6fcdb7-1186-4e1f-a706-475e75c168bb"
4046
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
4147
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
4248

4349
[targets]
44-
test = ["AtomsBuilder", "EmpiricalPotentials", "Test", "TestItemRunner"]
50+
test = ["AtomsBuilder", "EmpiricalPotentials", "Optimization", "OptimizationNLopt", "Test", "TestItemRunner"]

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A package for optimising the structural parameters of an atomistic system,
99
i.e. the step usually referred to as
1010
**geometry optimisation** or **structural relaxation**
1111
in electronic structure theory and atomistic modelling.
12+
Both relaxing **atomic positions** as well as the **unit cell** is supported.
1213

1314
The package is generic in the datastructures used to represent the geometry,
1415
the calculator used to evaluate energies and forces as well as the solver algorithm.

docs/src/examples/aluminium_dftk.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,14 @@ calc = DFTKCalculator(; model_kwargs, basis_kwargs, scf_kwargs)
3636

3737
We perform the structure optimisation using the LBFGS solver
3838
from Optim with solver parameters adapted for our geometry optimisation setting.
39-
This is selected by passing the [GeometryOptimization.OptimLBFGS](@ref)
39+
This is selected by passing the [OptimLBFGS](@ref)
4040
solver as the third argument. The `verbosity=2` flag makes sure we get
4141
output from both the geometry optimisation as well as the inner SCF solver.
4242

4343
```@example dftk-aluminium
4444
using GeometryOptimization
45-
GO = GeometryOptimization
4645
47-
results = minimize_energy!(system, calc, GO.OptimLBFGS();
46+
results = minimize_energy!(system, calc, OptimLBFGS();
4847
tol_forces=1e-4u"eV/Å", verbosity=2)
4948
nothing
5049
```
@@ -65,13 +64,10 @@ We can view the final structure
6564
results.system
6665
```
6766

68-
Some statistics about the optimisation
67+
The results object returned from Optim (containing some statistics
68+
about the optimisation):
6969
```@example dftk-aluminium
70-
results.stats
71-
```
72-
or the details about the selected algorithm:
73-
```@example dftk-aluminium
74-
results.alg
70+
results.optimres
7571
```
7672

7773
The final state of the calculator object is also accessible

docs/src/examples/other_solvers.md

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,23 @@
1-
# Using a different Optimization.jl compatible solver
1+
# Using a Optimization.jl compatible solver
22

3-
In this example we perform the simplistic optimisation
4-
the bond length of a Hydrogen molecule using a trust region
5-
quasi-Newton method from
3+
In this example we optimize a bulk silicon structure
4+
using a trust region quasi-Newton method from
65
[NLopt](https://github.com/JuliaOpt/NLopt.jl).
76

8-
We create a calculator employing the
9-
[density-functional toolkit](https://dftk.org/)
10-
to compute energies and forces at using the LDA density functional.
7+
We create a Stillinger-Weber calculator
118

129
```@example other-solvers
13-
using DFTK
14-
using PseudoPotentialData
15-
16-
pseudopotentials = PseudoFamily("dojo.nc.sr.lda.v0_4_1.oncvpsp3.standard.upf")
17-
model_kwargs = (; functionals=LDA(), pseudopotentials)
18-
basis_kwargs = (; kgrid=(1, 1, 1), Ecut=20.0)
19-
calc = DFTKCalculator(; model_kwargs, basis_kwargs)
10+
using EmpiricalPotentials
11+
sw = StillingerWeber()
12+
nothing
2013
```
2114

22-
and we build the hydrogen molecular system,
23-
where we attach pseudopotential information for DFTK:
15+
and we build a slightly rattled silicon structure
2416

2517
```@example other-solvers
2618
using AtomsBuilder
2719
using Unitful
28-
using UnitfulAtomic
29-
30-
bounding_box = [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]u"Å"
31-
system = periodic_system([:H => [0, 0, 1.]u"bohr",
32-
:H => [0, 0, 3.]u"bohr"],
33-
bounding_box)
34-
nothing
20+
system = rattle!(bulk(:Si, cubic=true) * (2, 2, 2), 0.1u"Å")
3521
```
3622

3723
We now run [`GeometryOptimization.minimize_energy!`](@ref), but notably
@@ -44,15 +30,15 @@ using GeometryOptimization
4430
using OptimizationNLopt
4531
solver = NLopt.LD_TNEWTON()
4632
47-
results = minimize_energy!(system, calc, solver;
33+
results = minimize_energy!(system, sw, solver;
4834
tol_forces=1e-4u"eV/Å", verbosity=1,
4935
maxeval=100)
5036
nothing
5137
```
5238

53-
The final hydrogen bond length is:
54-
55-
```@example other-solvers
56-
using LinearAlgebra
57-
norm(position(results.system[1]) - position(results.system[2]))
58-
```
39+
!!! info ""
40+
While in principle all *first-order* solvers supported
41+
by Optimization.jl can be employed, only few have been tested with this
42+
package so far. Given the complexity of the Optimization.jl ecosystem
43+
we expect that minor changes of our integration may be needed to make
44+
specific solvers work well. We welcome any PRs.

docs/src/examples/tial_lj.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ nothing # hide
3434
Minimise energy:
3535
```@example tial
3636
using GeometryOptimization
37-
GO = GeometryOptimization
3837
39-
results = minimize_energy!(system, calc, GO.OptimCG(); maxiters=10, verbosity=1)
38+
results = minimize_energy!(system, calc, OptimCG(); maxiters=10, verbosity=1)
4039
nothing # hide
4140
```
4241

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
module GeometryOptimizationOptimizationExt
2+
using AtomsCalculators
3+
using Optimization
4+
using GeometryOptimization
5+
import GeometryOptimization: GeoOptProblem, GeoOptConvergence
6+
const GO = GeometryOptimization
7+
8+
function GeometryOptimization.solve_problem(
9+
prob::GeoOptProblem, solver, cvg::GeoOptConvergence;
10+
callback, maxiters, maxtime, kwargs...)
11+
12+
function inner_callback(optim_state, ::Any, geoopt_state)
13+
cache_evaluations = geoopt_state.cache_evaluations
14+
15+
# Find position in the cache matching the current state
16+
i_match = findlast(cache_evaluations) do eval
17+
isnothing(eval.gradnorm) && return false
18+
19+
tol = 10eps(typeof(optim_state.objective))
20+
obj_matches = abs(eval.objective - optim_state.objective) < tol
21+
22+
if isnothing(optim_state.grad)
23+
# Nothing we can do, let's just hope it's ok
24+
grad_matches = true
25+
else
26+
g_norm = maximum(abs, optim_state.grad)
27+
grad_matches = abs(eval.gradnorm - g_norm) < tol
28+
end
29+
30+
obj_matches && grad_matches
31+
end
32+
i_match = @something i_match length(cache_evaluations)
33+
34+
# Commit data from state and discard the rest
35+
geoopt_state.n_iter = optim_state.iter
36+
push!(geoopt_state.history_energy, cache_evaluations[i_match].energy)
37+
if !isnothing(cache_evaluations[i_match].forces)
38+
geoopt_state.forces .= cache_evaluations[i_match].forces
39+
end
40+
if !isnothing(cache_evaluations[i_match].virial)
41+
geoopt_state.virial .= cache_evaluations[i_match].virial
42+
end
43+
empty!(cache_evaluations)
44+
45+
# Check for convergence
46+
geoopt_state.converged = GO.is_converged(cvg, geoopt_state)
47+
48+
# Callback and possible abortion
49+
halt = callback(optim_state, geoopt_state)
50+
halt && return true
51+
52+
geoopt_state.converged
53+
end
54+
55+
optimres = solve(Optimization.OptimizationProblem(prob), solver;
56+
maxiters, maxtime, callback=inner_callback, kwargs...)
57+
(; minimizer=optimres.u, minimum=optimres.objective, optimres)
58+
end
59+
60+
61+
function Optimization.OptimizationProblem(prob::GeoOptProblem; kwargs...)
62+
f = function(x::AbstractVector{<:Real}, ps)
63+
GO.eval_objective_gradient!(nothing, prob, ps, x), prob.geoopt_state
64+
end
65+
g! = function(G::AbstractVector{<:Real}, x::AbstractVector{<:Real}, ps)
66+
GO.eval_objective_gradient!(G, prob, ps, x), prob.geoopt_state
67+
G
68+
end
69+
f_opt = OptimizationFunction(f; grad=g!)
70+
71+
# Note: Some optimisers modify Dofs x0 in-place, so x0 needs to be mutable type.
72+
x0 = GO.get_dofs(prob.system, prob.dofmgr)
73+
OptimizationProblem(f_opt, x0, AtomsCalculators.get_parameters(prob.calculator);
74+
sense=Optimization.MinSense, kwargs...)
75+
end
76+
end

src/GeometryOptimization.jl

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,14 @@ using AtomsBase
44
using AtomsCalculators
55
using DocStringExtensions
66
using LinearAlgebra
7-
using Optimization
7+
using LineSearches
8+
using Optim
89
using StaticArrays
910
using Unitful
1011
using UnitfulAtomic
1112

12-
# Make sure Optim is always available
13-
using OptimizationOptimJL
14-
using LineSearches
15-
16-
# Useful shortcuts
1713
using AtomsCalculators: Energy, Forces, Virial
18-
AC = AtomsCalculators
14+
const AC = AtomsCalculators
1915

2016
@template METHODS =
2117
"""
@@ -24,11 +20,13 @@ $(TYPEDSIGNATURES)
2420
$(DOCSTRING)
2521
"""
2622

27-
include("dof_management.jl")
28-
include("optimization.jl")
29-
include("callbacks.jl")
30-
3123
export minimize_energy!
3224
export GeoOptDefaultCallback
25+
export Autoselect, OptimLBFGS, OptimCG, OptimSD
26+
27+
include("dof_management.jl")
28+
include("minimize_energy.jl")
29+
include("optim.jl")
30+
include("callbacks.jl")
3331

3432
end

src/callbacks.jl

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,25 @@ function (cb::GeoOptDefaultCallback)(optim_state, geoopt_state)
2525
cb.verbosity 0 && return false # No printing, just continue iterations
2626

2727
# If first iteration clear a potentially cached previous time
28-
optim_state.iter 0 && (cb.prev_time[] = 0)
28+
geoopt_state.n_iter 0 && (cb.prev_time[] = 0)
2929
runtime_ns = time_ns() - geoopt_state.start_time
3030
tstr = @sprintf "% 6s" TimerOutputs.prettytime(runtime_ns - cb.prev_time[])
3131
cb.prev_time[] = runtime_ns
3232

33-
Estr = (@sprintf "%+15.12f" round(austrip(geoopt_state.energy), sigdigits=13))[1:15]
34-
if iszero(geoopt_state.energy_change) && optim_state.iter < 1
33+
energy = austrip(geoopt_state.history_energy[end])
34+
Estr = (@sprintf "%+15.12f" round(energy, sigdigits=13))[1:15]
35+
if geoopt_state.n_iter < 2
3536
logΔE = ""
3637
else
37-
logΔE = format_log8(austrip(geoopt_state.energy_change))
38+
energy_change = geoopt_state.history_energy[end] - geoopt_state.history_energy[end-1]
39+
logΔE = format_log8(austrip(energy_change))
3840
end
3941

4042
maxforce = austrip(maximum(norm, geoopt_state.forces))
4143
fstr = iszero(maxforce) ? "" : round(maxforce, sigdigits=8)
4244

4345
fields = [ # Header, width, value
44-
("n", 3, optim_state.iter),
46+
("n", 3, geoopt_state.n_iter),
4547
("Energy", 15, Estr),
4648
("log10(ΔE)", 9, logΔE),
4749
("max(Force)", 11, fstr),
@@ -62,14 +64,14 @@ function (cb::GeoOptDefaultCallback)(optim_state, geoopt_state)
6264
if cb.always_show_header
6365
hlines = :all
6466
show_header = true
65-
elseif optim_state.iter == 0
67+
elseif geoopt_state.n_iter == 0
6668
hlines = [0, 1]
6769
show_header = true
6870
else
6971
hlines = :none
7072
show_header = false
7173
end
72-
title = iszero(optim_state.iter) ? "Geometry optimisation convergence (in atomic units)" : ""
74+
title = iszero(geoopt_state.n_iter) ? "Geometry optimisation convergence (in atomic units)" : ""
7375

7476
cb.always_show_header && println(stdout)
7577
pretty_table(stdout, reshape(getindex.(fields, 3), 1, length(fields));

src/dof_management.jl

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ One aspect of this definition is that clamped atom positions still change via
2929
the deformation `F`. This is natural in the context of optimizing the
3030
cell shape.
3131
"""
32-
mutable struct DofManager{D,T}
32+
struct DofManager{D,T}
3333
variablecell::Bool
3434
ifree::Vector{Int} # extract the free position dofs
3535
r0::T
3636
X0::Vector{SVector{D,T}} # reference positions
3737
C0::NTuple{D, SVector{D, T}} # reference cell
3838
end
39-
# TODO Why is this mutable ?
39+
40+
# TODO: Change F to F-I (such that the initial guess is exactly 0 everywhere)
4041

4142

4243
# NOTES:
@@ -181,12 +182,12 @@ end
181182
# Compute the gradient with respect to dofs
182183
# from forces and virials
183184

184-
function energy_dofs(system, calculator, dofmgr, x::AbstractVector, ps, state)
185+
function eval_objective(system, calculator, dofmgr, x::AbstractVector, ps, state)
185186
res = AC.calculate(Energy(), set_dofs(system, dofmgr, x), calculator, ps, state)
186187
(; energy_unitless=austrip(res.energy), res...)
187188
end
188189

189-
function gradient_dofs(system, calculator, dofmgr, x::AbstractVector{T}, ps, state) where {T}
190+
function eval_gradient(system, calculator, dofmgr, x::AbstractVector{T}, ps, state) where {T}
190191
# Compute and transform forces and virial into a gradient w.r.t. x
191192
if fixedcell(dofmgr)
192193
# fixed cell version
@@ -212,3 +213,18 @@ function gradient_dofs(system, calculator, dofmgr, x::AbstractVector{T}, ps, sta
212213
end
213214
(; grad, res...)
214215
end
216+
217+
# =======================
218+
# Idea for making this more flexible:
219+
#
220+
# struct FixedCell end
221+
# struct UnitaryCell end
222+
#
223+
# struct DofManager{D,T,CellParam,Sys}
224+
# system0::Sys # Reference system
225+
# r0::T # Reference unit length
226+
# X0::Vector{SVector{D,T}} # Reference positions
227+
# C0::NTuple{D, SVector{D, T}} # Reference cell
228+
# cellparam::CellParam # Parametrisation used for the unit cell (fixed, voigt, unitary)
229+
# ifree::Vector{Int} # Degrees of freedom, which can be changed
230+
# end

0 commit comments

Comments
 (0)