Relaxation Settings

ASSYST provides several relaxation modes that differ in which degrees of freedom are minimized. They all share the same interface (see assyst.relaxations.Relax) but apply different ASE filters and constraints to control whether positions, cell shape, volume, or symmetry are allowed to vary.

This notebook walks through each option on a small Cu structure using ASE’s EMT calculator so the examples run quickly.

Class

Positions

Cell shape

Volume

Symmetry

Relax

yes

fixed

fixed

free

CellRelax

fixed

yes

fixed

free

VolumeRelax

fixed

fixed

yes

free

SymmetryRelax

yes

yes

yes

preserved

FullRelax

yes

yes

yes

free

from assyst.relaxations import Relax, CellRelax, VolumeRelax, SymmetryRelax, FullRelax, relax

from ase.build import bulk
from ase.calculators.emt import EMT
import numpy as np

Starting structure

We start from an FCC Cu cell that is slightly distorted away from its equilibrium so that all relaxation modes have something to do.

def make_structure():
    s = bulk('Cu', 'fcc', a=3.7, cubic=True)
    # Shear and stretch the cell a little
    strain = np.eye(3) + np.array([[0.02, 0.01, 0.0], [0.01, -0.01, 0.0], [0.0, 0.0, 0.03]])
    s.set_cell(s.cell.array @ strain, scale_atoms=True)
    # Displace one atom
    s.positions[0] += [0.05, 0.0, 0.0]
    return s

s0 = make_structure()
print(f'volume   = {s0.get_volume():.3f} A^3')
print(f'cell =\n{s0.cell.array}')
volume   = 52.679 A^3
cell =
[[3.774 0.037 0.   ]
 [0.037 3.663 0.   ]
 [0.    0.    3.811]]

Helper to run a single relaxation through assyst.relaxations.relax() and print a summary.

def show(settings, label):
    [out] = list(relax([make_structure()], settings, EMT()))
    print(f'--- {label} ---')
    print(f'energy   = {out.get_potential_energy():.4f} eV')
    print(f'volume   = {out.get_volume():.3f} A^3')
    print(f'cell  =\n{out.cell.array}')
    print(f'pos[0]   = {out.positions[0]}')
    print()
    return out

Relax – positions only

The base class only relaxes internal positions. Cell shape and volume are kept fixed. This is the right choice if you want to study atomic relaxations at a fixed lattice (e.g. defect cores at a given volume).

show(Relax(force_tolerance=1e-3), 'Relax (positions)')
--- Relax (positions) ---
energy   = 0.2782 eV
volume   = 52.679 A^3
cell  =
[[3.774 0.037 0.   ]
 [0.037 3.663 0.   ]
 [0.    0.    3.811]]
pos[0]   = [1.24991806e-02 4.56971337e-05 2.44048787e-16]
Atoms(symbols='Cu4', pbc=True, cell=[[3.7740000000000005, 0.037000000000000005, 0.0], [0.037000000000000005, 3.6630000000000003, 0.0], [0.0, 0.0, 3.8110000000000004]], calculator=SinglePointCalculator(...))

VolumeRelax – isotropic volume only

VolumeRelax keeps relative atomic positions and the cell shape fixed and only scales the cell isotropically. An optional pressure (in eV/A^3) can be applied. Use this to find the equilibrium volume at fixed shape, e.g. for an EOS scan.

show(VolumeRelax(force_tolerance=1e-3), 'VolumeRelax')
--- VolumeRelax ---
energy   = -0.0066 eV
volume   = 46.349 A^3
cell  =
[[3.61636196 0.03545453 0.        ]
 [0.03545453 3.50999837 0.        ]
 [0.         0.         3.65181649]]
pos[0]   = [ 4.79115257e-02 -4.76068290e-20  0.00000000e+00]
Atoms(symbols='Cu4', pbc=True, cell=[[3.616361961945911, 0.03545452903868547, 0.0], [0.03545452903868556, 3.5099983748298618, 0.0], [0.0, 0.0, 3.6518164909846065]], calculator=SinglePointCalculator(...))

CellRelax – cell shape at fixed volume

CellRelax allows the cell shape to relax while the volume and the relative atomic positions are constrained. This is useful to determine an equilibrium cell shape independent of volume.

show(CellRelax(force_tolerance=1e-3), 'CellRelax')
--- CellRelax ---
energy   = 0.2749 eV
volume   = 52.679 A^3
cell  =
[[ 3.74999627e+00  4.22598103e-04 -2.74415677e-16]
 [-5.58519886e-04  3.74761880e+00  1.05826558e-15]
 [-2.89973337e-16 -1.41779516e-16  3.74842220e+00]]
pos[0]   = [ 4.96869808e-02 -4.95970383e-04 -3.77760288e-18]
Atoms(symbols='Cu4', pbc=True, cell=[[3.749996272558185, 0.00042259810312605706, -2.7441567748951744e-16], [-0.0005585198857762368, 3.7476187968640704, 1.0582655815858194e-15], [-2.8997333667320494e-16, -1.4177951621226878e-16, 3.7484221994809563]], calculator=SinglePointCalculator(...))

SymmetryRelax – full relaxation, preserving space group

SymmetryRelax relaxes positions and cell, but uses ASE’s FixSymmetry constraint to keep the initial space group of the structure. This is useful when you want to reach the energy minimum of a given symmetry-constrained crystal without accidentally hopping into a different polymorph.

show(SymmetryRelax(force_tolerance=1e-3), 'SymmetryRelax')
--- SymmetryRelax ---
energy   = -0.0281 eV
volume   = 46.260 A^3
cell  =
[[ 3.58982183e+00  4.43425047e-04  0.00000000e+00]
 [-4.86369808e-04  3.58975972e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  3.58979071e+00]]
pos[0]   = [ 0.01193187 -0.00010575  0.        ]
Atoms(symbols='Cu4', pbc=True, cell=[[3.5898218289953747, 0.0004434250465215887, 0.0], [-0.0004863698075401491, 3.5897597186116306, 0.0], [0.0, 0.0, 3.5897907114688534]], calculator=SinglePointCalculator(...))

FullRelax – everything free

FullRelax relaxes positions and the full cell with no constraints. This may break the original symmetry of the structure but yields the true local minimum for the given calculator.

show(FullRelax(force_tolerance=1e-3), 'FullRelax')
--- FullRelax ---
energy   = -0.0281 eV
volume   = 46.260 A^3
cell  =
[[ 3.58982183e+00  4.43425047e-04 -3.17074887e-17]
 [-4.86369808e-04  3.58975972e+00  3.47731333e-16]
 [-1.64591795e-17 -1.69308377e-15  3.58979071e+00]]
pos[0]   = [ 1.19318730e-02 -1.05747012e-04 -6.55088215e-15]
Atoms(symbols='Cu4', pbc=True, cell=[[3.589821828995376, 0.00044342504652072906, -3.170748871808039e-17], [-0.0004863698075419537, 3.5897597186116235, 3.477313332380583e-16], [-1.6459179464011644e-17, -1.6930837749666468e-15, 3.589790711468833]], calculator=SinglePointCalculator(...))

Applying pressure

VolumeRelax, SymmetryRelax, and FullRelax accept a pressure argument (units of eV/A^3, the ASE convention). A positive pressure compresses the cell, a negative pressure expands it. This is convenient to generate structures off the equilibrium volume for the training set.

for p in (-0.05, 0.0, 0.05):
    show(SymmetryRelax(force_tolerance=1e-3, pressure=p), f'SymmetryRelax @ p={p} eV/A^3')
--- SymmetryRelax @ p=-0.05 eV/A^3 ---
energy   = 0.0594 eV
volume   = 49.562 A^3
cell  =
[[ 3.67334413e+00  4.53060207e-04  0.00000000e+00]
 [-4.72114196e-04  3.67326515e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  3.67309008e+00]]
pos[0]   = [ 0.01213643 -0.00011955  0.        ]
--- SymmetryRelax @ p=0.0 eV/A^3 ---
energy   = -0.0281 eV
volume   = 46.260 A^3
cell  =
[[ 3.58982183e+00  4.43425047e-04  0.00000000e+00]
 [-4.86369808e-04  3.58975972e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  3.58979071e+00]]
pos[0]   = [ 0.01193187 -0.00010575  0.        ]
--- SymmetryRelax @ p=0.05 eV/A^3 ---
energy   = 0.0292 eV
volume   = 43.860 A^3
cell  =
[[ 3.52662319e+00  4.61513352e-04  0.00000000e+00]
 [-4.82133340e-04  3.52658468e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  3.52660237e+00]]
pos[0]   = [ 0.01172025 -0.00010472  0.        ]

Optimizer and convergence

All relaxation classes share three knobs:

  • algorithm'LBFGS' (default), 'BFGS', or 'FIRE'. LBFGS is usually fastest for well-behaved systems; FIRE is more robust for noisy or stiff potentials.

  • force_tolerance – maximum residual force (eV/A) at which the optimizer stops.

  • max_steps – hard cap on the number of optimizer steps.

for algo in ('LBFGS', 'BFGS', 'FIRE'):
    show(FullRelax(algorithm=algo, force_tolerance=1e-3, max_steps=200), f'FullRelax / {algo}')
--- FullRelax / LBFGS ---
energy   = -0.0281 eV
volume   = 46.260 A^3
cell  =
[[ 3.58982183e+00  4.43425047e-04 -3.17074887e-17]
 [-4.86369808e-04  3.58975972e+00  3.47731333e-16]
 [-1.64591795e-17 -1.69308377e-15  3.58979071e+00]]
pos[0]   = [ 1.19318730e-02 -1.05747012e-04 -6.55088215e-15]
--- FullRelax / BFGS ---
energy   = -0.0281 eV
volume   = 46.260 A^3
cell  =
[[ 3.58982183e+00  4.43425047e-04 -4.10397021e-16]
 [-4.86369808e-04  3.58975972e+00  1.84800982e-15]
 [-4.51102644e-16  1.81496158e-15  3.58979071e+00]]
pos[0]   = [ 1.19318730e-02 -1.05747012e-04 -1.31728955e-15]
--- FullRelax / FIRE ---
energy   = -0.0281 eV
volume   = 46.262 A^3
cell  =
[[ 3.59016243e+00  4.25469374e-04  8.57522550e-18]
 [-4.74899174e-04  3.58954000e+00  5.16658685e-16]
 [ 1.27749967e-17 -9.82100490e-16  3.58981764e+00]]
pos[0]   = [ 1.18927460e-02 -1.19549147e-04 -4.20777233e-16]

Relaxing many structures

relax is a generator: pass it any iterable of Atoms and it yields the relaxed copies one by one. Each output structure has a SinglePointCalculator attached with the final energy, forces, and stress, ready to be written to a training file.

To illustrate this we relax four different fcc metals supported by EMT (Cu, Ag, Au, Ni). Each settles at its own equilibrium lattice parameter rather than collapsing to a single common value.

structures = [bulk(el, 'fcc', cubic=True) for el in ('Cu', 'Ag', 'Au', 'Ni')]
settings = SymmetryRelax(force_tolerance=1e-3)
for s in relax(structures, settings, EMT()):
    a = s.cell.array.diagonal().mean()  # cubic, so a = side length
    print(f'{s.symbols[0]:2s}  a = {a:.3f} A   E = {s.get_potential_energy():.4f} eV')
Cu  a = 3.590 A   E = -0.0281 eV
Ag  a = 4.064 A   E = -0.0015 eV
Au  a = 4.056 A   E = -0.0005 eV
Ni  a = 3.487 A   E = -0.0532 eV