""" Provides some of the high level functions and classes to run optimizations. This is a good starting place for anyone
looking to add features to the code to familarize themselves with the overall workings of OptKing.
Functions may be useful to users seeking greater control over the inner workings of OptKing than provided by the
``OptHelpers``. For instance, if manually creating a molecular system or manually controlling / switching algorithms
on the fly.
See Also
--------
:py:class:`stepalgorithms.OptimizationAlgorithm`
:py:class:`stepalgorithms.OptimizationInterface`
"""
import copy
import logging
import pathlib
import os
from typing import Union
import numpy as np
from optking.compute_wrappers import ComputeWrapper
from optking.molsys import Molsys
from . import IRCfollowing, addIntcos, hessian, history, intcosMisc, misc
from . import stepAlgorithms
from . import testB, linesearch
from .exceptions import AlgError, OptError
from .printTools import print_array_string, print_geom_grad, print_mat_string
from . import log_name
from . import op
logger = logging.getLogger(f"{log_name}{__name__}")
[docs]
def optimize(o_molsys, computer):
"""Driver for OptKing's optimization procedure. It is suggested that users use
:py:class:`optking.opt_helper.EngineHelper`, :py:class:`optking.opt_helper.CustomHelper`, or
one of the external interfaces (Psi4 and Qcengine) to perform a normal (full) optimization
Parameters
----------
o_molsys : cls
optking molecular system
computer : compute_wrappers.ComputeWrapper
Returns
-------
float, float or dict
energy and nuclear repulsion energy or MolSSI qc_schema_output as dict
"""
logger = logging.getLogger(__name__)
H = 0 # hessian in internals
fq = 0
opt_history = history.History(op.Params)
# Try to optimize one structure OR set of IRC points. OptError and all Exceptions caught below.
try:
converged = False
if not o_molsys.intcos_present:
make_internal_coords(o_molsys, op.Params)
logger.debug("Molecular system after make_internal_coords:")
logger.debug(str(o_molsys))
opt_object = OptimizationManager(o_molsys, opt_history, op.Params, computer)
logger.info("\tStarting optimization algorithm.\n")
while converged is not True:
try:
H, fq, energy = opt_object.start_step(H)
dq = opt_object.take_step(fq, H, energy, return_str=False)
converged = opt_object.converged(energy, fq, dq)
opt_object.check_maxiter() # raise error otherwise continue
except AlgError as AF:
if isinstance(H, int) or isinstance(fq, int):
raise OptError("Failed to compute Hessian and Forces")
opt_object.alg_error_handler(H, fq, AF)
if opt_object.erase_hessian == "stashed":
H = opt_object.H
qc_output = opt_object.finish(error=None)
return qc_output
except OptError as error:
logger.error(error)
return opt_object.opt_error_handler(error)
except Exception as error:
logger.error(error)
return opt_object.unknown_error_handler(error)
[docs]
class OptimizationManager(stepAlgorithms.OptimizationInterface):
"""Recommended use of Optking's Optimization Algorithms is to create this class and then loop
over take_step. OptimizationFactory will either return the appropriate OptimizationAlgorithm or
return itself if management of multiple algorithms is required (linesearching).
Currently only 1 linesearch method is implemented so changing linesearch_method to anything that does
not evaluate to None will turn linesearch on.
This class' primary purpose is to abstract the interface for an OptimizationAlgorithm, Linesearch, or
IRC so no special handling is needed for an IRC optimization as opposed to a NR optimization.
# TODO add dynamic_level management here
"""
_LINESEARCHES = {"ENERGY": linesearch.ThreePointEnergy}
def __init__(
self,
molsys: Molsys,
history_object: history.History,
params: op.OptParams,
computer: ComputeWrapper,
):
super().__init__(molsys, history_object, params)
self.direction: Union[np.ndarray, None] = None
method = "IRC" if params.opt_type == "IRC" else params.step_type
self.opt_method = optimization_factory(method, molsys, self.history, params)
self.step_number = 0
self.computer = computer
self.linesearch_method = None
self.stashed_hessian = None
if params.linesearch:
self.linesearch_method = OptimizationManager._LINESEARCHES["ENERGY"](
molsys, self.history, params
)
self.opt_method.trust_radius_on = False
self.requires = self.update_requirements()
self.current_requirements = self.update_requirements()
self.params = params
self.erase_hessian = ""
self.check_linesearch = True
self.error = None
self.protocol = {
"protocol": None,
"step_number": -100,
} # Just a number that will never occur naturally
[docs]
def to_dict(self):
"""Convert attributes to serializable form."""
d = {
"direction": self.direction,
"step_number": self.step_number,
"stashed_hessian": self.stashed_hessian,
"requires": self.requires,
"current_requirements": self.current_requirements,
"erase_hessian": self.erase_hessian,
"check_linesearch": self.check_linesearch,
"error": self.error,
}
if self.linesearch_method:
d["linesearch_method"] = self.linesearch_method.to_dict()
if self.params.opt_type == "IRC":
d["irc_object"] = self.opt_method.to_dict()
return d
[docs]
@classmethod
def from_dict(cls, d, molsys, history, params, computer):
"""Reload attributes from the provided dictionary. Create all necessary classes.
To prevent duplication, OptHelper handles converting the molsys, history, params, and computer to/from dict
"""
manager = cls(molsys, history, params, computer)
manager.direction = d["direction"]
method = "IRC" if params.opt_type == "IRC" else params.step_type
manager.step_number = d["step_number"]
manager.stashed_hessian = d["stashed_hessian"]
manager.requires = d["requires"]
manager.current_requirements = d["current_requirements"]
manager.erase_hessian = d["erase_hessian"]
manager.check_linesearch = d["check_linesearch"]
manager.error = d["error"]
if params.opt_type == "IRC":
manager.opt_method = IRCfollowing.IntrinsicReactionCoordinate.from_dict(
d["irc_object"], molsys, history, params
)
else:
manager.opt_method = optimization_factory(
method, molsys, history, params
) # Can just recreate with current history and params
if d.get("linesearch_method"):
manager.linesearch_method = OptimizationManager._LINESEARCHES["ENERGY"].from_dict(
d["linesearch_method"], molsys, history, params
)
return manager
[docs]
def start_step(self, H: np.ndarray):
"""Initialize coordinates. Compute needed properties. Print molecular system and property information
Returns
-------
H: np.ndarray
2D. Hessian in appropriate coordinates
f_q: np.ndarray
1D. forces in appropriate coordinates
E: float
energy
"""
if self.molsys.natom == 1:
logger.info("There is only 1 atom. Nothing to optimize. Computing energy.")
E = get_pes_info(np.ndarray(0), self.computer, self.molsys,
self.history, self.params, "unneeded", ["energy"])[2]
raise OptError(
"There is only 1 atom. Nothing to optimize. Computing energy.", "OptError"
)
# if optimization coordinates are absent, choose them. Could be erased after AlgError
if not self.molsys.intcos_present:
make_internal_coords(self.molsys, self.params)
self.step_number += 1
header = f"{'----------------------------':^74}"
header += f"\n{'Taking A Step: Step Number %d' % self.step_number:^90}"
header += f"\n{'----------------------------':^90}"
logger.info(header)
requirements = self.opt_method.requires()
hessian_protocol = self.get_hessian_protocol(self.step_number)
protocol = hessian_protocol["protocol"]
H, g_q, g_x, E = get_pes_info(
H,
self.computer,
self.molsys,
self.history,
self.params,
protocol,
requirements,
)
logger.info("%s", print_geom_grad(self.molsys.geom, g_x))
self.molsys.q_show()
if self.params.test_B:
testB.test_b(self.molsys)
if self.params.test_derivative_B:
testB.test_derivative_b(self.molsys)
logger.info(print_array_string(-g_q, title="Internal forces in au:"))
return H, -g_q, E
[docs]
def take_step(self, fq=None, H=None, energy=None, return_str=False, **kwargs):
"""Take whatever step (normal, linesearch, IRC, constrained IRC) is next.
Parameters
----------
fq: Union[np.ndarray, None]
forces
H: Union[np.ndarray, None]
hessian
energy: Union[np.ndarray, None]
return_str: bool
if True return string with information about step (information is logged regardless)
"""
self.current_requirements = self.update_requirements()
if not self.params.linesearch:
achieved_dq, returned_str = self.opt_method.take_step(fq, H, energy, return_str=True)
else:
if self.direction is None:
self.direction = self.opt_method.step(fq, H, energy)
self.linesearch_method.start(self.direction)
if self.check_linesearch:
self.history.append(
self.molsys.geom,
energy,
fq,
self.molsys.gradient_to_cartesians(-1 * fq),
)
ls_energy = self.linesearch_method.expected_energy
dq_norm, unit_dq, grad, hess = self.opt_method.step_metrics(
self.direction, fq, H
)
self.history.append_record(ls_energy, self.direction, unit_dq, grad, hess)
self.stashed_hessian = H
self.check_linesearch = False
achieved_dq, returned_str = self.linesearch_method.take_step(
fq, H, energy, return_str=True
)
if self.linesearch_method.minimized:
logger.info("Linesearch complete. Next step will compute a new direction")
# cleanup
self.linesearch_method.reset()
self.direction = None
self.check_linesearch = True
self.requires = self.update_requirements()
if return_str:
return achieved_dq, returned_str
return achieved_dq
[docs]
def update_requirements(self):
"""Get the current requirements for the next step.
Notes
-----
If linesearching requirements can change. Always safe to provide a gradient regardless."""
if self.direction is None:
return self.opt_method.requires()
else:
return self.linesearch_method.requires()
[docs]
def converged(self, E, fq, dq, step_number=None, str_mode=None, **kwargs):
"""Test whether the optimization has finished. An optimization can only be declared converged
If a gradient has been provided (linesearching cannot terminate an optimization)"""
if step_number is None:
step_number = self.step_number
converged = False
if not self.linesearch_method or self.check_linesearch:
converged = self.opt_method.converged(dq, fq, step_number, str_mode=str_mode, **kwargs)
if str_mode:
return converged
if converged is True:
logger.info("\tConverged in %d steps!" % step_number)
logger.info("\tFinal energy is %20.13f" % E)
logger.info("\tFinal structure (Angstroms): \n" + self.molsys.show_geom())
return converged
[docs]
def check_maxiter(self):
"""Check iterations < geom_maxiter. For IRC's check `total_steps_taken`."""
if self.params.opt_type == "IRC":
iterations = self.opt_method.total_steps_taken
else:
iterations = self.step_number
# Hard quit if too many total steps taken (inc. all IRC points and algorithms).
if iterations >= self.params.geom_maxiter:
logger.error(
"\tTotal number of steps (%d) exceeds maximum allowed (%d).\n"
% (iterations, self.params.geom_maxiter)
)
raise OptError(
"Maximum number of steps exceeded: {}.".format(self.params.geom_maxiter),
"OptError",
)
[docs]
def get_hessian_protocol(self, step_number):
"""Determine action to take for how to compute a hessian. Handles alternate IRC behavior
Returns
-------
str: one of ('compute', 'update', 'guess', 'unneeded')
"""
# Check whether the protocol has already been generated for this
# step. This method needs to be called (potentially) multiple times and
# flags like self.erase_hessian will be reset once a protocol is created.
# Saving the protocol allows for this method to be called
# multiple times for a single step with consistent output
# This doesn't work for IRCs since step_number tracks IRC points not individual steps
if step_number == self.protocol.get("step_number") and self.params.opt_type != "IRC":
return self.protocol
# Have not called method yet for this step. Create new protocol
self.protocol = {"protocol": None, "step_number": step_number}
if "hessian" not in self.opt_method.requires():
self.protocol.update({"protocol": "unneeded"})
return self.protocol
if self.erase_hessian:
if self.erase_hessian == "erased":
self.erase_hessian = ""
action = "compute" if self.params.full_hess_every > 0 else "guess"
self.protocol.update({"protocol": action})
return self.protocol
elif self.erase_hessian == "stashed":
self.erase_hessian = ""
self.protocol.update({"protocol": "unneeded"})
return self.protocol
if self.params.cart_hess_read:
self.protocol.update({"protocol": "compute"})
return self.protocol
if self.step_number <= 1:
if self.params.opt_type != "IRC":
if self.params.full_hess_every > -1: # compute hessian at least once.
action = "compute"
else:
action = "guess"
else:
# IRC
if self.opt_method.sub_step_number == -1: # sub_step_number starts at -1 not 0
action = "compute"
else:
action = "update"
else:
if self.params.full_hess_every < 1:
action = "update"
elif (self.step_number - 1) % self.params.full_hess_every == 0:
action = "compute"
else:
action = "update"
self.protocol.update({"protocol": action})
return self.protocol
[docs]
def clear(self):
"""Reset history (inculding all steps) and molecule"""
self.history.steps = []
self.molsys.intcos = []
self.step_number = 0
self.history.steps_since_last_hessian = 0
self.history.consecutive_backsteps = 0
[docs]
def alg_error_handler(self, H, fq, error):
"""consumes an AlgError. Takes appropriate action"""
logger.error(" Caught AlgError exception\n")
eraseIntcos = False
if error.back_transformation:
logger.info("Resetting optimization after critical back-transformation failure")
eraseIntcos = True
eraseHistory = True
elif error.linear_bends:
# New linear bends detected; Add them, and continue at current level.
# from . import bend # import not currently being used according to IDE
# Convert the Updated Hessian back into internal coordinates
Hx = self.molsys.hessian_to_cartesians(H, -1 * fq)
gx = self.molsys.gradient_to_cartesians(-1 * fq)
# This takes a more heavy handed approach:
# Collect all the bends involved in our problematic coordinates (oofp or dihedral)
# Then remove all the bends involved in these coordinates as well as the
# oofps and torsions themselves.
# bends = [bend for dihedral in error.linear_torsions for bend in dihedral.bends]
# bends += [bend for oofp in error.oofp_failures for bend in oofp.bends]
# The algerror includes bends to remove and bends to add seperately
# bends += error.old_bends
logger.info(
f"Current bends considered for removal: {[str(bend) for bend in error.old_bends]}"
)
affected_frags = []
for bend in error.old_bends:
# no need to repeat this code for "COMPLEMENT". Bend already removed for LINEAR
# Needed to ensure that bends that become zero get removed and Don't get accidently
# readded when they're not quite linear, but will be on the next step.
if bend.bend_type != "COMPLEMENT":
iF = addIntcos.check_fragment(bend.atoms, self.molsys)
F = self.molsys.fragments[iF]
affected_frags.append(F)
intcosMisc.remove_old_now_linear_bend(bend.atoms, F.intcos)
# Add the new linear bends if not already added (here its easy to know which bends
# need to be added back)
for bend in error.linear_bends:
iF = addIntcos.check_fragment(bend.atoms, self.molsys)
F = self.molsys.fragments[iF]
if bend not in F.intcos:
F.intcos.append(bend)
# problematic coordinates are now all removed. linear bends should be taken care of.
# add bends and any appropriate four index coordinates to complete coord set.
for frag in set(affected_frags):
frag.add_intcos_from_connectivity(ignore_coords=error.old_bends)
eraseHistory = True
# Convert the Hessian back into the new coordinate system
self.H = self.molsys.hessian_to_internals(Hx, gx)
self.erase_hessian = "stashed"
elif self.params.dynamic_level == self.params.dynamic_lvl_max:
logger.critical(
"\n\t Current algorithm/dynamic_level is %d.\n" % self.params.dynamic_level
)
logger.critical("\n\t Alternative approaches are not available or turned on.\n")
raise OptError("Maximum dynamic_level reached.")
else:
self.params.dynamic_level += 1
logger.warning(
"\n\t Increasing dynamic_level algorithm to %d.\n" % self.params.dynamic_level
)
logger.warning("\n\t Erasing old history, hessian, intcos.\n")
eraseIntcos = True
eraseHistory = True
self.params.update_dynamic_level_params(self.params.dynamic_level)
logger.info("Printing the parameters %s", self.params)
if eraseIntcos:
logger.warning(" Erasing coordinates.\n")
for f in self.molsys.fragments:
del f.intcos[:]
self.molsys._dimer_intcos = []
self.erase_hessian = "erased"
if eraseHistory:
logger.warning(" Erasing history.\n")
self.clear()
# If a method needs to erase the hessian. erase all intcos and start over
if isinstance(self.opt_method, IRCfollowing.IntrinsicReactionCoordinate):
self.opt_method.irc_history.recompute_all_internals(self.molsys)
# prevent overcounting steps that couldn't be completed
self.opt_method.irc_step_number -= 1
self.error = "AlgError"
[docs]
def opt_error_handler(self, error):
"""OptError indicates an unrecoverable error. Print information and trigger cleanup."""
logger.critical("\tA critical optimization-specific error has occured.")
logger.critical("\tResetting all optimization options for potential queued jobs.\n")
logger.exception("Error caught:" + str(error))
# Dump histories if possible
return self._exception_cleanup(error)
[docs]
def unknown_error_handler(self, error):
"""Unknown errors are not recoverable error. Print information and trigger cleanup."""
logger.critical("\tA non-optimization-specific error has occurred.\n")
logger.critical("\tResetting all optimization options for potential queued jobs.\n")
logger.exception("Error Type: " + str(type(error)))
logger.exception("Error caught:" + str(error))
return self._exception_cleanup(error)
def _exception_cleanup(self, error):
logger.info(
"\tDumping history: Warning last point not converged.\n %s",
self.history.summary_string(),
)
if self.params.opt_type == "IRC":
logging.debug("\tDumping IRC points completed")
self.opt_method.irc_history.progress_report()
self.error = "OptError"
return self.finish(error)
[docs]
def finish(self, error=None):
rxnpath = None
if self.params.opt_type == "IRC":
self.opt_method.irc_history.progress_report()
rxnpath = self.opt_method.irc_history.rxnpath_dict()
else:
logger.info("\tOptimization Finished\n" + self.history.summary_string())
qc_output = prepare_opt_output(self.molsys, self.computer, rxnpath=rxnpath, error=error, history=self.history)
self.clear()
if self.params.write_trajectory:
if self.params.opt_type == "IRC":
misc.write_irc_xyz_trajectory(qc_output)
else:
misc.write_opt_xyz_trajectory(qc_output)
return qc_output
[docs]
def optimization_factory(method, molsys, history_object, params=None):
"""create optimization algorithms. method may be redundant however params is allowed
to be none so method is required explicitly
Returns
-------
OptimizationAlgorithm"""
ALGORITHMS = {
"RFO": stepAlgorithms.RestrictedStepRFO,
"P_RFO": stepAlgorithms.PartitionedRFO,
"NR": stepAlgorithms.QuasiNewtonRaphson,
"SD": stepAlgorithms.SteepestDescent,
"IRC": IRCfollowing.IntrinsicReactionCoordinate,
"CONJUGATE": stepAlgorithms.ConjugateGradient,
"RS_I_RFO": stepAlgorithms.ImageRFO,
}
return ALGORITHMS.get(method, stepAlgorithms.RestrictedStepRFO)(molsys, history_object, params)
[docs]
def get_pes_info(
H: np.ndarray,
computer: ComputeWrapper,
o_molsys: Molsys,
opt_history: history.History,
params: op.OptParams,
hessian_protocol="update",
requires=("energy", "gradient"),
):
"""Calculate, update, or guess hessian as appropriate. Calculate gradient and transform to the
current coordinate system, pulls the gradient from hessian output if possible.
Parameters
----------
H: np.ndarray
current Hessian
computer : compute_wrappers.ComputeWrapper
o_molsys : molsys.Molsys
opt_history: history.History
params : op.OptParams
requires : list
("energy", "gradient", "hessian")
hessian_protocol : str
one of ("unneeded", "compute", "guess", "update")
Returns
-------
np.ndarray
hessian matrix (inteneral coordinates)
np.ndarray:
gradient vector (internal coordinates)
np.ndarrray
gradient vector (cartesian coordinates)
float
energy
Notes
-----
Some functionality from start_step has now been placed here. external forces are added into the forces.
Redundancies and constraints are projected out of the forces and hessian.
"""
if "gradient" in requires:
driver = "gradient"
else:
driver = "energy"
if hessian_protocol == "update":
# For update. Compute new gradient. Update with external forces if present. Update the hessian
logger.debug(f"Updating Hessian with {str(params.hess_update)}")
result = computer.compute(o_molsys.geom, driver=driver, return_full=False)
g_x = np.asarray(result) if driver == "gradient" else None
f_q = o_molsys.gradient_to_internals(g_x, -1.0)
f_q, H = o_molsys.apply_external_forces(f_q, H)
H = opt_history.hessian_update(H, f_q, o_molsys)
else:
if hessian_protocol == "compute" and not params.cart_hess_read:
logger.debug("Computing Hessian")
H, g_x = get_hess_grad(computer, o_molsys) # get gradient from hessian
elif hessian_protocol in ["guess", "unneeded"]:
# guess hessian compute gradient
logger.debug(f"Guessing Hessian with {str(params.intrafrag_hess)}")
H = hessian.guess(o_molsys, guessType=params.intrafrag_hess)
result = computer.compute(o_molsys.geom, driver=driver, return_full=False)
g_x = np.asarray(result) if driver == "gradient" else None
elif params.cart_hess_read:
# read hessian from file. calculate gradient. Update params to not read from disk again
logger.debug("Reading hessian from file")
result = computer.compute(o_molsys.geom, driver=driver, return_full=False)
g_x = np.asarray(result) if driver == "gradient" else None
Hx = hessian.from_file(params.hessian_file)
H = o_molsys.hessian_to_internals(Hx)
params.cart_hess_read = False
params.hessian_file = pathlib.Path(".")
else:
raise OptError("Encountered unknown value from get_hessian_protocol()")
# Handle external forces (not included in hessian currently)
f_q = o_molsys.gradient_to_internals(g_x, -1)
f_q, H = o_molsys.apply_external_forces(f_q, H)
f_q, H = o_molsys.project_redundancies_and_constraints(f_q, H)
g_q = -f_q
# Remove incredibly small values. This primarly helps makes tests more consistent.
# eigensolvers give consistent eigenvectors.
g_q[np.abs(g_q) < np.finfo(float).resolution] = 0
H[np.abs(H) < np.finfo(float).resolution] = 0
if H.size:
logger.info(print_mat_string(H, title="Hessian matrix"))
return H, g_q, g_x, computer.energies[-1]
[docs]
def get_hess_grad(computer, o_molsys):
"""Compute hessian and fetch gradient from output if possible. Perform separate gradient
calculation if needed
Parameters
----------
computer: compute_wrappers.ComputeWrapper
o_molsys: molsys.Molsys
Returns
-------
tuple(np.ndarray, np.ndarray)
Notes
-----
Hessian is in internals gradient is in cartesian
"""
ret = computer.compute(o_molsys.geom, driver="hessian", return_full=True, print_result=False)
h_cart = np.asarray(ret["return_result"]).reshape(o_molsys.geom.size, o_molsys.geom.size)
try:
logger.debug("Looking for gradient in hessian output")
g_cart = ret["extras"]["qcvars"]["CURRENT GRADIENT"]
except KeyError:
logger.error("Could not find the gradient in qcschema")
grad = computer.compute(o_molsys.geom, driver="gradient", return_full=False)
g_cart = np.asarray(grad)
# Likely not at stationary point. Include forces
# ADDENDUM currently neglects forces term for all points - including non-stationary
H = o_molsys.hessian_to_internals(h_cart)
return H, g_cart
[docs]
def make_internal_coords(o_molsys: Molsys, params: op.OptParams):
"""
Add optimization coordinates to molecule system.
May be called if coordinates have not been added yet, or have been removed due to an
algorithm error (bend going linear, or energy increasing, etc.).
Parameters
----------
o_molsys: Molsys
current molecular system.
params: op.OptParams
Returns
-------
o_molsys: Molsys
The molecular system updated with internal coordinates.
"""
# if params is None:
# params = op.Params
logger.debug("\t Adding internal coordinates to molecular system")
# Use covalent radii to determine bond connectivity.
connectivity = addIntcos.connectivity_from_distances(
o_molsys.geom, o_molsys.Z, params.covalent_connect
)
logger.debug("Connectivity Matrix\n" + print_mat_string(connectivity))
if params.frag_mode == "SINGLE":
try:
# Make a single, supermolecule.
o_molsys.consolidate_fragments() # collapse into one frag (if > 1)
o_molsys.split_fragments_by_connectivity() # separate by connectivity
# increase connectivity until all atoms are connected
o_molsys.augment_connectivity_to_single_fragment(connectivity)
o_molsys.consolidate_fragments() # collapse into one frag
if params.opt_coordinates in ["INTERNAL", "REDUNDANT", "BOTH"]:
o_molsys.fragments[0].add_intcos_from_connectivity(connectivity)
if params.add_auxiliary_bonds:
o_molsys.fragments[0].add_auxiliary_bonds(connectivity)
except AlgError as error:
o_molsys.fragments[0]._intcos = []
if error.oofp_failures or error.linear_bends or error.linear_torsions:
params.opt_coordinates = "CARTESIAN"
if params.opt_coordinates in ["CARTESIAN", "BOTH"]:
o_molsys.fragments[0].add_cartesian_intcos()
elif params.frag_mode == "MULTI":
try:
# if provided multiple frags, then we use these.
# if not, then split them (if not connected).
if o_molsys.nfragments == 1:
o_molsys.split_fragments_by_connectivity()
if o_molsys.nfragments > 1:
addIntcos.add_dimer_frag_intcos(o_molsys, params)
# remove connectivity so that we don't add redundant coordinates
# between fragments
o_molsys.purge_interfragment_connectivity(connectivity)
if params.opt_coordinates in ["INTERNAL", "REDUNDANT", "BOTH"]:
for iF, F in enumerate(o_molsys.fragments):
C = np.ndarray((F.natom, F.natom))
C[:] = connectivity[o_molsys.frag_atom_slice(iF), o_molsys.frag_atom_slice(iF)]
F.add_intcos_from_connectivity(C)
if params.add_auxiliary_bonds:
F.add_auxiliary_bonds(connectivity)
except AlgError as error:
if error.oofp_failures or error.linear_bends or error.linear_torsions:
for frag in o_molsys.fragments:
frag._intcos = []
params.opt_coordinates = "CARTESIAN"
if params.opt_coordinates in ["CARTESIAN", "BOTH"]:
for F in o_molsys.fragments:
F.add_cartesian_intcos()
addIntcos.add_constrained_intcos(o_molsys, params) # make sure these are in the set
return
[docs]
def prepare_opt_output(o_molsys, computer, rxnpath=[], error=None, history=None):
logger.info("Preparing OptimizationResult")
# Get molecule from most recent step. Add provenance and fill in non-required fills.
# Turn back to dict
computer.update_geometry(o_molsys.geom)
final_molecule = computer.molecule
if computer.dtype == 1:
qc_output = {
"schema_name": "qcschema_optimization_output",
"trajectory": computer.trajectory,
"energies": computer.energies,
"final_molecule": final_molecule,
"extras": {},
"success": True,
}
elif computer.dtype == 2:
import optking
final_props = {} if history is None else history.summary(printoption=False)[-1]
qc_output = {
"schema_name": "qcschema_optimization_result",
# RAK/AH, please check sourcing for all these properties please
"properties": {} if len(computer.energies) == 0 else {
"return_energy": computer.energies[-1],
"return_gradient": computer.trajectory[-1]["return_result"],
"optimization_iterations": len(computer.energies),
"nuclear_repulsion_energy": o_molsys.to_schema(2).nuclear_repulsion_energy(),
"final_max_force": final_props["max_force"],
"final_rms_force": final_props["rms_force"],
"final_max_displacement": final_props["max_disp"],
"final_rms_displacement": final_props["rms_disp"],
# TODO add "final_rms_force": computer.params._i_rms_force}, etc.
},
"trajectory_results": computer.trajectory,
"trajectory_properties": [r["properties"] for r in computer.trajectory],
"final_molecule": final_molecule,
"extras": {},
"success": True,
"provenance": optking._optking_provenance_stamp,
}
if isinstance(error, OptError):
qc_output.update(
{
"success": False,
"error": {"error_type": error.err_type, "error_message": error.mesg},
}
)
elif error:
qc_output.update(
{
"success": False,
"error": {"error_type": str(type(error)), "error_message": str(error)}
}
)
if rxnpath:
qc_output["extras"]["irc_rxn_path"] = rxnpath
if rxnpath[-2]:
qc_output["final_geometry"] = rxnpath[-2]["x"]
qc_output["extras"]["final_irc_energy"] = rxnpath[-2]["energy"]
else:
qc_output.update({
"success": False,
"error": {
"error_type": "OptError",
"error": """IRC calculation has terminated before taking a single step. Please
ensure that the starting point is a transition state at the level of theory
being utilized. If issues continue, decrease ``irc_convergence`` and/or
``irc_step_size``"""}
}
)
return qc_output