Source code for openff.toolkit.utils.base_wrapper
"Base class for the toolkit wrappers. Defines the public API and some shared methods"
__all__ = ("ToolkitWrapper",)
# =============================================================================================
# IMPORTS
# =============================================================================================
from functools import wraps
from typing import Optional
from openff.toolkit.utils.exceptions import (
IncorrectNumConformersError,
IncorrectNumConformersWarning,
ToolkitUnavailableException,
)
# =============================================================================================
# Implementation
# =============================================================================================
[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
# @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
# @classmethod
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 : str
The name of the wrapped toolkit
"""
return self.__class__._toolkit_name
@property
# @classmethod
def toolkit_installation_instructions(self):
"""
Instructions on how to install the wrapped toolkit.
"""
return self._toolkit_installation_instructions
# @classmethod
@property
def toolkit_file_read_formats(self):
"""
List of file formats that this toolkit can read.
"""
return self._toolkit_file_read_formats
# @classmethod
@property
def toolkit_file_write_formats(self):
"""
List of file formats that this toolkit can write.
"""
return self._toolkit_file_write_formats
[docs] @classmethod
def is_available(cls):
"""
Check whether the corresponding toolkit can be imported
Returns
-------
is_installed : bool
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 : str
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 : str
The file to read the molecule from
file_format : str
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 : bool, default=False
If false, raises an exception if any molecules contain undefined stereochemistry.
_cls : class
Molecule constructor
Returns
-------
molecules : Molecule or list of 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 : file-like object
The file-like object to read the molecule from
file_format : str
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 : bool, default=False
If false, raises an exception if any molecules contain undefined stereochemistry.
If false, the function skips loading the molecule.
_cls : class
Molecule constructor
Returns
-------
molecules : Molecule or list of Molecules
a list of Molecule objects is returned.
"""
return NotImplementedError
def _check_n_conformers(
self,
molecule,
partial_charge_method,
min_confs=None,
max_confs=None,
strict_n_conformers=False,
):
"""
Private method for validating the number of conformers on a molecule prior to partial
charge calculation
Parameters
----------
molecule : Molecule
Molecule for which partial charges are to be computed
partial_charge_method : str, optional, default=None
The name of the charge method being used
min_confs : int, optional, default=None
The minimum number of conformers required to use this charge method
max_confs : int, optional, default=None
The maximum number of conformers required to use this charge method
strict_n_conformers : bool, default=False
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)
def __repr__(self):
return (
f"ToolkitWrapper around {self.toolkit_name} version {self.toolkit_version}"
)