Source code for optking.optwrapper

# wrapper for optking's optimize function for input by psi4API
# creates a moleuclar system from psi4s and generates optkings options from psi4's lsit of options
import copy
import inspect
import json
import logging
import pprint

import qcelemental
from qcelemental.util.serialization import json_dumps
from pydantic import ValidationError
from pydantic.v1.error_wrappers import ValidationError as v1ValidationError

import optking

from . import caseInsensitiveDict, molsys
from .compute_wrappers import ComputeWrapper, Psi4Computer, QCEngineComputer, UserComputer
from .exceptions import OptError
from .optimize import optimize
from .printTools import welcome
from . import log_name
from . import op

logger = logging.getLogger(f"{log_name}{__name__}")


[docs] def optimize_psi4(calc_name, program="psi4", dertype=None, **xtra_opt_params): """Wrapper for optimize.optimize() Looks for an active psi4 molecule and optimizes. This is the written warning that Optking will try to use psi4 if no program is provided Parameters ---------- calc_name: str level of theory for optimization. eg MP2 program: str program used for gradients, hessians... dertype: ? hack to try to get finite differences working in psi4 xtra_opt_params: dictionary extra keywords currently forbidden by psi4's read_options, but supported by optking Returns ------- opt_output: dict dictionary serialized MolSSI OptimizationResult. If Psi4 supports QCSchema v2 (~v1.11 Spring 2026), *opt_output* will be v2: https://molssi.github.io/QCElemental/next/model_opt.html . Otherwise, *opt_output* will be v1: https://molssi.github.io/QCElemental/dev/api/qcelemental.models.OptimizationInput.html#qcelemental.models.OptimizationInput . """ opt_input = {} opt_output = {} try: op.Params, oMolsys, computer, opt_input = initialize_from_psi4( calc_name, program, computer_type="psi4", dertype=dertype, **xtra_opt_params ) opt_output = optimize(oMolsys, computer) except (ValidationError, v1ValidationError) as error: logger.critical("A ValidationError has occured: %s", error, exc_info=True) opt_output = { "success": False, "error": {"error_type": "ValidationError", "error_message": str(error)} } except TypeError as error: logger.critical("A TypeError has occured: %s", error, exc_info=True) logger.critical("This TypeError is likely related to option validation.") opt_output = { "success": False, "error": {"error_type": "ValidationError", "error_message": str(error)} } except Exception as error: logger.critical( "A critical exception has occured:\n%s - %s", type(error), str(error), exc_info=True ) opt_output = { "success": False, "error": {"error_type": type(error), "error_message": str(error)}, } finally: if computer.dtype == 1: opt_input.update({"provenance": optking._optking_provenance_stamp}) opt_input["provenance"]["routine"] = "optimize_psi4" opt_input.update(opt_output) return opt_input elif computer.dtype == 2: opt_output["input_data"] = opt_input opt_output["provenance"]["routine"] = "optimize_psi4" return opt_output
[docs] def initialize_from_psi4(calc_name, program, computer_type, dertype=None, **xtra_opt_params): """Gathers information from an active psi4 instance. to cleanly run optking from a psithon or psi4api input file Parameters ---------- calc_name: str computer_type: str dertype: Union[int, None] program: str **xtra_opt_params extra keywords which are not recognized by psi4 Returns ------- params: op.OptParams o_molsys: molsys.Molsys computer: ComputeWrapper opt_input: qcelemental.models.OptimizationInput """ import psi4 # Get Molecule and Options from Psi4 as QCSchema logger.debug("Setting up optimization from Psi4's current state: p4util.state_to_atomicinput") psi4_can_v2 = "dtype" in inspect.signature(psi4.driver.p4util.state_to_atomicinput).parameters # PR 3341 pre-v1.11 if psi4_can_v2: full_atomic_input = psi4.driver.p4util.state_to_atomicinput( dtype=2, driver="gradient", method=calc_name, ).model_dump() atomic_input = full_atomic_input["specification"] else: atomic_input = psi4.driver.p4util.state_to_atomicinput( driver="gradient", method=calc_name, ).dict() opt_keys = {} # QCEngine doesn't expect information to already have been validated if psi4_can_v2: o_molsys = molsys.Molsys.from_schema(full_atomic_input.get("molecule")) atomic_input["program"] = program else: atomic_input.pop("id") atomic_input.pop("provenance") atomic_input.pop("protocols") o_molsys = molsys.Molsys.from_schema(atomic_input.get("molecule")) opt_keys["program"] = program optking_canon = op.OptParams().to_dict(by_alias=True).keys() # Psi4 options can get mixed in with optking's options in prepare_options_for_module anyway. # Atomic_input will contain all set options so only accept options that are present in # options dict. Assume everything else is for Psi4. for opt, optval in atomic_input.get("keywords").items(): if opt.upper() in optking_canon: opt_keys[opt] = optval if xtra_opt_params: for xtra_key, xtra_value in xtra_opt_params.items(): opt_keys[xtra_key] = xtra_value # Make a qcSchema OptimizationInput if psi4_can_v2: opt_input = { "initial_molecule": full_atomic_input.pop("molecule"), "specification": { "specification": atomic_input, "keywords": opt_keys, }, } else: opt_input = { "keywords": opt_keys, "initial_molecule": atomic_input.pop("molecule"), "input_specification": atomic_input, } # in case user has selected a specific dertype if dertype: if psi4_can_v2: opt_input.get("specification").get("specification").get("keywords").update("dertype", dertype) else: opt_input.get("input_specification").get("keywords").update("dertype", dertype) # any Psi4 that is v2-capable requires QCElemental >=0.50 where QCSchema v2 is RTG if psi4_can_v2: from qcelemental.models.v2 import OptimizationInput else: from qcelemental.models import OptimizationInput try: logger.debug("Creating OptimizationInput") opt_input = OptimizationInput(**opt_input) except (ValidationError, v1ValidationError) as error: logger.critical("A Validation Error has occured while initializing optking: %s", error) logger.critical("Could not create an OptimizationInput") logger.critical( """Note: `optimize_psi4` is not recommended for users. Consider calling `psi4.optimize` or see `tests/test_opthelper` for an example of using the OptHelper interface.""" ) # Remove numpy elements that can appear from psi4 to allow at will json serialization # Json cannot handle numpy types only python builtins opt_input = json.loads(json_dumps(opt_input)) params = initialize_options(opt_keys) computer = make_computer(opt_input, computer_type) return params, o_molsys, computer, opt_input
[docs] def optimize_qcengine(opt_input, computer_type="qc"): """Try to optimize, find TS, or find IRC of the system as specifed by a QCSchema OptimizationInput. Parameters ---------- opt_input: Union[OptimizationInput, dict] Pydantic Schema of the OptimizationInput model. see https://github.com/MolSSI/QCElemental/blob/master/qcelemental/models/procedures.py computer_type: str Returns ------- dict """ try: allowed = (qcelemental.models.v1.OptimizationInput, qcelemental.models.v2.OptimizationInput) except AttributeError: allowed = (qcelemental.models.OptimizationInput) if isinstance(opt_input, allowed): # Remove numpy elements and turn into dictionary opt_input = json.loads(json_dumps(opt_input)) opt_output = copy.deepcopy(opt_input) dtype = 2 if "specification" in opt_output.keys() else 1 # Make basic optking molecular system oMolsys = molsys.Molsys.from_schema(opt_input["initial_molecule"]) try: if dtype == 1: initialize_options(opt_input["keywords"]) elif dtype == 2: initialize_options(opt_input["specification"]["keywords"]) computer = make_computer(opt_input, computer_type) opt_output = optimize(oMolsys, computer) except Exception as error: logger.critical("A critical error has occured: %s - %s", type(error), error, exc_info=True) opt_output = { "success": False, "error": {"error_type": type(error), "error_message": str(error)}, } logger.critical(f"Error placed in qcschema: {opt_output}") finally: if dtype == 1: opt_input.update(opt_output) opt_input.update({"provenance": optking._optking_provenance_stamp}) opt_input["provenance"]["routine"] = "optimize_qcengine" return opt_input elif dtype == 2: opt_output["input_data"] = opt_input opt_output["provenance"] = optking._optking_provenance_stamp opt_output["provenance"]["routine"] = "optimize_qcengine" return opt_output
# QCEngine.procedures.optking.py takes 'output_data', unpacks and creates Optimization Schema # from qcel.models.v1.procedures.py or qcel.models.v2.optimization.py
[docs] def make_computer(opt_input: dict, computer_type): logger.debug("Creating a Compute Wrapper") program = op.Params.program # This gets updated so it shouldn't be a reference molecule = copy.deepcopy(opt_input["initial_molecule"]) schver = 2 if "specification" in opt_input.keys() else 1 # Sorting by spec_schema_name isn't foolproof b/c opt_input might not be a # constructed model at this point if it's not arriving through QCEngine. if schver == 1: spec_schema_name = opt_input["input_specification"].get("schema_name", "qcschema_input") if spec_schema_name == "qcschema_manybodyspecification": # route is defunct model = "(proc_spec_in_options)" options = opt_input["input_specification"] protocols = {} else: qc_input = opt_input["input_specification"] options = qc_input["keywords"] model = qc_input["model"] protocols = {} # no field for protocols in v1 QCInputSpecification elif schver == 2: spec_schema_name = opt_input["specification"]["specification"].get("schema_name", "qcschema_atomic_specification") if spec_schema_name == "qcschema_many_body_specification": model = "(proc_spec_in_options)" options = opt_input["specification"]["specification"] if "model" in (qc := options["specification"]): # single spec protocols = qc.get("protocols", {}) else: # mapping spec protocols = next(iter(qc.values())).get("protocols", {}) program = opt_input["specification"]["specification"].get("program") or "qcmanybody" else: qc_input = opt_input["specification"]["specification"] options = qc_input["keywords"] model = qc_input["model"] protocols = qc_input.get("protocols", {}) program = qc_input.get("program", program) if computer_type == "psi4": # Please note that program is not actually used here return Psi4Computer(molecule, model, options, program, protocols, dtype=schver) elif computer_type == "qc": return QCEngineComputer(molecule, model, options, program, protocols, dtype=schver) elif computer_type == "user": logger.info("Creating a UserComputer") return UserComputer(molecule, model, options, program, protocols, dtype=schver) else: raise OptError("computer_type is unknown")
[docs] def initialize_options(opt_keys, silent=False): if not silent: logger.info(welcome()) # Turn keys but not vals to upper. Optparams will handle lowercase values opt_keys = {key.upper(): val for key, val in opt_keys.items()} # Create full list of parameters from user options plus defaults. try: params = op.OptParams(**opt_keys) except (KeyError, ValueError, AttributeError, ValidationError) as e: error_key = '' if isinstance(e, ValidationError): for key in opt_keys.keys(): if key in str(e): error_key = key logger.critical("\nEncountered an %s error parsing options %s", type(e), e) logger.critical( "\nFor valdidation errors please check the documenation to find an example of the desired\n" "keyword being uitilized: https://optking.readthedocs.io/en/latest/options.html#options\n" ) if error_key: raise OptError( f"unable to parse user provided options. \n{e}" f"\n{error_key}:\t{opt_keys[error_key]}" ) from e else: raise OptError(f"unable to parse provided options. \n{e}") from e # TODO we should make this just be a normal object # we should return it to the optimize method if not silent: logger.info(params) op.Params = params return params