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 spaceConstrainBond(i, j)freezes the current bond length between two atomsConstrainBond(i, j, value=1.8)constrains that bond length to1.8angstromConstrainAngle(i, j, k)freezes the current bond angleConstrainAngle(i, j, k, value=109.5)constrains that angle to109.5degreesConstrainDihedral(i, j, k, l)freezes the current dihedral angleConstrainDihedral(i, j, k, l, value=180.0)constrains that dihedral to180.0degrees
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:
| Field | Type | Description |
|---|---|---|
trajectory | list[TrajectoryStep] | Ordered list of frames. Index 0 is the input geometry; the last entry is the converged structure. |
converged | bool | Always True when the result is available (non-converged runs produce a failed job instead). |
Each TrajectoryStep contains:
| Field | Type | Units | Shape |
|---|---|---|---|
positions | list[list[float]] | Angstrom | (n_atoms, 3) |
energy | float | Hartree | -- |
forces | list[list[float]] | Hartree/Angstrom | (n_atoms, 3) |
Convenience properties on OptimizationResult:
| Property | Returns |
|---|---|
trajectory_atoms | list[Atoms] — trajectory frames as Atoms objects. |
final_atoms | Atoms — final optimized structure. |
final_energy | Energy of the last trajectory frame (Hartree) |
final_positions | Positions of the last trajectory frame (Angstrom) |
final_forces | Forces of the last trajectory frame (Hartree/Angstrom) |
Convergence Modes
Atomiverse offers four convergence presets via the mode parameter:
| Mode | Max gradient | When to use |
|---|---|---|
loose | 0.007 Hartree/Å | Quick relaxations, pre-scan sanity checks, strained geometries where full optimization is overkill |
standard (default) | 0.005 Hartree/Å | General-purpose production optimizations |
tight | 0.0009 Hartree/Å | More careful stationary-point refinement before downstream analysis |
verytight | 0.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.
Transition State Search
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.
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.LOOSEor 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.