@@ -13,6 +13,7 @@
Loading
13 13
from openforcefield.topology.topology import (
14 14
    DuplicateUniqueMoleculeError,
15 15
    ImproperDict,
16 +
    InvalidBoxVectorsError,
16 17
    NotBondedError,
17 18
    SortedDict,
18 19
    Topology,

@@ -38,6 +38,7 @@
Loading
38 38
    "OpenEyeToolkitWrapper",
39 39
    "RDKitToolkitWrapper",
40 40
    "AmberToolsToolkitWrapper",
41 +
    "BuiltInToolkitWrapper",
41 42
    "ToolkitRegistry",
42 43
    "GLOBAL_TOOLKIT_REGISTRY",
43 44
    "OPENEYE_AVAILABLE",
@@ -2361,7 +2362,9 @@
Loading
2361 2362
        return tuple(unique_tags), tuple(connections)
2362 2363
2363 2364
    @staticmethod
2364 -
    def _find_smarts_matches(oemol, smarts, aromaticity_model=None):
2365 +
    def _find_smarts_matches(
2366 +
        oemol, smarts, aromaticity_model=DEFAULT_AROMATICITY_MODEL
2367 +
    ):
2365 2368
        """Find all sets of atoms in the provided OpenEye molecule that match the provided SMARTS string.
2366 2369
2367 2370
        Parameters
@@ -2373,7 +2376,7 @@
Loading
2373 2376
            If there are N tagged atoms numbered 1..N, the resulting matches will be N-tuples of atoms that match the corresponding tagged atoms.
2374 2377
        aromaticity_model : str, optional, default=None
2375 2378
            OpenEye aromaticity model designation as a string, such as ``OEAroModel_MDL``.
2376 -
            If ``None``, molecule is processed exactly as provided; otherwise it is prepared with this aromaticity model prior to querying.
2379 +
            Molecule is prepared with this aromaticity model prior to querying.
2377 2380
2378 2381
        Returns
2379 2382
        -------
@@ -2394,30 +2397,33 @@
Loading
2394 2397
2395 2398
        # Make a copy of molecule so we don't influence original (probably safer than deepcopy per C Bayly)
2396 2399
        mol = oechem.OEMol(oemol)
2397 -
2398 2400
        # Set up query
2399 2401
        qmol = oechem.OEQMol()
2400 2402
        if not oechem.OEParseSmarts(qmol, smarts):
2401 2403
            raise ValueError(f"Error parsing SMARTS '{smarts}'")
2402 2404
2403 -
        # Determine aromaticity model
2404 -
        if aromaticity_model:
2405 -
            if type(aromaticity_model) == str:
2406 -
                # Check if the user has provided a manually-specified aromaticity_model
2407 -
                if hasattr(oechem, aromaticity_model):
2408 -
                    oearomodel = getattr(oechem, "OEAroModel_" + aromaticity_model)
2409 -
                else:
2410 -
                    raise ValueError(
2411 -
                        "Error: provided aromaticity model not recognized by oechem."
2412 -
                    )
2405 +
        # Apply aromaticity model
2406 +
        if type(aromaticity_model) == str:
2407 +
            # Check if the user has provided a manually-specified aromaticity_model
2408 +
            if hasattr(oechem, aromaticity_model):
2409 +
                oearomodel = getattr(oechem, aromaticity_model)
2413 2410
            else:
2414 -
                raise ValueError("Error: provided aromaticity model must be a string.")
2411 +
                raise ValueError(
2412 +
                    "Error: provided aromaticity model not recognized by oechem."
2413 +
                )
2414 +
        else:
2415 +
            raise ValueError("Error: provided aromaticity model must be a string.")
2416 +
2417 +
        # OEPrepareSearch will clobber our desired aromaticity model if we don't sync up mol and qmol ahead of time
2418 +
        # Prepare molecule
2419 +
        oechem.OEClearAromaticFlags(mol)
2420 +
        oechem.OEAssignAromaticFlags(mol, oearomodel)
2415 2421
2416 -
            # If aromaticity model was provided, prepare molecule
2417 -
            oechem.OEClearAromaticFlags(mol)
2418 -
            oechem.OEAssignAromaticFlags(mol, oearomodel)
2419 -
            # Avoid running OEPrepareSearch or we lose desired aromaticity, so instead:
2420 -
            oechem.OEAssignHybridization(mol)
2422 +
        # If aromaticity model was provided, prepare query molecule
2423 +
        oechem.OEClearAromaticFlags(qmol)
2424 +
        oechem.OEAssignAromaticFlags(qmol, oearomodel)
2425 +
        oechem.OEAssignHybridization(mol)
2426 +
        oechem.OEAssignHybridization(qmol)
2421 2427
2422 2428
        # Build list of matches
2423 2429
        # TODO: The MoleculeImage mapping should preserve ordering of template molecule for equivalent atoms
@@ -2425,6 +2431,7 @@
Loading
2425 2431
        unique = False  # We require all matches, not just one of each kind
2426 2432
        substructure_search = OESubSearch(qmol)
2427 2433
        substructure_search.SetMaxMatches(0)
2434 +
        oechem.OEPrepareSearch(mol, substructure_search)
2428 2435
        matches = list()
2429 2436
        for match in substructure_search.Match(mol, unique):
2430 2437
            # Compile list of atom indices that match the pattern tags
@@ -2453,13 +2460,15 @@
Loading
2453 2460
        smarts : str
2454 2461
            SMARTS string with optional SMIRKS-style atom tagging
2455 2462
        aromaticity_model : str, optional, default='OEAroModel_MDL'
2456 -
            Aromaticity model to use during matching
2463 +
            Molecule is prepared with this aromaticity model prior to querying.
2457 2464
2458 2465
        .. note :: Currently, the only supported ``aromaticity_model`` is ``OEAroModel_MDL``
2459 2466
2460 2467
        """
2461 2468
        oemol = self.to_openeye(molecule)
2462 -
        return self._find_smarts_matches(oemol, smarts)
2469 +
        return self._find_smarts_matches(
2470 +
            oemol, smarts, aromaticity_model=aromaticity_model
2471 +
        )
2463 2472
2464 2473
2465 2474
class RDKitToolkitWrapper(ToolkitWrapper):
@@ -3746,7 +3755,7 @@
Loading
3746 3755
            If there are N tagged atoms numbered 1..N, the resulting matches will be N-tuples of atoms that match the corresponding tagged atoms.
3747 3756
        aromaticity_model : str, optional, default='OEAroModel_MDL'
3748 3757
            OpenEye aromaticity model designation as a string, such as ``OEAroModel_MDL``.
3749 -
            If ``None``, molecule is processed exactly as provided; otherwise it is prepared with this aromaticity model prior to querying.
3758 +
            Molecule is prepared with this aromaticity model prior to querying.
3750 3759
3751 3760
        Returns
3752 3761
        -------
@@ -3795,7 +3804,7 @@
Loading
3795 3804
        # since the C++ signature is a uint
3796 3805
        max_matches = np.iinfo(np.uintc).max
3797 3806
        for match in rdmol.GetSubstructMatches(
3798 -
            qmol, uniquify=False, maxMatches=max_matches
3807 +
            qmol, uniquify=False, maxMatches=max_matches, useChirality=True
3799 3808
        ):
3800 3809
            mas = [match[x] for x in map_list]
3801 3810
            matches.append(tuple(mas))
@@ -3815,7 +3824,7 @@
Loading
3815 3824
        smarts : str
3816 3825
            SMARTS string with optional SMIRKS-style atom tagging
3817 3826
        aromaticity_model : str, optional, default='OEAroModel_MDL'
3818 -
            Aromaticity model to use during matching
3827 +
            Molecule is prepared with this aromaticity model prior to querying.
3819 3828
3820 3829
        .. note :: Currently, the only supported ``aromaticity_model`` is ``OEAroModel_MDL``
3821 3830
@@ -4649,83 +4658,81 @@
Loading
4649 4658
    Register toolkits in a specified order, skipping if unavailable
4650 4659
4651 4660
    >>> from openforcefield.utils.toolkits import ToolkitRegistry
4652 -
    >>> toolkit_registry = ToolkitRegistry()
4653 4661
    >>> toolkit_precedence = [OpenEyeToolkitWrapper, RDKitToolkitWrapper, AmberToolsToolkitWrapper]
4654 -
    >>> for toolkit in toolkit_precedence:
4655 -
    ...     if toolkit.is_available():
4656 -
    ...         toolkit_registry.register_toolkit(toolkit)
4662 +
    >>> toolkit_registry = ToolkitRegistry(toolkit_precedence)
4663 +
    >>> toolkit_registry
4664 +
    ToolkitRegistry containing OpenEye Toolkit, The RDKit, AmberTools
4657 4665
4658 -
    Register specified toolkits, raising an exception if one is unavailable
4666 +
    Register all available toolkits (in the order OpenEye, RDKit, AmberTools, built-in)
4659 4667
4660 -
    >>> toolkit_registry = ToolkitRegistry()
4661 -
    >>> toolkits = [OpenEyeToolkitWrapper, AmberToolsToolkitWrapper]
4662 -
    >>> for toolkit in toolkits:
4663 -
    ...     toolkit_registry.register_toolkit(toolkit)
4664 -
4665 -
    Register all available toolkits in arbitrary order
4666 -
4667 -
    >>> from openforcefield.utils import all_subclasses
4668 -
    >>> toolkits = all_subclasses(ToolkitWrapper)
4669 -
    >>> for toolkit in toolkit_precedence:
4670 -
    ...     if toolkit.is_available():
4671 -
    ...         toolkit_registry.register_toolkit(toolkit)
4668 +
    >>> toolkits = [OpenEyeToolkitWrapper, RDKitToolkitWrapper, AmberToolsToolkitWrapper, BuiltInToolkitWrapper]
4669 +
    >>> toolkit_registry = ToolkitRegistry(toolkit_precedence=toolkits)
4670 +
    >>> toolkit_registry
4671 +
    ToolkitRegistry containing OpenEye Toolkit, The RDKit, AmberTools, Built-in Toolkit
4672 4672
4673 4673
    Retrieve the global singleton toolkit registry, which is created when this module is imported from all available
4674 4674
    toolkits:
4675 4675
4676 4676
    >>> from openforcefield.utils.toolkits import GLOBAL_TOOLKIT_REGISTRY as toolkit_registry
4677 -
    >>> available_toolkits = toolkit_registry.registered_toolkits
4677 +
    >>> toolkit_registry
4678 +
    ToolkitRegistry containing OpenEye Toolkit, The RDKit, AmberTools, Built-in Toolkit
4679 +
4680 +
    Note that this will contain different ToolkitWrapper objects based on what toolkits
4681 +
    are currently installed.
4678 4682
4679 4683
    .. warning :: This API is experimental and subject to change.
4680 4684
    """
4681 4685
4682 4686
    def __init__(
4683 4687
        self,
4684 -
        register_imported_toolkit_wrappers=False,
4685 -
        toolkit_precedence=None,
4688 +
        toolkit_precedence=[],
4686 4689
        exception_if_unavailable=True,
4690 +
        _register_imported_toolkit_wrappers=False,
4687 4691
    ):
4688 4692
        """
4689 4693
        Create an empty toolkit registry.
4690 4694
4691 4695
        Parameters
4692 4696
        ----------
4693 -
        register_imported_toolkit_wrappers : bool, optional, default=False
4694 -
            If True, will attempt to register all imported ToolkitWrapper subclasses that can be found, in no particular
4695 -
             order.
4696 -
        toolkit_precedence : list, optional, default=None
4697 +
        toolkit_precedence : list, default=[]
4697 4698
            List of toolkit wrapper classes, in order of desired precedence when performing molecule operations. If
4698 -
            None, defaults to [OpenEyeToolkitWrapper, RDKitToolkitWrapper, AmberToolsToolkitWrapper].
4699 +
            None, no toolkits will be registered.
4700 +
4699 4701
        exception_if_unavailable : bool, optional, default=True
4700 4702
            If True, an exception will be raised if the toolkit is unavailable
4701 4703
4702 -
        """
4704 +
        _register_imported_toolkit_wrappers : bool, optional, default=False
4705 +
            If True, will attempt to register all imported ToolkitWrapper subclasses that can be
4706 +
            found in the order of toolkit_precedence, if specified. If toolkit_precedence is not
4707 +
            specified, the default order is [OpenEyeToolkitWrapper, RDKitToolkitWrapper,
4708 +
            AmberToolsToolkitWrapper, BuiltInToolkitWrapper].
4703 4709
4710 +
        """
4704 4711
        self._toolkits = list()
4705 4712
4706 -
        if toolkit_precedence is None:
4707 -
            toolkit_precedence = [
4708 -
                OpenEyeToolkitWrapper,
4709 -
                RDKitToolkitWrapper,
4710 -
                AmberToolsToolkitWrapper,
4711 -
                BuiltInToolkitWrapper,
4712 -
            ]
4713 +
        toolkits_to_register = list()
4713 4714
4714 -
        if register_imported_toolkit_wrappers:
4715 -
            # TODO: The precedence ordering of any non-specified remaining wrappers will be arbitrary.
4716 -
            # How do we fix this?
4717 -
            # Note: The precedence of non-specifid wrappers may be determined by the order in which
4718 -
            # they were defined
4715 +
        if _register_imported_toolkit_wrappers:
4716 +
            if toolkit_precedence is None:
4717 +
                toolkit_precedence = [
4718 +
                    OpenEyeToolkitWrapper,
4719 +
                    RDKitToolkitWrapper,
4720 +
                    AmberToolsToolkitWrapper,
4721 +
                    BuiltInToolkitWrapper,
4722 +
                ]
4719 4723
            all_importable_toolkit_wrappers = all_subclasses(ToolkitWrapper)
4720 -
            for toolkit in all_importable_toolkit_wrappers:
4721 -
                if toolkit in toolkit_precedence:
4722 -
                    continue
4723 -
                toolkit_precedence.append(toolkit)
4724 +
            for toolkit in toolkit_precedence:
4725 +
                if toolkit in all_importable_toolkit_wrappers:
4726 +
                    toolkits_to_register.append(toolkit)
4727 +
        else:
4728 +
            if toolkit_precedence:
4729 +
                toolkits_to_register = toolkit_precedence
4724 4730
4725 -
        for toolkit in toolkit_precedence:
4726 -
            self.register_toolkit(
4727 -
                toolkit, exception_if_unavailable=exception_if_unavailable
4728 -
            )
4731 +
        if toolkits_to_register:
4732 +
            for toolkit in toolkits_to_register:
4733 +
                self.register_toolkit(
4734 +
                    toolkit, exception_if_unavailable=exception_if_unavailable
4735 +
                )
4729 4736
4730 4737
    @property
4731 4738
    def registered_toolkits(self):
@@ -4902,7 +4909,7 @@
Loading
4902 4909
4903 4910
        >>> from openforcefield.topology import Molecule
4904 4911
        >>> molecule = Molecule.from_smiles('Cc1ccccc1')
4905 -
        >>> toolkit_registry = ToolkitRegistry(register_imported_toolkit_wrappers=True)
4912 +
        >>> toolkit_registry = ToolkitRegistry([OpenEyeToolkitWrapper, RDKitToolkitWrapper, AmberToolsToolkitWrapper])
4906 4913
        >>> method = toolkit_registry.resolve('to_smiles')
4907 4914
        >>> smiles = method(molecule)
4908 4915
@@ -4959,7 +4966,7 @@
Loading
4959 4966
4960 4967
        >>> from openforcefield.topology import Molecule
4961 4968
        >>> molecule = Molecule.from_smiles('Cc1ccccc1')
4962 -
        >>> toolkit_registry = ToolkitRegistry(register_imported_toolkit_wrappers=True)
4969 +
        >>> toolkit_registry = ToolkitRegistry([OpenEyeToolkitWrapper, RDKitToolkitWrapper])
4963 4970
        >>> smiles = toolkit_registry.call('to_smiles', molecule)
4964 4971
4965 4972
        """
@@ -5004,7 +5011,13 @@
Loading
5004 5011
# Create global toolkit registry, where all available toolkits are registered
5005 5012
# TODO: Should this be all lowercase since it's not a constant?
5006 5013
GLOBAL_TOOLKIT_REGISTRY = ToolkitRegistry(
5007 -
    register_imported_toolkit_wrappers=True, exception_if_unavailable=False
5014 +
    toolkit_precedence=[
5015 +
        OpenEyeToolkitWrapper,
5016 +
        RDKitToolkitWrapper,
5017 +
        AmberToolsToolkitWrapper,
5018 +
        BuiltInToolkitWrapper,
5019 +
    ],
5020 +
    exception_if_unavailable=False,
5008 5021
)
5009 5022
5010 5023
# =============================================================================================

@@ -1825,7 +1825,7 @@
Loading
1825 1825
            raise ValueError("One of (parameter, parameter_kwargs) must be specified")
1826 1826
1827 1827
        if new_parameter.smirks in [p.smirks for p in self._parameters]:
1828 -
            msg = f"A parameter SMIRKS pattern {val} already exists."
1828 +
            msg = f"A parameter SMIRKS pattern {new_parameter.smirks} already exists."
1829 1829
            raise DuplicateParameterError(msg)
1830 1830
1831 1831
        if before is not None:

@@ -1422,3 +1422,32 @@
Loading
1422 1422
                raise KeyError(f"Parameter handler with name '{val}' not found.")
1423 1423
        elif isinstance(val, ParameterHandler) or issubclass(val, ParameterHandler):
1424 1424
            raise NotImplementedError
1425 +
1426 +
    def __hash__(self):
1427 +
        """Deterministically hash a ForceField object
1428 +
1429 +
        Notable behavior:
1430 +
          * `author` and `date` are stripped from the ForceField
1431 +
          * `id` and `parent_id` are stripped from each ParameterType"""
1432 +
1433 +
        # Completely re-constructing the force field may be overkill
1434 +
        # compared to deepcopying and modifying, but is not currently slow
1435 +
        ff_copy = ForceField()
1436 +
        ff_copy.date = None
1437 +
        ff_copy.author = None
1438 +
1439 +
        param_attrs_to_strip = ["_id", "_parent_id"]
1440 +
1441 +
        for handler_name in self.registered_parameter_handlers:
1442 +
            handler = copy.deepcopy(self.get_parameter_handler(handler_name))
1443 +
1444 +
            for param in handler._parameters:
1445 +
                for attr in param_attrs_to_strip:
1446 +
                    # param.__dict__.pop(attr, None) may be faster
1447 +
                    # https://stackoverflow.com/a/42303681/4248961
1448 +
                    if hasattr(param, attr):
1449 +
                        delattr(param, attr)
1450 +
1451 +
            ff_copy.register_parameter_handler(handler)
1452 +
1453 +
        return hash(ff_copy.to_string(discard_cosmetic_attributes=True))

@@ -25,6 +25,7 @@
Loading
25 25
from collections import OrderedDict
26 26
from collections.abc import MutableMapping
27 27
28 +
import numpy as np
28 29
from simtk import unit
29 30
from simtk.openmm import app
30 31
@@ -60,6 +61,12 @@
Loading
60 61
    pass
61 62
62 63
64 +
class InvalidBoxVectorsError(MessageException):
65 +
    """
66 +
    Exception for setting invalid box vectors
67 +
    """
68 +
69 +
63 70
# =============================================================================================
64 71
# PRIVATE SUBROUTINES
65 72
# =============================================================================================
@@ -1105,7 +1112,7 @@
Loading
1105 1112
        """Return the box vectors of the topology, if specified
1106 1113
        Returns
1107 1114
        -------
1108 -
        box_vectors : simtk.unit.Quantity wrapped numpy array
1115 +
        box_vectors : simtk.unit.Quantity wrapped numpy array of shape (3, 3)
1109 1116
            The unit-wrapped box vectors of this topology
1110 1117
        """
1111 1118
        return self._box_vectors
@@ -1117,7 +1124,7 @@
Loading
1117 1124
1118 1125
        Parameters
1119 1126
        ----------
1120 -
        box_vectors : simtk.unit.Quantity wrapped numpy array
1127 +
        box_vectors : simtk.unit.Quantity wrapped numpy array of shape (3, 3)
1121 1128
            The unit-wrapped box vectors
1122 1129
1123 1130
        """
@@ -1125,14 +1132,19 @@
Loading
1125 1132
            self._box_vectors = None
1126 1133
            return
1127 1134
        if not hasattr(box_vectors, "unit"):
1128 -
            raise ValueError("Given unitless box vectors")
1135 +
            raise InvalidBoxVectorsError("Given unitless box vectors")
1129 1136
        if not (unit.angstrom.is_compatible(box_vectors.unit)):
1130 -
            raise ValueError(
1137 +
            raise InvalidBoxVectorsError(
1131 1138
                "Attempting to set box vectors in units that are incompatible with simtk.unit.Angstrom"
1132 1139
            )
1133 1140
1134 1141
        if hasattr(box_vectors, "shape"):
1135 -
            assert box_vectors.shape == (3,)
1142 +
            if box_vectors.shape == (3,):
1143 +
                box_vectors *= np.eye(3)
1144 +
            if box_vectors.shape != (3, 3):
1145 +
                raise InvalidBoxVectorsError(
1146 +
                    f"Box vectors must be shape (3, 3). Found shape {box_vectors.shape}"
1147 +
                )
1136 1148
        else:
1137 1149
            assert len(box_vectors) == 3
1138 1150
        self._box_vectors = box_vectors

@@ -1022,3 +1022,29 @@
Loading
1022 1022
                    parameters_by_ID[pid].add(smi)
1023 1023
1024 1024
    return parameters_by_molecule, parameters_by_ID
1025 +
1026 +
1027 +
def sort_smirnoff_dict(data):
1028 +
    """
1029 +
    Recursively sort the keys in a dict of SMIRNOFF data.
1030 +
1031 +
    Adapted from https://stackoverflow.com/a/47882384/4248961
1032 +
1033 +
    TODO: Should this live elsewhere?
1034 +
    """
1035 +
    sorted_dict = dict()
1036 +
    for key, val in sorted(data.items()):
1037 +
        if isinstance(val, dict):
1038 +
            # This should hit each ParameterHandler and dicts within them
1039 +
            sorted_dict[key] = sort_smirnoff_dict(val)
1040 +
        elif isinstance(val, list):
1041 +
            # Handle case of ParameterLists, which show up in
1042 +
            # the smirnoff dicts as lists of OrderedDicts
1043 +
            new_parameter_list = list()
1044 +
            for param in val:
1045 +
                new_parameter_list.append(sort_smirnoff_dict(param))
1046 +
            sorted_dict[key] = new_parameter_list
1047 +
        else:
1048 +
            # Handle metadata or the bottom of a recursive dict
1049 +
            sorted_dict[key] = val
1050 +
    return sorted_dict
Files Coverage
openforcefield 86.55%
Project Totals (18 files) 86.55%
1
# Codecov configuration to make it a bit less noisy
2
coverage:
3
  status:
4
    patch: false
5
    project:
6
      default:
7
        threshold: 50%
8
comment:
9
  layout: "header"
10
  require_changes: false
11
  branches: null
12
  behavior: default
13
  flags: null
14
  paths: null
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading