Find the Ground State of an EON Structure

Atomistic workflows minimize. ASE relaxes structures with L-BFGS and FIRE; eOn (TheochemUI/eOn on GitHub) runs its own L-BFGS and conjugate-gradient minimizers at the end of every min-mode and nudged-elastic-band search, over structures stored as EON .con files and driven by a potential rgpot serves over RPC. The atomistic-cookbook example lab- cosmo/atomistic-cookbook runs exactly this stack: a PET-MAD model, a NEB path, eOn’s saddle search.

All of these minimizers are local: they walk downhill to the nearest minimum. The nearest minimum is often not the one you want. This tutorial finds the minimum a local search misses, and shows that anneal reaches it where ASE’s optimizer and scipy’s simulated annealing do not improve on it. The numbers you will reproduce, from one disordered start on a 7-atom Lennard- Jones cluster (reduced units, global minimum -16.505384):

Minimizer

Final energy

Evaluations

local L-BFGS (ASE, eOn)

-3.13

500

scipy dual_annealing (global)

-16.505384

17006

anneal portfolio (global)

-16.505384

14920

Local L-BFGS stops at the nearest minimum. Both global methods reach the ground state; anneal reaches it in fewer evaluations than scipy.

What you will need

  • pixi (manages the toolchain)

  • The rgpot, anneal, and readcon-core checkouts side by side

  • About 10 minutes

One command builds everything and runs the comparison:

cd rgpot
pixi run -e rpctest ase-anneal-compare

It builds the potserv potential server, builds the anneal and readcon Python modules, and runs the two scripts below.

Step 1: drive an rgpot potential from Python

rgpot exposes every potential through one ASE calculator, RgpotCalculator. It talks to the same potserv RPC server the eOn client connects to, so Python and eOn evaluate the same energies and forces. Point it at a potential and it launches the server:

import numpy as np
from ase import Atoms
from ase_calculator import RgpotCalculator   # CppCore/rgpot/rpc on sys.path

calc = RgpotCalculator.spawn(server_bin="path/to/potserv", potential="LJ")

start = np.random.RandomState(1).uniform(-2.3, 2.3, size=(7, 3))
atoms = Atoms(numbers=[0] * 7, positions=start, cell=[40, 40, 40])
atoms.calc = calc

print("start energy:", atoms.get_potential_energy())   # about +20

Use spawn as a context manager (with RgpotCalculator.spawn(...) as calc:) to stop the managed server on exit.

Step 2: local relaxation stops at the nearest minimum

from ase.optimize import LBFGS

LBFGS(atoms).run(fmax=1e-3)
print("local L-BFGS energy:", atoms.get_potential_energy())   # about -3.13

L-BFGS converges to a minimum, just not the global one. The LJ7 global minimum, the pentagonal bipyramid at -16.505384, sits in a different basin; from a random start a local minimizer cannot cross into it. eOn’s L-BFGS, run on the same surface, stops in the same place – this is the wall a local search hits when a relaxation lands in a metastable basin instead of the product state.

Step 3: find the global minimum

AnnealOptimizer wraps anneal’s portfolio optimizer (the method of the INFORMS Journal on Computing paper: Thompson-allocated basin hopping, differential evolution, generalized simulated annealing, parallel tempering, GLE-Langevin) behind the same run(). It uses the rgpot forces as the gradient:

from ase_helpers import AnnealOptimizer

atoms.set_positions(start)              # back to the disordered start
opt = AnnealOptimizer(atoms, budget=15000, seed=11, pad=3.0)
opt.run()
print("anneal energy:", opt.get_potential_energy())   # -16.505384
print("evaluations:", opt.nevals)

The geometry ends at the global minimum. scipy.optimize.dual_annealing reaches the same value on this surface; anneal reaches it in fewer evaluations, and both pass the value the local search returns. tests/ase_vs_anneal.py runs all three side by side.

Which minimizer for which job

  • Local L-BFGS / FIRE / conjugate gradient (ASE or eOn): refine a structure

inside the basin you want – an eOn saddle endpoint, a perturbed crystal, an MD snapshot.

  • AnnealOptimizer: find the minimum when the basin is unknown – cluster ground

states, conformer search, potential-parameter fitting.

Chain them: an anneal search to locate the basin, then a local polish.

Step 4: start from a .con file

EON workflows start from a structure on disk. atoms_from_con reads an EON .con file into ase.Atoms through readcon- core – the files eOn reads and writes:

from ase_helpers import atoms_from_con

calc = RgpotCalculator.spawn(server_bin="path/to/potserv", potential="CuH2")
atoms = atoms_from_con("tiny_cuh2.con", calc=calc)
print(atoms.get_chemical_symbols())          # ['Cu', 'Cu', 'H', 'H']
print("energy:", atoms.get_potential_energy())

# The cell comes from the .con; override it with a larger box when needed.
atoms = atoms_from_con("tiny_cuh2.con", cell=[15, 20, 30], pbc=False)

Relaxing it is the same run(): rattle the structure, then walk it back to the minimum the .con recorded:

atoms.calc = calc
atoms.rattle(stdev=0.15, seed=1)
LBFGS(atoms).run(fmax=0.02)
print("relaxed energy:", atoms.get_potential_energy())   # back to -6.474708

tests/con_relax_demo.py runs this round-trip.

Visualize the minimization on the (s, d) landscape

Two complementary views. First, sample the surface densely: a probe atom moved through the x-y plane near a lone atom (a shallow well) and a bound pair (a deeper well) gives a 2D Lennard-Jones double well. ASE LBFGS started in the shallow basin stops there; anneal’s portfolio reaches the deep one.

pixi run -e rpctest pes-double-well
_static/pes_double_well.png

rgpot LJ double well (densely evaluated 2D PES). ASE LBFGS (red) stops in the shallow left basin at -2.0; anneal’s global portfolio (orange star) reaches the deep right basin at -3.0.

Second, for an EON minimization, ``rgpycrumbs eon plt-min`` renders the actual trajectory on a gradient-enhanced 2D landscape: progress ``s`` toward the minimum against lateral deviation ``d``, with the energy surface fitted from the path. ``tests/eonminlandscape.py\`` runs an ``eonclient`` LJ7 minimization and plots it (energies capped with ``plt-min –energy-cap-window`` so the colorbar resolves the well):

pixi run -e eonviz eon-min-landscape
_static/eon_min_landscape.png

An EON LJ7 minimization on the (s, d) landscape: the path descends from the start (Init) to the global minimum at -16.505 (Min), and the surface energy drops monotonically along progress s.

The ``eonviz`` environment is separate from the C++ build env: ``eon`` pins ``fmt>=12`` (clashing with rgpot’s C++ ``fmt<12``), so it carries no default feature and pulls ``eon``, ``ira``, ``jax``, and the dev ``chemparseplot`` / ``rgpycrumbs`` (the published releases lack the embedded-metadata trajectory parser and ``plt-min``).

Where to go next

  • Swap potential”LJ”= for CuH2, XTB, TBLite, or Metatomic:<model> (the

PET-MAD model the cookbook uses); the calculator stays the same, only the server backend changes, and the same server serves eOn.

links rgpot’s RPC server from C++.

for how an rgpot potential becomes an anneal objective with no glue code.