Compare 3195e6f ... +39 ... 19f64ab

No flags found

Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.

e.g., #unittest #integration

#production #enterprise

#frontend #backend

Learn more about Codecov Flags here.


@@ -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,37 +2397,41 @@
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
2424 2430
        #       and speed matching for larger molecules.
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
# =============================================================================================

@@ -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,22 +1124,27 @@
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
        """
1124 1131
        if box_vectors is None:
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

Click to load this diff.
Loading diff...

Learn more Showing 2 files with coverage changes found.

Changes in openforcefield/utils/toolkits.py
-7
+7
Loading file...
Changes in openforcefield/topology/topology.py
-2
+2
Loading file...

41 Commits

+5
+5
+3
+9
-6
+5
+6
-1
+3
+3
+22
+22
-22
-22
Hiding 1 contexual commits
+22
+16
+6
-2
+3
-5
+2
-3
+5
+1
+6
-5
+1
-1
-23
-23
Hiding 1 contexual commits
Hiding 2 contexual commits
+18 Files
+5932
+5125
+807
Hiding 1 contexual commits Hiding 1 contexual commits Hiding 2 contexual commits
-19 Files
-6145
-5233
-912
+2
+1058
-1056
+6
+6
+1 Files
+189
-973
+1162
Files Coverage
openforcefield 86.55%
Project Totals (18 files) 86.55%
Loading