Skip to content

Commit c4cf1f0

Browse files
authored
Add verbosity flag and callback for structured printing (#16)
1 parent 96574cf commit c4cf1f0

File tree

12 files changed

+190
-63
lines changed

12 files changed

+190
-63
lines changed

Project.toml

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

66
[deps]
77
AtomsBase = "a963bdd2-2df7-4f54-a1ee-49d51e6be12a"
@@ -11,7 +11,10 @@ LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255"
1111
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1212
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
1313
OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e"
14+
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
15+
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1416
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
17+
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
1518
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
1619
UnitfulAtomic = "a7773ee8-282e-5fa2-be4e-bd808c38a91a"
1720

@@ -23,16 +26,20 @@ DocStringExtensions = "0.9"
2326
LineSearches = "7"
2427
Optimization = "3"
2528
OptimizationOptimJL = "0.3"
29+
PrettyTables = "2"
2630
StaticArrays = "1"
2731
TestItemRunner = "0.2"
32+
TimerOutputs = "0.5.12"
2833
Unitful = "1"
2934
UnitfulAtomic = "1"
3035
julia = "1.10"
3136

3237
[extras]
38+
ASEconvert = "3da9722f-58c2-4165-81be-b4d7253e8fd2"
3339
AtomsBuilder = "f5cc8831-eeb7-4288-8d9f-d6c1ddb77004"
40+
PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
3441
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3542
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
3643

3744
[targets]
38-
test = ["AtomsBuilder", "Test", "TestItemRunner"]
45+
test = ["ASEconvert", "AtomsBuilder", "PythonCall", "Test", "TestItemRunner"]

docs/src/examples/aluminium_dftk.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ using DFTK
2020
2121
model_kwargs = (; functionals=[:lda_x, :lda_c_pw], temperature=1e-3)
2222
basis_kwargs = (; kgrid=(3, 3, 3), Ecut=10.0)
23-
scf_kwargs = (; tol=1e-6, mixing=KerkerMixing())
24-
calc = DFTKCalculator(; model_kwargs, basis_kwargs, scf_kwargs, verbose=true)
23+
scf_kwargs = (; mixing=KerkerMixing())
24+
calc = DFTKCalculator(; model_kwargs, basis_kwargs, scf_kwargs)
2525
nothing
2626
```
2727

@@ -42,18 +42,24 @@ nothing
4242
We perform the structure optimisation using the LBFGS solver
4343
from Optim with solver parameters adapted for our geometry optimisation setting.
4444
This is selected by passing the [GeometryOptimization.OptimLBFGS](@ref)
45-
solver as the third argument.
45+
solver as the third argument. The `verbosity=2` flag makes sure we get
46+
output from both the geometry optimisation as well as the inner SCF solver.
4647

4748
```@example dftk-aluminium
4849
using GeometryOptimization
4950
GO = GeometryOptimization
5051
5152
results = minimize_energy!(system, calc, GO.OptimLBFGS();
52-
tol_force=1e-4u"eV/Å",
53-
show_trace=true)
53+
tol_forces=1e-4u"eV/Å", verbosity=2)
5454
nothing
5555
```
5656

57+
!!! tip "Automatically adapted calculator parameters"
58+
Some calculators (such as DFTK) are able to adapt to the keyword arguments
59+
and parameters passed to `minimize_energy!`. In this case the SCF tolerance
60+
is automatically adapted according to the convergence parameters
61+
(here `tol_forces`) passed to `minimize_energy!`.
62+
5763
The final energy is
5864
```@example dftk-aluminium
5965
results.energy

docs/src/examples/other_solvers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ using OptimizationNLopt
4646
solver = NLopt.LD_TNEWTON()
4747
4848
results = minimize_energy!(system, calc, solver;
49-
tol_force=1e-4u"eV/Å", maxeval=100)
49+
tol_forces=1e-4u"eV/Å", maxeval=100)
5050
nothing
5151
```
5252

docs/src/examples/tial_lj.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Minimise energy:
2626
```julia
2727
## TODO: Should run as @example once EmpiricalPotentials is compatible
2828

29-
results = minimize_energy!(system, calc, GO.OptimCG(); maxiters=10, show_trace=true)
29+
results = minimize_energy!(system, calc, GO.OptimCG(); maxiters=10, verbosity=1)
3030
results.energy
3131
```
3232

docs/src/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ system = isolated_system([:H => [0, 0, 0.0]u"bohr",
2727
:H => [0, 0, 1.9]u"bohr"])
2828
calc = LennardJones(-1.17u"hartree", 0.743u"angstrom", 1, 1, 0.6u"nm")
2929

30-
# Run the geometry optimisation
31-
results = minimize_energy!(system, calc)
30+
# Run the geometry optimisation (using verbosity=1 to print the progress)
31+
results = minimize_energy!(system, calc; verbosity=1)
3232

3333
# Inspect the results
3434
optimised_system = results.system

src/GeometryOptimization.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,9 @@ $(DOCSTRING)
2424

2525
include("clamping_updating_positions.jl")
2626
include("optimization.jl")
27+
include("callbacks.jl")
28+
29+
export minimize_energy!
30+
export GeoOptDefaultCallback
2731

2832
end

src/callbacks.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import TimerOutputs
2+
using PrettyTables
3+
using Printf
4+
5+
"""
6+
Callback producing a convergence table summarising the geometry
7+
optimisation convergence. If `always_show_header=true` the
8+
header is shown in each iteration. This is helpful if the calculator
9+
produces output as well.
10+
"""
11+
struct GeoOptDefaultCallback
12+
verbosity::Int
13+
always_show_header::Bool
14+
prev_time::Ref{UInt64}
15+
end
16+
function GeoOptDefaultCallback(verbosity=1; always_show_header=verbosity > 1)
17+
GeoOptDefaultCallback(verbosity, always_show_header, Ref{UInt64}(0))
18+
end
19+
20+
format_log8(value) = (value < 0 ? " " : "+") * (@sprintf "%8.2f" log10(abs(value)))
21+
22+
function (cb::GeoOptDefaultCallback)(optim_state, geoopt_state)
23+
cb.verbosity 0 && return false # No printing, just continue iterations
24+
25+
# If first iteration clear a potentially cached previous time
26+
optim_state.iter 0 && (cb.prev_time[] = 0)
27+
runtime_ns = time_ns() - geoopt_state.start_time
28+
tstr = @sprintf "% 6s" TimerOutputs.prettytime(runtime_ns - cb.prev_time[])
29+
cb.prev_time[] = runtime_ns
30+
31+
Estr = (@sprintf "%+15.12f" round(austrip(geoopt_state.energy), sigdigits=13))[1:15]
32+
logΔE = optim_state.iter < 1 ? "" : format_log8(austrip(geoopt_state.energy_change))
33+
34+
maxforce = austrip(maximum(norm, geoopt_state.forces))
35+
fstr = iszero(maxforce) ? "" : round(maxforce, sigdigits=8)
36+
37+
fields = [ # Header, width, value
38+
("n", 3, optim_state.iter),
39+
("Energy", 15, Estr),
40+
("log10(ΔE)", 9, logΔE),
41+
("max(Force)", 10, fstr),
42+
# TODO Maximal atomic displacement
43+
# TODO Current virial, trace of lattice deformation matrix
44+
("Δtime", 6, tstr),
45+
# TODO Would be nice to have some simple way to add in
46+
# a few calculator-specific things (e.g. total number of SCF iterations)
47+
]
48+
49+
if cb.always_show_header
50+
hlines = :all
51+
show_header = true
52+
elseif optim_state.iter == 0
53+
hlines = [0, 1]
54+
show_header = true
55+
else
56+
hlines = :none
57+
show_header = false
58+
end
59+
title = iszero(optim_state.iter) ? "Geometry optimisation convergence (in atomic units)" : ""
60+
61+
cb.always_show_header && println(stdout)
62+
pretty_table(stdout, reshape(getindex.(fields, 3), 1, length(fields));
63+
header=Symbol.(getindex.(fields, 1)), columns_width=getindex.(fields, 2),
64+
title, show_header, hlines)
65+
cb.always_show_header && println(stdout)
66+
67+
flush(stdout)
68+
return false
69+
end

src/clamping_updating_positions.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Interface between AtomsBase.jl and GeometryOptimization.jl that provides
33
# utility functions for manipulating systems.
44
#
5-
export clamp_atoms
65

76
"""
87
Convert the input system to a kind of system we can work with, i.e. one where
@@ -48,6 +47,7 @@ indices corresponding to their positions in the system.
4847
in the future.
4948
"""
5049
function clamp_atoms(system, clamped_indexes::Union{AbstractVector{<:Integer},Nothing})
50+
system = convert_to_updatable(system)
5151
clamped = falses(length(system))
5252
clamped[clamped_indexes] .= true
5353
particles = [Atom(atom; clamped=msk) for (atom, msk) in zip(system, clamped)]

src/optimization.jl

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,27 @@
33
# We always work in cartesian coordinates.
44
#
55

6-
export minimize_energy!
7-
86
using AtomsCalculators: Energy, Forces, calculate
97

108
mutable struct GeometryOptimizationState
11-
calc_state # Reference to the current calculator state
12-
energy # Most recent energy value
13-
forces # Most recent force value
14-
virial # Most recent virial value
9+
calculator
10+
calc_state # Reference to the most recent calculator state
11+
energy # Current energy value
12+
forces # Current force value
13+
virial # Current virial value
14+
energy_change # Change in energy since previous step
15+
start_time::UInt64 # Time when the calculation started from time_ns()
1516
end
16-
function GeometryOptimizationState(system, calculator)
17-
GeometryOptimizationState(AC.get_state(calculator),
18-
AC.zero_energy(system, calculator),
19-
AC.zero_forces(system, calculator),
20-
AC.zero_virial(system, calculator))
17+
function GeometryOptimizationState(system, calculator; start_time=time_ns())
18+
GeometryOptimizationState(
19+
calculator,
20+
AC.get_state(calculator),
21+
AC.zero_energy(system, calculator),
22+
AC.zero_forces(system, calculator),
23+
AC.zero_virial(system, calculator),
24+
AC.zero_energy(system, calculator),
25+
start_time,
26+
)
2127
end
2228

2329
"""
@@ -48,7 +54,6 @@ function Optimization.OptimizationProblem(system, calculator, geoopt_state; kwar
4854
new_system = update_not_clamped_positions(system, x * u"bohr")
4955
res = calculate(Energy(), new_system, calculator, ps, geoopt_state.calc_state)
5056
geoopt_state.calc_state = res.state
51-
geoopt_state.energy = res.energy
5257
austrip(res.energy), geoopt_state
5358
end
5459
g! = function(G::AbstractVector{<:Real}, x::AbstractVector{<:Real}, ps)
@@ -89,13 +94,18 @@ can also be employed here.
8994
9095
## Keyword arguments:
9196
- `maxiters`: Maximal number of iterations
97+
- `maxtime`: Maximal allowed runtime (in seconds)
9298
- `tol_energy`: Tolerance in the energy to stop the minimisation (all `tol_*` need to be satisfied)
93-
- `tol_force`: Tolerance in the force to stop the minimisation (all `tol_*` need to be satisfied)
99+
- `tol_forces`: Tolerance in the force to stop the minimisation (all `tol_*` need to be satisfied)
94100
- `tol_virial`: Tolerance in the virial to stop the minimisation (all `tol_*` need to be satisfied)
95-
- `maxstep`: Maximal step size (in AU) to be taken in a single optimisation step
101+
- `maxstep`: Maximal step size (in AU or length units) to be taken in a single optimisation step
96102
(not supported for all `solver`s)
97-
- `callback`: A custom callback, which obtains the pair `(optimization_state, geoopt_state)` and is
98-
expected to return `false` (continue iterating) or `true` (halt iterations).
103+
- `verbosity`: Printing level. The idea is that `0` is silent, `1` displays the optimisation
104+
progress and `≥ 2` starts displaying things from the calculator as well (e.g SCF iterations).
105+
- `callback`: A custom callback, which obtains the pair `(optimization_state, geoopt_state)`
106+
and is expected to return `false` (continue iterating) or `true` (halt iterations). Note
107+
that specifying this overwrites the default printing callback. The calculation thus becomes
108+
silent unless a [`GeoOptDefaultCallback`](@ref) is included in the callback.
99109
- `kwargs`: All other keyword arguments are passed to the call to `solve`. Note, that
100110
if special `kwargs` should be passed to the `Optimization.OptimizationProblem` the user
101111
needs to setup the problem manually (e.g. `OptimizationProblem(system, calculator)`)
@@ -110,12 +120,14 @@ end
110120
# do some additional calculator-specific setup (e.g. callbacks) and so on. Then
111121
# by calling this function the actual minimisation is started off.
112122
function _minimize_energy!(system, calculator, solver;
113-
maxiters=100,
123+
maxiters::Integer=100,
124+
maxtime::Integer=60*60*24*365, # 1 year
114125
tol_energy=Inf*u"eV",
115-
tol_force=1e-4u"eV/Å", # VASP default
116-
tol_virial=1e-6u"eV", # TODO How reasonable ?
126+
tol_forces=1e-4u"eV/Å", # VASP default
127+
tol_virial=1e-6u"eV", # TODO How reasonable ?
117128
maxstep=0.8u"bohr",
118-
callback=(x,y) -> false,
129+
verbosity::Integer=0,
130+
callback=GeoOptDefaultCallback(verbosity),
119131
kwargs...)
120132
solver = setup_solver(system, calculator, solver; maxstep)
121133
system = convert_to_updatable(system)
@@ -124,23 +136,27 @@ function _minimize_energy!(system, calculator, solver;
124136
problem = OptimizationProblem(system, calculator, geoopt_state; sense=Optimization.MinSense)
125137
converged = false
126138

127-
Eold = AC.zero_energy(system, calculator)
139+
Eold = 0 # Atomic units
128140
function inner_callback(optim_state, ::Any, geoopt_state)
141+
geoopt_state.energy = optim_state.objective * u"hartree"
142+
geoopt_state.energy_change = (optim_state.objective - Eold) * u"hartree"
143+
129144
halt = callback(optim_state, geoopt_state)
130145
halt && return true
131146

132-
energy_converged = austrip(abs(geoopt_state.energy - Eold)) < austrip(tol_energy)
133-
force_converged = austrip(maximum(norm, geoopt_state.forces)) < austrip(tol_force)
147+
energy_converged = optim_state.objective - Eold < austrip(tol_energy)
148+
force_converged = austrip(maximum(norm, geoopt_state.forces)) < austrip(tol_forces)
134149
virial_converged = austrip(maximum(abs, geoopt_state.virial)) < austrip(tol_virial)
135150

136-
Eold = geoopt_state.energy
151+
Eold = optim_state.objective
137152
converged = energy_converged && force_converged && virial_converged
138153
return converged
139154
end
140155

141-
optimres = solve(problem, solver; maxiters, callback=inner_callback, kwargs...)
156+
optimres = solve(problem, solver; maxiters, maxtime, callback=inner_callback, kwargs...)
157+
converged || @warn "Geometry optimisation not converged."
142158
(; system=update_not_clamped_positions(system, optimres.u * u"bohr"), converged,
143-
energy=optimres.objective, geoopt_state.forces, geoopt_state.virial,
159+
energy=optimres.objective * u"hartree", geoopt_state.forces, geoopt_state.virial,
144160
state=geoopt_state.calc_state, optimres.stats, optimres.alg, optimres)
145161
end
146162

test/interface_test.jl

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
@testitem "Test GeometryOptimization with AtomsCalculators interface" #=
2-
=# setup=[TestCalculators] begin
1+
@testitem "Test GeometryOptimization with AtomsCalculators interface" begin
32
# This is a fake test. It does not really test anything,
43
# just that the interface code compiles and works properly.
54

65
using AtomsBuilder
6+
using AtomsCalculators
77
using GeometryOptimization
88
using LinearAlgebra
99
using Unitful
1010
using UnitfulAtomic
11+
AC = AtomsCalculators
1112
GO = GeometryOptimization
1213

13-
system = rattle!(bulk(:Al; cubic=true), 0.1u"Å")
14-
system = clamp_atoms(system, [1])
14+
struct DummyCalc end
15+
AC.energy_unit(::DummyCalc) = u"hartree"
16+
AC.length_unit(::DummyCalc) = u"bohr"
17+
AC.@generate_interface function AC.potential_energy(system, calc::DummyCalc; kwargs...)
18+
AC.zero_energy(system, calc)
19+
end
20+
AC.@generate_interface function AC.forces(system, calc::DummyCalc; kwargs...)
21+
AC.zero_forces(system, calc)
22+
end
1523

16-
calculator = TestCalculators.DummyCalc()
24+
system = GO.clamp_atoms(bulk(:Al; cubic=true), [1])
1725
for solver in (GO.Autoselect(), GO.OptimCG(), GO.OptimLBFGS(), GO.OptimSD())
18-
results = minimize_energy!(system, calculator, solver)
26+
results = minimize_energy!(system, DummyCalc(), solver; verbosity=1)
1927
@test norm(position(results.system[1]) - position(system[1])) < 1e-5u"bohr"
2028
end
2129
end

0 commit comments

Comments
 (0)