Source code for openff.toolkit.utils.base_wrapper

"""
Base class for toolkit wrappers. Defines the public API and some shared methods
"""

__all__ = ("ToolkitWrapper",)

from functools import wraps
from typing import TYPE_CHECKING, Optional, TypedDict

from openff.toolkit.utils.constants import DEFAULT_AROMATICITY_MODEL
from openff.toolkit.utils.exceptions import (
    IncorrectNumConformersError,
    IncorrectNumConformersWarning,
    ToolkitUnavailableException,
)

if TYPE_CHECKING:
    from openff.toolkit.topology.molecule import Molecule


class _ChargeSettings(TypedDict, total=False):
    n_conformers: int
    rec_confs: int
    min_confs: int
    max_confs: int  # OpenEyeToolkitWrapper sometimes defines this as None
    antechamber_keyword: str
    oe_charge_method: str


def _mol_to_ctab_and_aro_key(
    self, molecule: "Molecule", aromaticity_model=DEFAULT_AROMATICITY_MODEL
) -> str:
    return f"{molecule.ordered_connection_table_hash()}-{aromaticity_model}"


[docs]class ToolkitWrapper: """ Toolkit wrapper base class. .. warning :: This API is experimental and subject to change. """ _is_available: Optional[bool] = None # True if toolkit is available _toolkit_version: Optional[str] = None _toolkit_name: Optional[str] = None # Name of the toolkit _toolkit_installation_instructions: Optional[str] = ( None # Installation instructions for the toolkit ) _supported_charge_methods: dict[str, _ChargeSettings] = dict() _toolkit_file_read_formats: list[str] = list() _toolkit_file_write_formats: list[str] = list() # @staticmethod # TODO: Right now, to access the class definition, I have to make this a classmethod # and thereby call it with () on the outermost decorator. Is this wasting time? Are we caching # the is_available results? @classmethod def requires_toolkit(cls): # remember cls is a ToolkitWrapper subclass here def decorator(func): @wraps(func) def wrapped_function(*args, **kwargs): if not cls.is_available(): msg = "This function requires the {} toolkit".format( cls._toolkit_name ) raise ToolkitUnavailableException(msg) value = func(*args, **kwargs) return value return wrapped_function return decorator @property def toolkit_name(self): """ Return the name of the toolkit wrapped by this class as a str .. warning :: This API is experimental and subject to change. Returns ------- toolkit_name The name of the wrapped toolkit """ return self.__class__._toolkit_name @property def toolkit_installation_instructions(self): """ Instructions on how to install the wrapped toolkit. """ return self._toolkit_installation_instructions @property def toolkit_file_read_formats(self): """ List of file formats that this toolkit can read. """ return self._toolkit_file_read_formats @property def toolkit_file_write_formats(self) -> list[str]: """ List of file formats that this toolkit can write. """ return self._toolkit_file_write_formats @property def supported_charge_methods(self) -> list[str]: return [*self._supported_charge_methods.keys()]
[docs] @classmethod def is_available(cls): """ Check whether the corresponding toolkit can be imported Returns ------- is_installed True if corresponding toolkit is installed, False otherwise. """ return NotImplementedError
@property def toolkit_version(self): """ Return the version of the wrapped toolkit as a str .. warning :: This API is experimental and subject to change. Returns ------- toolkit_version The version of the wrapped toolkit """ return self._toolkit_version
[docs] def from_file(self, file_path, file_format, allow_undefined_stereo=False): """ Return an openff.toolkit.topology.Molecule from a file using this toolkit. Parameters ---------- file_path The file to read the molecule from file_format Format specifier, usually file suffix (eg. 'MOL2', 'SMI') Note that not all toolkits support all formats. Check ToolkitWrapper.toolkit_file_read_formats for details. allow_undefined_stereo If false, raises an exception if any molecules contain undefined stereochemistry. _cls Molecule constructor Returns ------- molecules a list of Molecule objects is returned. """ return NotImplementedError
[docs] def from_file_obj( self, file_obj, file_format, allow_undefined_stereo=False, _cls=None ): """ Return an openff.toolkit.topology.Molecule from a file-like object (an object with a ".read()" method using this toolkit. Parameters ---------- file_obj The file-like object to read the molecule from file_format Format specifier, usually file suffix (eg. 'MOL2', 'SMI') Note that not all toolkits support all formats. Check ToolkitWrapper.toolkit_file_read_formats for details. allow_undefined_stereo If false, raises an exception if any molecules contain undefined stereochemistry. If false, the function skips loading the molecule. _cls Molecule constructor Returns ------- molecules a list of Molecule objects is returned. """ return NotImplementedError
def _check_n_conformers( self, molecule: "Molecule", partial_charge_method: Optional[str] = None, min_confs: Optional[int] = None, max_confs: Optional[int] = None, strict_n_conformers: bool = False, ): """ Private method for validating the number of conformers on a molecule prior to partial charge calculation Parameters ---------- molecule Molecule for which partial charges are to be computed partial_charge_method The name of the charge method being used min_confs The minimum number of conformers required to use this charge method max_confs The maximum number of conformers required to use this charge method strict_n_conformers Whether to raise an exception if an invalid number of conformers is provided. If this is False and an invalid number of conformers is found, a warning will be raised. Raises ------ IncorrectNumConformersError If the wrong number of conformers is attached to the input molecule, and strict_n_conformers is True. """ import warnings n_confs = molecule.n_conformers wrong_confs_msg = ( f"Molecule '{molecule}' has {n_confs} conformers, " f"but charge method '{partial_charge_method}' expects" ) exception_suffix = ( "You can disable this error by setting `strict_n_conformers=False' " "when calling 'molecule.assign_partial_charges'." ) # If there's no n_confs filter, then this molecule automatically passes if min_confs is None and max_confs is None: return # If there's constraints on both ends, check both limits elif min_confs is not None and max_confs is not None: if not (min_confs <= n_confs <= max_confs): if min_confs == max_confs: wrong_confs_msg += f" exactly {min_confs}." else: wrong_confs_msg += f" between {min_confs} and {max_confs}." else: return # If there's only a max constraint, check that elif min_confs is not None and max_confs is None: if not (min_confs <= n_confs): wrong_confs_msg += f" at least {min_confs}." else: return # If there's only a maximum constraint, check that elif min_confs is None and max_confs is not None: if not (n_confs <= max_confs): wrong_confs_msg += f" at most {max_confs}." else: return # If we've made it this far, the molecule has the wrong number of conformers if strict_n_conformers: wrong_confs_msg += exception_suffix raise IncorrectNumConformersError(wrong_confs_msg) else: warnings.warn(wrong_confs_msg, IncorrectNumConformersWarning, stacklevel=2) def __repr__(self): return ( f"ToolkitWrapper around {self.toolkit_name} version {self.toolkit_version}" )