Geometry optimization
Basic Working Example¶
from qcio import DualProgramInput, OptimizationOutput, Structure
from chemcloud import CCClient
water = Structure(
symbols=["O", "H", "H"],
geometry=[
[-0.11904094, -0.36695321, -0.21996706],
[1.24615604, -0.14134141, 0.99915579],
[-0.24300973, 1.16287522, -1.24168873],
],
)
client = CCClient()
prog_inp = DualProgramInput(
structure=water,
calctype="optimization",
keywords={"maxiter": 25},
subprogram="psi4",
subprogram_args={"model": {"method": "b3lyp", "basis": "6-31g"}},
)
# Submit calculation
future_result = client.compute("geometric", prog_inp)
output: OptimizationOutput = future_result.get()
if output.success:
print("Optimization succeeded!")
# Will be OptimizationResult object
print(output)
# The final structure of the geometry optimization
print(output.results.final_structure)
# Initial structure
print(output.input_data.structure)
# A list of ordered AtomicResult objects for each step in the optimization
print(output.results.trajectory)
# A list of ordered energies for each step in the optimization
print(output.results.energies)
else:
print("Optimization failed!")
# Will be FailedOperation object
print(output)
# Error information
print(output.traceback)
Using Force Fields¶
rdkit
can be specified as a compute backend to perform optimizations using force field methods instead of quantum chemistry backends. To use rdkit
force field methods simply modify the model specification and subprogram
specification as shown below. Also note that rdkit
requires the molecular connectivity to be defined.
water = Structure(
symbols=["O", "H", "H"],
geometry=[
[0.0000, 0.00000, 0.0000],
[0.2774, 0.89290, 0.2544],
[0.6067, -0.23830, -0.7169],
],
# Add bond connectivity to water (from_atom, to_atom, bond_order)
connectivity=[(0, 1, 1.0), (0, 2, 1.0)],
)
opt_input = DualProgramInput(
...
subprogram="rdkit",
subprogram_args={"model": {"method": "UFF"}} # or any other force field
)
future_output = client.compute("geometric", opt_input)
Berny Specifics¶
The berny
procedure uses the pyberny package to perform a geometry optimization. berny
specific keywords are subject to change as the berny
package evolves, but for simplicity a short list is included here with default values noted:
Keyword | Description | Default Value |
---|---|---|
maxsteps |
Maximum number of steps in the optimization | 100 |
gradientmax |
Convergence criteria (AU) | 0.45e-3 |
gradientrms |
Convergence criteria (AU) | 0.15e-3 |
stepmax |
Step in internal coordinates, assuming radian units for angles (AU) | 1.8e-3 |
steprms |
Step in internal coordinates, assuming radian units for angles (AU) | 0.45e-3 |
trust |
Initial trust radius in AU. It is the maximum RMS of the quadratic step | 0.3 |
dihedral |
Form dihedral angles | True |
superweakdih |
Form dihedral angles containing two or more noncovalent bonds | False |
geomeTRIC Specifics¶
The geometric
procedure uses the geomeTRIC package to perform a geometry optimization. geomeTRIC
specific keywords are subject to change as the geomeTRIC
package evolves. Since geomeTRIC
has considerably more keywords, here's the source code that defines various parameters for an optimization. Keywords noted below can be included in the OptimizationInput
keywords dictionary. If these options are overwhelming, keep in mind you can run both the berny
and geometric
optimizers without any keywords and the optimizers will use sensible defaults.
class OptParams(object):
"""
Container for optimization parameters.
The parameters used to be contained in the command-line "args",
but this was dropped in order to call Optimize() from another script.
"""
def __init__(self, **kwargs):
# Whether we are optimizing for a transition state. This changes a number of default parameters.
self.transition = kwargs.get('transition', False)
# CI optimizations sometimes require tiny steps
self.meci = kwargs.get('meci', False)
# Handle convergence criteria; this edits the kwargs
self.convergence_criteria(**kwargs)
# Threshold (in a.u. / rad) for activating alternative algorithm that enforces precise constraint satisfaction
self.enforce = kwargs.get('enforce', 0.0)
# Small eigenvalue threshold
self.epsilon = kwargs.get('epsilon', 1e-5)
# Interval for checking the coordinate system for changes
self.check = kwargs.get('check', 0)
# More verbose printout
self.verbose = kwargs.get('verbose', False)
# Starting value of the trust radius
# Because TS optimization is experimental, use conservative trust radii
self.trust = kwargs.get('trust', 0.01 if self.transition else 0.1)
# Maximum value of trust radius
self.tmax = kwargs.get('tmax', 0.03 if self.transition else 0.3)
# Minimum value of the trust radius
self.tmin = kwargs.get('tmin', 0.0 if (self.transition or self.meci) else min(1.2e-3, self.Convergence_drms))
# Minimum size of a step that can be rejected
self.thre_rj = kwargs.get('thre_rj', 1e-4 if (self.transition or self.meci) else 1e-2)
# Sanity checks on trust radius
if self.tmax < self.tmin:
raise ParamError("Max trust radius must be larger than min")
# The trust radius should not be outside (tmin, tmax)
self.trust = min(self.tmax, self.trust)
self.trust = max(self.tmin, self.trust)
# Maximum number of optimization cycles
self.maxiter = kwargs.get('maxiter', 300)
# Use updated constraint algorithm implemented 2019-03-20
self.conmethod = kwargs.get('conmethod', 0)
# Write Hessian matrix at optimized structure to text file
self.write_cart_hess = kwargs.get('write_cart_hess', None)
# Output .xyz file name may be set separately in
# run_optimizer() prior to calling Optimize().
self.xyzout = kwargs.get('xyzout', None)
# Name of the qdata.txt file to be written.
# The CLI is designed so the user passes true/false instead of the file name.
self.qdata = 'qdata.txt' if kwargs.get('qdata', False) else None
# Whether to calculate or read a Hessian matrix.
self.hessian = kwargs.get('hessian', None)
if self.hessian is None:
# Default is to calculate Hessian in the first step if searching for a transition state.
# Otherwise the default is to never calculate the Hessian.
if self.transition: self.hessian = 'first'
else: self.hessian = 'never'
if self.hessian.startswith('file:'):
if os.path.exists(self.hessian[5:]):
# If a path is provided for reading a Hessian file, read it now.
self.hess_data = np.loadtxt(self.hessian[5:])
else:
raise IOError("No Hessian data file found at %s" % self.hessian)
elif self.hessian.lower() in ['never', 'first', 'each', 'stop', 'last', 'first+last']:
self.hessian = self.hessian.lower()
else:
raise RuntimeError("Hessian command line argument can only be never, first, last, first+last, each, stop, or file:<path>")
# Perform a frequency analysis whenever a cartesian Hessian is computed
self.frequency = kwargs.get('frequency', None)
if self.frequency is None: self.frequency = True
# Temperature and pressure for harmonic free energy
self.temperature, self.pressure = kwargs.get('thermo', [300.0, 1.0])
# Number of desired samples from Wigner distribution
self.wigner = kwargs.get('wigner', 0)
if self.wigner and not self.frequency:
raise ParamError('Wigner sampling requires frequency analysis')
# Reset Hessian to guess whenever eigenvalues drop below epsilon
self.reset = kwargs.get('reset', None)
if self.reset is None: self.reset = not (self.transition or self.meci or self.hessian == 'each')
And convergence criteria:
def convergence_criteria(self, **kwargs):
criteria = kwargs.get('converge', [])
if len(criteria)%2 != 0:
raise RuntimeError('Please pass an even number of options to --converge')
for i in range(int(len(criteria)/2)):
key = 'convergence_' + criteria[2*i].lower()
try:
val = float(criteria[2*i+1])
logger.info('Using convergence criteria: %s %.2e\n' % (key, val))
except ValueError:
# This must be a set
val = str(criteria[2*i+1])
logger.info('Using convergence criteria set: %s %s\n' % (key, val))
kwargs[key] = val
# convergence dictionary to store criteria stored in order of energy, grms, gmax, drms, dmax
# 'GAU' contains the default convergence criteria that are used when nothing is passed.
convergence_sets = {'GAU': [1e-6, 3e-4, 4.5e-4, 1.2e-3, 1.8e-3],
'NWCHEM_LOOSE': [1e-6, 3e-3, 4.5e-3, 3.6e-3, 5.4e-3],
'GAU_LOOSE': [1e-6, 1.7e-3, 2.5e-3, 6.7e-3, 1e-2],
'TURBOMOLE': [1e-6, 5e-4, 1e-3, 5.0e-4, 1e-3],
'INTERFRAG_TIGHT': [1e-6, 1e-5, 1.5e-5, 4.0e-4, 6.0e-4],
'GAU_TIGHT': [1e-6, 1e-5, 1.5e-5, 4e-5, 6e-5],
'GAU_VERYTIGHT': [1e-6, 1e-6, 2e-6, 4e-6, 6e-6]}
# Q-Chem style convergence criteria (i.e. gradient and either energy or displacement)
self.qccnv = kwargs.get('qccnv', False)
# Molpro style convergence criteria (i.e. gradient and either energy or displacement, with different defaults)
self.molcnv = kwargs.get('molcnv', False)
# Check if there is a convergence set passed else use the default
set_name = kwargs.get('convergence_set', 'GAU').upper()
# If we have extra keywords apply them here else use the set
# Convergence criteria in a.u. and Angstrom
self.Convergence_energy = kwargs.get('convergence_energy', convergence_sets[set_name][0])
self.Convergence_grms = kwargs.get('convergence_grms', convergence_sets[set_name][1])
self.Convergence_gmax = kwargs.get('convergence_gmax', convergence_sets[set_name][2])
self.Convergence_drms = kwargs.get('convergence_drms', convergence_sets[set_name][3])
self.Convergence_dmax = kwargs.get('convergence_dmax', convergence_sets[set_name][4])
# Convergence criteria that are only used if molconv is set to True
self.Convergence_molpro_gmax = kwargs.get('convergence_molpro_gmax', 3e-4)
self.Convergence_molpro_dmax = kwargs.get('convergence_molpro_dmax', 1.2e-3)