Skip to main content

Geometry Optimization

A geometry optimization relaxes a molecular structure toward a stationary point -- either a local minimum (default) or a transition state (first-order saddle point). The calculation returns the full optimization trajectory: positions, energy, and forces at every step.

For a single energy evaluation without relaxation, see Single Point Energy.

Your First Optimization

import atomiverse
from atomiverse import Atoms, JobFailedError, Optimization
from atomiverse.levels import GFN2_XTB

atomiverse.configure(api_key="your-api-key")

water = Atoms.from_smiles("O")

job = Optimization(
atoms=water,
level_of_theory=GFN2_XTB,
)

job.submit()

try:
result = job.require_result()
except JobFailedError as exc:
print(f"Job failed: {exc.failure.error}")
else:
print(f"Final energy: {result.final_energy} Hartree")
print(f"Steps: {len(result.trajectory)}")

Constraints

Optimization jobs can optionally keep selected internal coordinates fixed during the relaxation via the constraints parameter.

Supported constraint types:

  • FreezeAtom(atom_index) locks one atom in Cartesian space
  • ConstrainBond(i, j) freezes the current bond length between two atoms
  • ConstrainBond(i, j, value=1.8) constrains that bond length to 1.8 angstrom
  • ConstrainAngle(i, j, k) freezes the current bond angle
  • ConstrainAngle(i, j, k, value=109.5) constrains that angle to 109.5 degrees
  • ConstrainDihedral(i, j, k, l) freezes the current dihedral angle
  • ConstrainDihedral(i, j, k, l, value=180.0) constrains that dihedral to 180.0 degrees
from atomiverse import (
ConstrainBond,
ConstrainDihedral,
FreezeAtom,
Optimization,
)

job = Optimization(
atoms=molecule,
level_of_theory=GFN2_XTB,
constraints=[
FreezeAtom(0),
ConstrainBond(0, 1),
ConstrainBond(1, 2, value=1.8),
ConstrainDihedral(3, 4, 5, 8, value=180.0),
],
)

Constraints use zero-based atom indices. If value is omitted, the current value of the selected coordinate from the input geometry is frozen. Explicit target values use angstrom for bonds and degrees for angles and dihedrals.

Constraint validation happens before submission. Atom indices must be valid, indices within a single constraint must be distinct, target values must be finite, and duplicate constraints with the same atom selection and target value are rejected.

Result

job.require_result() returns an OptimizationResult with the following fields:

FieldTypeDescription
trajectorylist[TrajectoryStep]Ordered list of frames. Index 0 is the input geometry; the last entry is the converged structure.
convergedboolAlways True when the result is available (non-converged runs produce a failed job instead).

Each TrajectoryStep contains:

FieldTypeUnitsShape
positionslist[list[float]]Angstrom(n_atoms, 3)
energyfloatHartree--
forceslist[list[float]]Hartree/Angstrom(n_atoms, 3)

Convenience properties on OptimizationResult:

PropertyReturns
trajectory_atomslist[Atoms] — trajectory frames as Atoms objects.
final_atomsAtoms — final optimized structure.
final_energyEnergy of the last trajectory frame (Hartree)
final_positionsPositions of the last trajectory frame (Angstrom)
final_forcesForces of the last trajectory frame (Hartree/Angstrom)

Convergence Modes

Atomiverse offers four convergence presets via the mode parameter:

ModeMax gradientWhen to use
loose0.007 Hartree/ÅQuick relaxations, pre-scan sanity checks, strained geometries where full optimization is overkill
standard (default)0.005 Hartree/ÅGeneral-purpose production optimizations
tight0.0009 Hartree/ÅMore careful stationary-point refinement before downstream analysis
verytight0.00003 Hartree/ÅExtremely strict final refinement and high-precision follow-up work
from atomiverse import Optimization, OptimizationMode

job = Optimization(
atoms=water,
level_of_theory=GFN2_XTB,
mode=OptimizationMode.VERYTIGHT,
)

The optimization is considered converged when the maximum gradient on any atom drops below the selected mode's threshold.

Set is_transition_state=True to search for a first-order saddle point instead of a minimum:

job = Optimization(
atoms=ts_guess,
level_of_theory=GFN2_XTB,
is_transition_state=True,
)

job.submit()

A good starting geometry is essential for transition state searches. The optimizer follows the lowest eigenmode of the Hessian uphill while relaxing all other modes downhill. Poor starting guesses will typically fail to converge.

warning

Transition state searches require at least 3 atoms and a reasonable initial guess close to the saddle point. The optimizer cannot find a TS from an arbitrary geometry.

Charge, Multiplicity, and Level of Theory

These work exactly as for single point energy calculations:

job = Optimization(
atoms=molecule,
charge=-1,
multiplicity=2,
level_of_theory=GFN2_XTB,
)

See Levels of Theory for all available methods and Single Point Energy for charge/multiplicity details.

Handling Failures

If the optimization does not converge within the allowed number of steps, the job enters the FAILED state. A partial trajectory is still available for inspection through the typed failure payload:

from atomiverse import JobFailedError

job.submit()

try:
result = job.require_result()
except JobFailedError:
failure = job.require_failure()
print(f"Job failed: {failure.error}")

if failure.partial_trajectory:
print(f"Partial trajectory has {len(failure.partial_trajectory)} steps")
last = failure.partial_trajectory[-1]
print(f"Last energy: {last.energy} Hartree")
else:
print(f"Converged in {len(result.trajectory)} steps")
print(f"Final energy: {result.final_energy} Hartree")

Common reasons for failure:

  • The geometry did not converge within the step budget -- try mode=OptimizationMode.LOOSE or provide a better starting structure.
  • The starting geometry is chemically unreasonable -- check bond lengths and atom types.
  • The level of theory could not evaluate the input geometry -- verify that the method supports the elements in your system.

Non-blocking Submission

job.submit(wait=False)
print(job.job_id)

# do other work ...

job.refresh()
if job.state == State.DONE:
print(job.require_result().final_energy)
elif job.state == State.FAILED:
print(job.require_failure().error)

See Single Point Energy -- Non-blocking Submission for the full pattern including save/load, cancellation, and BYOC capacity waits.