Skip to content

Conversation

@jchodera
Copy link
Member

Fixes #252

@jchodera
Copy link
Member Author

jchodera commented Dec 1, 2022

@mattwthompson : Now that CI is re-enabled, I'm noting an interesting error that appears to be related to the new OpenFF toolkit's unit-handling scheme mis-parsing a SMARTS string it is trying to identify as a quantity.

This traceback is long, so I've made it collapsable:

Full traceback
-------------------------------- live log call ---------------------------------
ERROR    openmmforcefields.generators.template_generators:template_generators.py:1284 missing unary operator "*"
FAILED                                                                   [ 63%]

=================================== FAILURES ===================================
_______________ TestSMIRNOFFTemplateGenerator.test_add_molecules _______________

self = <openmmforcefields.generators.template_generators.SMIRNOFFTemplateGenerator object at 0x7f7be13d9580>
molecules = None, cache = None, forcefield = 'openff-2.0.0'

    def __init__(self, molecules=None, cache=None, forcefield=None):
        """
        Create a SMIRNOFFTemplateGenerator with some OpenFF toolkit molecules
    
        Requies the OpenFF toolkit: http://openforcefield.org
    
        Parameters
        ----------
        molecules : openff.toolkit.topology.Molecule or list, optional, default=None
            Can alternatively be an object (such as an OpenEye OEMol or RDKit Mol or SMILES string) that can be used to construct a Molecule.
            Can also be a list of Molecule objects or objects that can be used to construct a Molecule.
            If specified, these molecules will be recognized and parameterized with SMIRNOFF as needed.
            The parameters will be cached in case they are encountered again the future.
        cache : str, optional, default=None
            Filename for global caching of parameters.
            If specified, parameterized molecules will be stored in a TinyDB instance as a JSON file.
        forcefield : str, optional, default=None
            Name of installed SMIRNOFF force field (without .offxml) or local .offxml filename (with extension).
            If not specified, the latest Open Force Field Initiative release is used.
    
        Examples
        --------
    
        Create a SMIRNOFF template generator for a single molecule (benzene, created from SMILES) and register it with ForceField:
    
        >>> from openff.toolkit.topology import Molecule
        >>> molecule = Molecule.from_smiles('c1ccccc1')
        >>> from openmmforcefields.generators import SMIRNOFFTemplateGenerator
        >>> smirnoff = SMIRNOFFTemplateGenerator(molecules=molecule)
        >>> from openmm.app import ForceField
        >>> amber_forcefields = ['amber/protein.ff14SB.xml', 'amber/tip3p_standard.xml', 'amber/tip3p_HFE_multivalent.xml']
        >>> forcefield = ForceField(*amber_forcefields)
    
        The latest Open Force Field Initiative release is used if none is specified.
    
        >>> smirnoff.forcefield
        'openff-2.0.0'
    
        You can check which SMIRNOFF force field filename is in use with
    
        >>> smirnoff.smirnoff_filename  # doctest:+ELLIPSIS
        '/.../openff-2.0.0.offxml'
    
        Create a template generator for a specific SMIRNOFF force field for multiple
        molecules read from an SDF file:
    
        >>> molecules = Molecule.from_file('molecules.sdf')  # doctest: +SKIP
        >>> smirnoff = SMIRNOFFTemplateGenerator(molecules=molecules, forcefield='openff-2.0.0')  # doctest: +SKIP
    
        You can also add molecules later on after the generator has been registered:
    
        >>> smirnoff.add_molecules(molecules)  # doctest: +SKIP
    
        To check which SMIRNOFF versions are supported, check the `INSTALLED_FORCEFIELDS` attribute:
    
        >>> print(SMIRNOFFTemplateGenerator.INSTALLED_FORCEFIELDS)  # doctest: +SKIP
        ['openff-1.0.1', 'openff-1.1.1', 'openff-1.0.0-RC1', 'openff-1.2.0', 'openff-1.1.0', 'openff-1.0.0', 'openff-1.0.0-RC2', 'smirnoff99Frosst-1.0.2', 'smirnoff99Frosst-1.0.0', 'smirnoff99Frosst-1.1.0', 'smirnoff99Frosst-1.0.4', 'smirnoff99Frosst-1.0.8', 'smirnoff99Frosst-1.0.6', 'smirnoff99Frosst-1.0.3', 'smirnoff99Frosst-1.0.1', 'smirnoff99Frosst-1.0.5', 'smirnoff99Frosst-1.0.9', 'smirnoff99Frosst-1.0.7']
    
        You can optionally create or use a cache of pre-parameterized molecules:
    
        >>> smirnoff = SMIRNOFFTemplateGenerator(cache='smirnoff.json', forcefield='openff-2.0.0')  # doctest: +SKIP
    
        Newly parameterized molecules will be written to the cache, saving time next time!
        """
        # Initialize molecules and cache
        super().__init__(molecules=molecules, cache=cache)
    
        if forcefield is None:
            # Use latest supported Open Force Field Initiative release if none is specified
            forcefield = 'openff-2.0.0'
            # TODO: After toolkit provides date-ranked force fields,
            # use latest dated version if we can sort by date, such as self.INSTALLED_FORCEFIELDS[-1]
        self._forcefield = forcefield
    
        # Track parameters by provided SMIRNOFF name
        # TODO: Can we instead use the force field hash, or some other unique identifier?
        # TODO: Use file hash instead of name?
        import os
        self._database_table_name = os.path.basename(forcefield)
    
        # Create ForceField object
        import openff.toolkit.typing.engines.smirnoff
        try:
            filename = forcefield
            if not filename.endswith('.offxml'):
                filename += '.offxml'
>           self._smirnoff_forcefield = openff.toolkit.typing.engines.smirnoff.ForceField(filename)

openmmforcefields/generators/template_generators.py:1282: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <openff.toolkit.typing.engines.smirnoff.forcefield.ForceField object at 0x7f7bef20bdc0>
aromaticity_model = 'OEAroModel_MDL'
parameter_handler_classes = [<class 'openff.toolkit.typing.engines.smirnoff.parameters.ConstraintHandler'>, <class 'openff.toolkit.typing.engines....arameters.ImproperTorsionHandler'>, <class 'openff.toolkit.typing.engines.smirnoff.parameters._NonbondedHandler'>, ...]
parameter_io_handler_classes = [<class 'openff.toolkit.typing.engines.smirnoff.io.XMLParameterIOHandler'>]
disable_version_check = False

    def __init__(
        self,
        *sources,
        aromaticity_model=DEFAULT_AROMATICITY_MODEL,
        parameter_handler_classes=None,
        parameter_io_handler_classes=None,
        disable_version_check=False,
        allow_cosmetic_attributes=False,
        load_plugins=False,
    ):
        """Create a new :class:`ForceField` object from one or more SMIRNOFF parameter definition files.
    
        Parameters
        ----------
        sources : string or file-like object or open file handle or URL (or iterable of these)
            A list of files defining the SMIRNOFF force field to be loaded.
            Currently, only `the SMIRNOFF XML format <https://openforcefield.github.io/standards/standards/smirnoff/>`_
            is supported.  Each entry may be an absolute file path, a path relative to the current working directory, a
            path relative to this module's data subdirectory (for built in force fields), or an open file-like object
            with a ``read()`` method from which the force field XML data can be loaded.  If multiple files are
            specified, any top-level tags that are repeated will be merged if they are compatible, with files appearing
            later in the sequence resulting in parameters that have higher precedence.  Support for multiple files is
            primarily intended to allow solvent parameters to be specified by listing them last in the sequence.
        aromaticity_model : string, default='OEAroModel_MDL'
            The aromaticity model used by the force field. Currently, only 'OEAroModel_MDL' is supported
        parameter_handler_classes : iterable of ParameterHandler classes, optional, default=None
            If not None, the specified set of ParameterHandler classes will be instantiated to create the parameter
            object model.  By default, all imported subclasses of ParameterHandler are automatically registered.
        parameter_io_handler_classes : iterable of ParameterIOHandler classes
            If not None, the specified set of ParameterIOHandler classes will be used to parse/generate serialized
            parameter sets.  By default, all imported subclasses of ParameterIOHandler are automatically registered.
        disable_version_check : bool, optional, default=False
            If True, will disable checks against the current highest supported force field version.
            This option is primarily intended for force field development.
        allow_cosmetic_attributes : bool, optional. Default = False
            Whether to retain non-spec kwargs from data sources.
        load_plugins: bool, optional. Default = False
            Whether to load ``ParameterHandler`` classes which have been registered
            by installed plugins.
    
        Examples
        --------
    
        Load one SMIRNOFF parameter set in XML format (searching the package data directory by default, which includes
        some standard parameter sets):
    
        >>> forcefield = ForceField('test_forcefields/test_forcefield.offxml')
    
        Load multiple SMIRNOFF parameter sets:
    
        >>> forcefield = ForceField('test_forcefields/test_forcefield.offxml', 'test_forcefields/tip3p.offxml')
    
        Load a parameter set from a string:
    
        >>> offxml = '<SMIRNOFF version="0.2" aromaticity_model="OEAroModel_MDL"/>'
        >>> forcefield = ForceField(offxml)
    
        """
        # Clear all object fields
        self._initialize()
    
        self.aromaticity_model = aromaticity_model
        # Store initialization options
        self.disable_version_check = disable_version_check
        # if True, we won't check which SMIRNOFF version number we're parsing
    
        # Register all ParameterHandler objects that will process SMIRNOFF force definitions
        # TODO: We need to change this to just find all ParameterHandler objects in this file;
        # otherwise, we can't define two different ParameterHandler subclasses to compare for a new type of energy term
        # since both will try to register themselves for the same XML tag and an Exception will be raised.
        if parameter_handler_classes is None:
            parameter_handler_classes = all_subclasses(ParameterHandler)
        if load_plugins:
    
            registered_handlers = load_handler_plugins()
    
            # Make sure the same handlers aren't added twice.
            parameter_handler_classes += [
                handler
                for handler in registered_handlers
                if handler not in parameter_handler_classes
            ]
    
        self._register_parameter_handler_classes(parameter_handler_classes)
    
        # Register all ParameterIOHandler objects that will process serialized parameter representations
        if parameter_io_handler_classes is None:
            parameter_io_handler_classes = all_subclasses(ParameterIOHandler)
    
        self._register_parameter_io_handler_classes(parameter_io_handler_classes)
    
        # Parse all sources containing SMIRNOFF parameter definitions
>       self.parse_sources(sources, allow_cosmetic_attributes=allow_cosmetic_attributes)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/typing/engines/smirnoff/forcefield.py:307: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <openff.toolkit.typing.engines.smirnoff.forcefield.ForceField object at 0x7f7bef20bdc0>
sources = <tuple_iterator object at 0x7f7bdcce2490>
allow_cosmetic_attributes = False

    def parse_sources(self, sources, allow_cosmetic_attributes=True):
        """Parse a SMIRNOFF force field definition.
    
        Parameters
        ----------
        sources : string or file-like object or open file handle or URL (or iterable of these)
            A list of files defining the SMIRNOFF force field to be loaded.
            Currently, only `the SMIRNOFF XML format <https://openforcefield.github.io/standards/standards/smirnoff/>`_
            is supported.  Each entry may be an absolute file path, a path relative to the current working directory, a
            path relative to this module's data subdirectory (for built in force fields), or an open file-like object
            with a ``read()`` method from which the force field XML data can be loaded.  If multiple files are
            specified, any top-level tags that are repeated will be merged if they are compatible, with files appearing
            later in the sequence resulting in parameters that have higher precedence.  Support for multiple files is
            primarily intended to allow solvent parameters to be specified by listing them last in the sequence.
        allow_cosmetic_attributes : bool, optional. Default = False
            Whether to permit non-spec kwargs present in the source.
    
        Notes
        -----
    
            * New SMIRNOFF sections are handled independently, as if they were specified in the same file.
            * If a SMIRNOFF section that has already been read appears again, its definitions are appended to the end
                of the previously-read definitions if the sections are configured with compatible attributes;
                otherwise, an ``IncompatibleTagException`` is raised.
    
        """
        # Ensure that we are working with an iterable
        try:
            sources = iter(sources)
        except TypeError:
            # Make iterable object
            sources = [sources]
    
        # TODO: If a non-first source fails here, the force field might be partially modified
        for source in sources:
            smirnoff_data = self.parse_smirnoff_from_source(source)
>           self._load_smirnoff_data(
                smirnoff_data, allow_cosmetic_attributes=allow_cosmetic_attributes
            )

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/typing/engines/smirnoff/forcefield.py:768: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <openff.toolkit.typing.engines.smirnoff.forcefield.ForceField object at 0x7f7bef20bdc0>
smirnoff_data = {'SMIRNOFF': {'Angles': {'Angle': [{'angle': <Quantity(116.547586, 'degree')>, 'id': 'a1', 'k': <Quantity(106.410633, ...uantity(1.51390065, 'angstrom')>, 'id': 'c-tip3p-H-O-H', 'smirks': '[#1:1]-[#8X2H2+0]-[#1:2]'}], 'version': 0.3}, ...}}
allow_cosmetic_attributes = False

    def _load_smirnoff_data(self, smirnoff_data, allow_cosmetic_attributes=False):
        """
        Add parameters from a SMIRNOFF-format data structure to this ForceField.
    
        Parameters
        ----------
        smirnoff_data : OrderedDict
            A representation of a SMIRNOFF-format data structure. Begins at top-level 'SMIRNOFF' key.
        allow_cosmetic_attributes : bool, optional. Default = False
            Whether to permit non-spec kwargs in smirnoff_data.
        """
        import packaging.version
    
        # Check that the SMIRNOFF version of this data structure is supported by this ForceField implementation
    
        if "SMIRNOFF" in smirnoff_data:
            version = smirnoff_data["SMIRNOFF"]["version"]
        elif "SMIRFF" in smirnoff_data:
            version = smirnoff_data["SMIRFF"]["version"]
        else:
            raise SMIRNOFFParseError(
                "'version' attribute must be specified in SMIRNOFF tag"
            )
    
        self._check_smirnoff_version_compatibility(str(version))
    
        # Convert 0.1 spec files to 0.3 SMIRNOFF data format by converting
        # from 0.1 spec to 0.2, then 0.2 to 0.3
        if packaging.version.parse(str(version)) == packaging.version.parse("0.1"):
            # NOTE: This will convert the top-level "SMIRFF" tag to "SMIRNOFF"
            smirnoff_data = convert_0_1_smirnoff_to_0_2(smirnoff_data)
            smirnoff_data = convert_0_2_smirnoff_to_0_3(smirnoff_data)
    
        # Convert 0.2 spec files to 0.3 SMIRNOFF data format by removing units
        # from section headers and adding them to quantity strings at all levels.
        elif packaging.version.parse(str(version)) == packaging.version.parse("0.2"):
            smirnoff_data = convert_0_2_smirnoff_to_0_3(smirnoff_data)
    
        # Ensure that SMIRNOFF is a top-level key of the dict
        if not ("SMIRNOFF" in smirnoff_data):
            raise SMIRNOFFParseError(
                "'SMIRNOFF' must be a top-level key in the SMIRNOFF object model"
            )
    
        # Check that the aromaticity model required by this parameter set is compatible with
        # others loaded by this ForceField
        if "aromaticity_model" in smirnoff_data["SMIRNOFF"]:
            aromaticity_model = smirnoff_data["SMIRNOFF"]["aromaticity_model"]
            self.aromaticity_model = aromaticity_model
    
        elif self._aromaticity_model is None:
            raise SMIRNOFFParseError(
                "'aromaticity_model' attribute must be specified in SMIRNOFF "
                "tag, or contained in a previously-loaded SMIRNOFF data source"
            )
    
        if "Author" in smirnoff_data["SMIRNOFF"]:
            self._add_author(smirnoff_data["SMIRNOFF"]["Author"])
    
        if "Date" in smirnoff_data["SMIRNOFF"]:
            self._add_date(smirnoff_data["SMIRNOFF"]["Date"])
    
        # Go through the whole SMIRNOFF data structure, trying to convert all strings to Quantity
>       smirnoff_data = convert_all_strings_to_quantity(smirnoff_data)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/typing/engines/smirnoff/forcefield.py:876: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

smirnoff_data = {'SMIRNOFF': {'Angles': {'Angle': [{'angle': <Quantity(116.547586, 'degree')>, 'id': 'a1', 'k': <Quantity(106.410633, ...uantity(1.51390065, 'angstrom')>, 'id': 'c-tip3p-H-O-H', 'smirks': '[#1:1]-[#8X2H2+0]-[#1:2]'}], 'version': 0.3}, ...}}

    def convert_all_strings_to_quantity(smirnoff_data):
        """
        Traverses a SMIRNOFF data structure, attempting to convert all
        quantity-defining strings into openff.units.unit.Quantity objects.
    
        Integers and floats are ignored and not converted into a dimensionless
        ``openff.units.unit.Quantity`` object.
    
        Parameters
        ----------
        smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec
    
        Returns
        -------
        converted_smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec,
            with quantity-defining strings converted to openff.units.unit.Quantity objects
        """
        if isinstance(smirnoff_data, dict):
            for key, value in smirnoff_data.items():
>               smirnoff_data[key] = convert_all_strings_to_quantity(value)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:233: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

smirnoff_data = {'Angles': {'Angle': [{'angle': <Quantity(116.547586, 'degree')>, 'id': 'a1', 'k': <Quantity(106.410633, 'kilocalorie ...Quantity(1.51390065, 'angstrom')>, 'id': 'c-tip3p-H-O-H', 'smirks': '[#1:1]-[#8X2H2+0]-[#1:2]'}], 'version': 0.3}, ...}

    def convert_all_strings_to_quantity(smirnoff_data):
        """
        Traverses a SMIRNOFF data structure, attempting to convert all
        quantity-defining strings into openff.units.unit.Quantity objects.
    
        Integers and floats are ignored and not converted into a dimensionless
        ``openff.units.unit.Quantity`` object.
    
        Parameters
        ----------
        smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec
    
        Returns
        -------
        converted_smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec,
            with quantity-defining strings converted to openff.units.unit.Quantity objects
        """
        if isinstance(smirnoff_data, dict):
            for key, value in smirnoff_data.items():
>               smirnoff_data[key] = convert_all_strings_to_quantity(value)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:233: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

smirnoff_data = {'Angle': [{'angle': <Quantity(116.547586, 'degree')>, 'id': 'a1', 'k': <Quantity(106.410633, 'kilocalorie / mole / ra...**-1 * radian**-2 * kilocalorie', 'smirks': '[#1:1]-[*;r3:2]~;!@[*:3]'}, ...], 'potential': 'harmonic', 'version': 0.3}

    def convert_all_strings_to_quantity(smirnoff_data):
        """
        Traverses a SMIRNOFF data structure, attempting to convert all
        quantity-defining strings into openff.units.unit.Quantity objects.
    
        Integers and floats are ignored and not converted into a dimensionless
        ``openff.units.unit.Quantity`` object.
    
        Parameters
        ----------
        smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec
    
        Returns
        -------
        converted_smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec,
            with quantity-defining strings converted to openff.units.unit.Quantity objects
        """
        if isinstance(smirnoff_data, dict):
            for key, value in smirnoff_data.items():
>               smirnoff_data[key] = convert_all_strings_to_quantity(value)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:233: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

smirnoff_data = [{'angle': <Quantity(116.547586, 'degree')>, 'id': 'a1', 'k': <Quantity(106.410633, 'kilocalorie / mole / radian ** 2'...', 'id': 'a6', 'k': '73.67[220](https://github.com/openmm/openmmforcefields/actions/runs/3588100701/jobs/6039120987#step:9:221)177811 * mole**-1 * radian**-2 * kilocalorie', 'smirks': '[#1:1]-[*;r3:2]~;!@[*:3]'}, ...]

    def convert_all_strings_to_quantity(smirnoff_data):
        """
        Traverses a SMIRNOFF data structure, attempting to convert all
        quantity-defining strings into openff.units.unit.Quantity objects.
    
        Integers and floats are ignored and not converted into a dimensionless
        ``openff.units.unit.Quantity`` object.
    
        Parameters
        ----------
        smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec
    
        Returns
        -------
        converted_smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec,
            with quantity-defining strings converted to openff.units.unit.Quantity objects
        """
        if isinstance(smirnoff_data, dict):
            for key, value in smirnoff_data.items():
                smirnoff_data[key] = convert_all_strings_to_quantity(value)
            obj_to_return = smirnoff_data
    
        elif isinstance(smirnoff_data, list):
            for index, item in enumerate(smirnoff_data):
>               smirnoff_data[index] = convert_all_strings_to_quantity(item)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:238: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

smirnoff_data = {'angle': '74.01137772986 * degree', 'id': 'a3', 'k': '250.0821003542 * mole**-1 * radian**-2 * kilocalorie', 'smirks': '[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1'}

    def convert_all_strings_to_quantity(smirnoff_data):
        """
        Traverses a SMIRNOFF data structure, attempting to convert all
        quantity-defining strings into openff.units.unit.Quantity objects.
    
        Integers and floats are ignored and not converted into a dimensionless
        ``openff.units.unit.Quantity`` object.
    
        Parameters
        ----------
        smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec
    
        Returns
        -------
        converted_smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec,
            with quantity-defining strings converted to openff.units.unit.Quantity objects
        """
        if isinstance(smirnoff_data, dict):
            for key, value in smirnoff_data.items():
>               smirnoff_data[key] = convert_all_strings_to_quantity(value)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:233: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

smirnoff_data = '[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1'

    def convert_all_strings_to_quantity(smirnoff_data):
        """
        Traverses a SMIRNOFF data structure, attempting to convert all
        quantity-defining strings into openff.units.unit.Quantity objects.
    
        Integers and floats are ignored and not converted into a dimensionless
        ``openff.units.unit.Quantity`` object.
    
        Parameters
        ----------
        smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec
    
        Returns
        -------
        converted_smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec,
            with quantity-defining strings converted to openff.units.unit.Quantity objects
        """
        if isinstance(smirnoff_data, dict):
            for key, value in smirnoff_data.items():
                smirnoff_data[key] = convert_all_strings_to_quantity(value)
            obj_to_return = smirnoff_data
    
        elif isinstance(smirnoff_data, list):
            for index, item in enumerate(smirnoff_data):
                smirnoff_data[index] = convert_all_strings_to_quantity(item)
            obj_to_return = smirnoff_data
    
        elif isinstance(smirnoff_data, int) or isinstance(smirnoff_data, float):
            obj_to_return = smirnoff_data
    
        else:
            try:
>               obj_to_return = object_to_quantity(smirnoff_data)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:246: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = ('[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1',), kw = {}

    def wrapper(*args, **kw):
        if not args:
            raise TypeError(f'{funcname} requires at least '
                            '1 positional argument')
    
>       return dispatch(args[0].__class__)(*args, **kw)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/functools.py:875: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

obj = '[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1'

    @object_to_quantity.register(str)
    def _(obj):
        import pint
    
        try:
>           return string_to_quantity(obj)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:318: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

quantity_string = '[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1'

    def string_to_quantity(quantity_string) -> Union[str, int, float, unit.Quantity]:
        """Attempt to parse a string into a unit.Quantity.
    
        Note that dimensionless floats and ints are returns as floats or ints, not Quantity objects.
        """
    
        from tokenize import TokenError
    
        from pint import UndefinedUnitError
    
        try:
>           quantity = unit.Quantity(quantity_string)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:200: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

cls = <class 'pint.util.Quantity'>, value = '[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1'
units = None

    def __new__(cls, value, units=None):
        if is_upcast_type(type(value)):
            raise TypeError(f"PlainQuantity cannot wrap upcast type {type(value)}")
    
        if units is None and isinstance(value, str) and value == "":
            raise ValueError(
                "Expression to parse as PlainQuantity cannot be an empty string."
            )
    
        if units is None and isinstance(value, str):
            ureg = SharedRegistryObject.__new__(cls)._REGISTRY
>           inst = ureg.parse_expression(value)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/facets/plain/quantity.py:209: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <openff.units.units.UnitRegistry object at 0x7f7beee859a0>
input_string = '[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1', case_sensitive = None
use_decimal = False, values = {}
gen = <generator object tokenizer at 0x7f7c0b6e2dd0>

    def parse_expression(
        self,
        input_string: str,
        case_sensitive: Optional[bool] = None,
        use_decimal: bool = False,
        **values,
    ) -> Quantity:
        """Parse a mathematical expression including units and return a quantity object.
    
        Numerical constants can be specified as keyword arguments and will take precedence
        over the names defined in the registry.
    
        Parameters
        ----------
        input_string :
    
        case_sensitive :
             (Default value = None, which uses registry setting)
        use_decimal :
             (Default value = False)
        **values :
    
    
        Returns
        -------
    
        """
    
        # TODO: remove this code when use_decimal is deprecated
        if use_decimal:
            raise DeprecationWarning(
                "`use_decimal` is deprecated, use `non_int_type` keyword argument when instantiating the registry.\n"
                ">>> from decimal import Decimal\n"
                ">>> ureg = UnitRegistry(non_int_type=Decimal)"
            )
    
        if not input_string:
            return self.Quantity(1)
    
        for p in self.preprocessors:
            input_string = p(input_string)
        input_string = string_preprocessor(input_string)
        gen = tokenizer(input_string)
    
>       return build_eval_tree(gen).evaluate(
            lambda x: self._eval_token(x, case_sensitive=case_sensitive, **values)
        )

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/facets/plain/registry.py:1255: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pint.pint_eval.EvalTreeNode object at 0x7f7be133ce80>
define_op = <function PlainRegistry.parse_expression.<locals>.<lambda> at 0x7f7be7cd94c0>
bin_op = {'': <built-in function mul>, '%': <built-in function mod>, '*': <built-in function mul>, '**': <function _power at 0x7f7beef89af0>, ...}
un_op = {'+': <function <lambda> at 0x7f7beefaaa60>, '-': <function <lambda> at 0x7f7beefaa9d0>}

    def evaluate(self, define_op, bin_op=None, un_op=None):
        """Evaluate node.
    
        Parameters
        ----------
        define_op : callable
            Translates tokens into objects.
        bin_op : dict or None, optional
             (Default value = _BINARY_OPERATOR_MAP)
        un_op : dict or None, optional
             (Default value = _UNARY_OPERATOR_MAP)
    
        Returns
        -------
    
        """
    
        bin_op = bin_op or _BINARY_OPERATOR_MAP
        un_op = un_op or _UNARY_OPERATOR_MAP
    
        if self.right:
            # binary or implicit operator
            op_text = self.operator[1] if self.operator else ""
            if op_text not in bin_op:
                raise DefinitionSyntaxError('missing binary operator "%s"' % op_text)
>           left = self.left.evaluate(define_op, bin_op, un_op)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/pint_eval.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pint.pint_eval.EvalTreeNode object at 0x7f7be133cdc0>
define_op = <function PlainRegistry.parse_expression.<locals>.<lambda> at 0x7f7be7cd94c0>
bin_op = {'': <built-in function mul>, '%': <built-in function mod>, '*': <built-in function mul>, '**': <function _power at 0x7f7beef89af0>, ...}
un_op = {'+': <function <lambda> at 0x7f7beefaaa60>, '-': <function <lambda> at 0x7f7beefaa9d0>}

    def evaluate(self, define_op, bin_op=None, un_op=None):
        """Evaluate node.
    
        Parameters
        ----------
        define_op : callable
            Translates tokens into objects.
        bin_op : dict or None, optional
             (Default value = _BINARY_OPERATOR_MAP)
        un_op : dict or None, optional
             (Default value = _UNARY_OPERATOR_MAP)
    
        Returns
        -------
    
        """
    
        bin_op = bin_op or _BINARY_OPERATOR_MAP
        un_op = un_op or _UNARY_OPERATOR_MAP
    
        if self.right:
            # binary or implicit operator
            op_text = self.operator[1] if self.operator else ""
            if op_text not in bin_op:
                raise DefinitionSyntaxError('missing binary operator "%s"' % op_text)
>           left = self.left.evaluate(define_op, bin_op, un_op)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/pint_eval.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pint.pint_eval.EvalTreeNode object at 0x7f7be133cb80>
define_op = <function PlainRegistry.parse_expression.<locals>.<lambda> at 0x7f7be7cd94c0>
bin_op = {'': <built-in function mul>, '%': <built-in function mod>, '*': <built-in function mul>, '**': <function _power at 0x7f7beef89af0>, ...}
un_op = {'+': <function <lambda> at 0x7f7beefaaa60>, '-': <function <lambda> at 0x7f7beefaa9d0>}

    def evaluate(self, define_op, bin_op=None, un_op=None):
        """Evaluate node.
    
        Parameters
        ----------
        define_op : callable
            Translates tokens into objects.
        bin_op : dict or None, optional
             (Default value = _BINARY_OPERATOR_MAP)
        un_op : dict or None, optional
             (Default value = _UNARY_OPERATOR_MAP)
    
        Returns
        -------
    
        """
    
        bin_op = bin_op or _BINARY_OPERATOR_MAP
        un_op = un_op or _UNARY_OPERATOR_MAP
    
        if self.right:
            # binary or implicit operator
            op_text = self.operator[1] if self.operator else ""
            if op_text not in bin_op:
                raise DefinitionSyntaxError('missing binary operator "%s"' % op_text)
>           left = self.left.evaluate(define_op, bin_op, un_op)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/pint_eval.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pint.pint_eval.EvalTreeNode object at 0x7f7be133c280>
define_op = <function PlainRegistry.parse_expression.<locals>.<lambda> at 0x7f7be7cd94c0>
bin_op = {'': <built-in function mul>, '%': <built-in function mod>, '*': <built-in function mul>, '**': <function _power at 0x7f7beef89af0>, ...}
un_op = {'+': <function <lambda> at 0x7f7beefaaa60>, '-': <function <lambda> at 0x7f7beefaa9d0>}

    def evaluate(self, define_op, bin_op=None, un_op=None):
        """Evaluate node.
    
        Parameters
        ----------
        define_op : callable
            Translates tokens into objects.
        bin_op : dict or None, optional
             (Default value = _BINARY_OPERATOR_MAP)
        un_op : dict or None, optional
             (Default value = _UNARY_OPERATOR_MAP)
    
        Returns
        -------
    
        """
    
        bin_op = bin_op or _BINARY_OPERATOR_MAP
        un_op = un_op or _UNARY_OPERATOR_MAP
    
        if self.right:
            # binary or implicit operator
            op_text = self.operator[1] if self.operator else ""
            if op_text not in bin_op:
                raise DefinitionSyntaxError('missing binary operator "%s"' % op_text)
>           left = self.left.evaluate(define_op, bin_op, un_op)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/pint_eval.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pint.pint_eval.EvalTreeNode object at 0x7f7be133c070>
define_op = <function PlainRegistry.parse_expression.<locals>.<lambda> at 0x7f7be7cd94c0>
bin_op = {'': <built-in function mul>, '%': <built-in function mod>, '*': <built-in function mul>, '**': <function _power at 0x7f7beef89af0>, ...}
un_op = {'+': <function <lambda> at 0x7f7beefaaa60>, '-': <function <lambda> at 0x7f7beefaa9d0>}

    def evaluate(self, define_op, bin_op=None, un_op=None):
        """Evaluate node.
    
        Parameters
        ----------
        define_op : callable
            Translates tokens into objects.
        bin_op : dict or None, optional
             (Default value = _BINARY_OPERATOR_MAP)
        un_op : dict or None, optional
             (Default value = _UNARY_OPERATOR_MAP)
    
        Returns
        -------
    
        """
    
        bin_op = bin_op or _BINARY_OPERATOR_MAP
        un_op = un_op or _UNARY_OPERATOR_MAP
    
        if self.right:
            # binary or implicit operator
            op_text = self.operator[1] if self.operator else ""
            if op_text not in bin_op:
                raise DefinitionSyntaxError('missing binary operator "%s"' % op_text)
>           left = self.left.evaluate(define_op, bin_op, un_op)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/pint_eval.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pint.pint_eval.EvalTreeNode object at 0x7f7be133c130>
define_op = <function PlainRegistry.parse_expression.<locals>.<lambda> at 0x7f7be7cd94c0>
bin_op = {'': <built-in function mul>, '%': <built-in function mod>, '*': <built-in function mul>, '**': <function _power at 0x7f7beef89af0>, ...}
un_op = {'+': <function <lambda> at 0x7f7beefaaa60>, '-': <function <lambda> at 0x7f7beefaa9d0>}

    def evaluate(self, define_op, bin_op=None, un_op=None):
        """Evaluate node.
    
        Parameters
        ----------
        define_op : callable
            Translates tokens into objects.
        bin_op : dict or None, optional
             (Default value = _BINARY_OPERATOR_MAP)
        un_op : dict or None, optional
             (Default value = _UNARY_OPERATOR_MAP)
    
        Returns
        -------
    
        """
    
        bin_op = bin_op or _BINARY_OPERATOR_MAP
        un_op = un_op or _UNARY_OPERATOR_MAP
    
        if self.right:
            # binary or implicit operator
            op_text = self.operator[1] if self.operator else ""
            if op_text not in bin_op:
                raise DefinitionSyntaxError('missing binary operator "%s"' % op_text)
>           left = self.left.evaluate(define_op, bin_op, un_op)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/pint_eval.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pint.pint_eval.EvalTreeNode object at 0x7f7be133cbb0>
define_op = <function PlainRegistry.parse_expression.<locals>.<lambda> at 0x7f7be7cd94c0>
bin_op = {'': <built-in function mul>, '%': <built-in function mod>, '*': <built-in function mul>, '**': <function _power at 0x7f7beef89af0>, ...}
un_op = {'+': <function <lambda> at 0x7f7beefaaa60>, '-': <function <lambda> at 0x7f7beefaa9d0>}

    def evaluate(self, define_op, bin_op=None, un_op=None):
        """Evaluate node.
    
        Parameters
        ----------
        define_op : callable
            Translates tokens into objects.
        bin_op : dict or None, optional
             (Default value = _BINARY_OPERATOR_MAP)
        un_op : dict or None, optional
             (Default value = _UNARY_OPERATOR_MAP)
    
        Returns
        -------
    
        """
    
        bin_op = bin_op or _BINARY_OPERATOR_MAP
        un_op = un_op or _UNARY_OPERATOR_MAP
    
        if self.right:
            # binary or implicit operator
            op_text = self.operator[1] if self.operator else ""
            if op_text not in bin_op:
                raise DefinitionSyntaxError('missing binary operator "%s"' % op_text)
>           left = self.left.evaluate(define_op, bin_op, un_op)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/pint_eval.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pint.pint_eval.EvalTreeNode object at 0x7f7be133c7f0>
define_op = <function PlainRegistry.parse_expression.<locals>.<lambda> at 0x7f7be7cd94c0>
bin_op = {'': <built-in function mul>, '%': <built-in function mod>, '*': <built-in function mul>, '**': <function _power at 0x7f7beef89af0>, ...}
un_op = {'+': <function <lambda> at 0x7f7beefaaa60>, '-': <function <lambda> at 0x7f7beefaa9d0>}

    def evaluate(self, define_op, bin_op=None, un_op=None):
        """Evaluate node.
    
        Parameters
        ----------
        define_op : callable
            Translates tokens into objects.
        bin_op : dict or None, optional
             (Default value = _BINARY_OPERATOR_MAP)
        un_op : dict or None, optional
             (Default value = _UNARY_OPERATOR_MAP)
    
        Returns
        -------
    
        """
    
        bin_op = bin_op or _BINARY_OPERATOR_MAP
        un_op = un_op or _UNARY_OPERATOR_MAP
    
        if self.right:
            # binary or implicit operator
            op_text = self.operator[1] if self.operator else ""
            if op_text not in bin_op:
                raise DefinitionSyntaxError('missing binary operator "%s"' % op_text)
            left = self.left.evaluate(define_op, bin_op, un_op)
            return bin_op[op_text](left, self.right.evaluate(define_op, bin_op, un_op))
        elif self.operator:
            # unary operator
            op_text = self.operator[1]
            if op_text not in un_op:
>               raise DefinitionSyntaxError('missing unary operator "%s"' % op_text)
E               pint.errors.DefinitionSyntaxError: missing unary operator "*"

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/pint/pint_eval.py:120: DefinitionSyntaxError

During handling of the above exception, another exception occurred:

self = <openmmforcefields.tests.test_template_generators.TestSMIRNOFFTemplateGenerator testMethod=test_add_molecules>

    def test_add_molecules(self):
        """Test that molecules can be added to template generator after its creation"""
        # Create a generator that does not know about any molecules
>       generator = self.TEMPLATE_GENERATOR()

openmmforcefields/tests/test_template_generators.py:122: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <openmmforcefields.generators.template_generators.SMIRNOFFTemplateGenerator object at 0x7f7be13d9580>
molecules = None, cache = None, forcefield = 'openff-2.0.0'

    def __init__(self, molecules=None, cache=None, forcefield=None):
        """
        Create a SMIRNOFFTemplateGenerator with some OpenFF toolkit molecules
    
        Requies the OpenFF toolkit: http://openforcefield.org/
    
        Parameters
        ----------
        molecules : openff.toolkit.topology.Molecule or list, optional, default=None
            Can alternatively be an object (such as an OpenEye OEMol or RDKit Mol or SMILES string) that can be used to construct a Molecule.
            Can also be a list of Molecule objects or objects that can be used to construct a Molecule.
            If specified, these molecules will be recognized and parameterized with SMIRNOFF as needed.
            The parameters will be cached in case they are encountered again the future.
        cache : str, optional, default=None
            Filename for global caching of parameters.
            If specified, parameterized molecules will be stored in a TinyDB instance as a JSON file.
        forcefield : str, optional, default=None
            Name of installed SMIRNOFF force field (without .offxml) or local .offxml filename (with extension).
            If not specified, the latest Open Force Field Initiative release is used.
    
        Examples
        --------
    
        Create a SMIRNOFF template generator for a single molecule (benzene, created from SMILES) and register it with ForceField:
    
        >>> from openff.toolkit.topology import Molecule
        >>> molecule = Molecule.from_smiles('c1ccccc1')
        >>> from openmmforcefields.generators import SMIRNOFFTemplateGenerator
        >>> smirnoff = SMIRNOFFTemplateGenerator(molecules=molecule)
        >>> from openmm.app import ForceField
        >>> amber_forcefields = ['amber/protein.ff14SB.xml', 'amber/tip3p_standard.xml', 'amber/tip3p_HFE_multivalent.xml']
        >>> forcefield = ForceField(*amber_forcefields)
    
        The latest Open Force Field Initiative release is used if none is specified.
    
        >>> smirnoff.forcefield
        'openff-2.0.0'
    
        You can check which SMIRNOFF force field filename is in use with
    
        >>> smirnoff.smirnoff_filename  # doctest:+ELLIPSIS
        '/.../openff-2.0.0.offxml'
    
        Create a template generator for a specific SMIRNOFF force field for multiple
        molecules read from an SDF file:
    
        >>> molecules = Molecule.from_file('molecules.sdf')  # doctest: +SKIP
        >>> smirnoff = SMIRNOFFTemplateGenerator(molecules=molecules, forcefield='openff-2.0.0')  # doctest: +SKIP
    
        You can also add molecules later on after the generator has been registered:
    
        >>> smirnoff.add_molecules(molecules)  # doctest: +SKIP
    
        To check which SMIRNOFF versions are supported, check the `INSTALLED_FORCEFIELDS` attribute:
    
        >>> print(SMIRNOFFTemplateGenerator.INSTALLED_FORCEFIELDS)  # doctest: +SKIP
        ['openff-1.0.1', 'openff-1.1.1', 'openff-1.0.0-RC1', 'openff-1.2.0', 'openff-1.1.0', 'openff-1.0.0', 'openff-1.0.0-RC2', 'smirnoff99Frosst-1.0.2', 'smirnoff99Frosst-1.0.0', 'smirnoff99Frosst-1.1.0', 'smirnoff99Frosst-1.0.4', 'smirnoff99Frosst-1.0.8', 'smirnoff99Frosst-1.0.6', 'smirnoff99Frosst-1.0.3', 'smirnoff99Frosst-1.0.1', 'smirnoff99Frosst-1.0.5', 'smirnoff99Frosst-1.0.9', 'smirnoff99Frosst-1.0.7']
    
        You can optionally create or use a cache of pre-parameterized molecules:
    
        >>> smirnoff = SMIRNOFFTemplateGenerator(cache='smirnoff.json', forcefield='openff-2.0.0')  # doctest: +SKIP
    
        Newly parameterized molecules will be written to the cache, saving time next time!
        """
        # Initialize molecules and cache
        super().__init__(molecules=molecules, cache=cache)
    
        if forcefield is None:
            # Use latest supported Open Force Field Initiative release if none is specified
            forcefield = 'openff-2.0.0'
            # TODO: After toolkit provides date-ranked force fields,
            # use latest dated version if we can sort by date, such as self.INSTALLED_FORCEFIELDS[-1]
        self._forcefield = forcefield
    
        # Track parameters by provided SMIRNOFF name
        # TODO: Can we instead use the force field hash, or some other unique identifier?
        # TODO: Use file hash instead of name?
        import os
        self._database_table_name = os.path.basename(forcefield)
    
        # Create ForceField object
        import openff.toolkit.typing.engines.smirnoff
        try:
            filename = forcefield
            if not filename.endswith('.offxml'):
                filename += '.offxml'
            self._smirnoff_forcefield = openff.toolkit.typing.engines.smirnoff.ForceField(filename)
        except Exception as e:
            _logger.error(e)
>           raise ValueError(f"Can't find specified SMIRNOFF force field ({forcefield}) in install paths")
E           ValueError: Can't find specified SMIRNOFF force field (openff-2.0.0) in install paths

openmmforcefields/generators/template_generators.py:1[285](https://github.com/openmm/openmmforcefields/actions/runs/3588100701/jobs/6039120987#step:9:286): ValueError
------------------------------ Captured log call -------------------------------
ERROR    openmmforcefields.generators.template_generators:template_generators.py:1284 missing unary operator "*"

If you follow it all the way to find what appears to be the error, we see this block:

quantity_string = '[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1'

    def string_to_quantity(quantity_string) -> Union[str, int, float, unit.Quantity]:
        """Attempt to parse a string into a unit.Quantity.
    
        Note that dimensionless floats and ints are returns as floats or ints, not Quantity objects.
        """
    
        from tokenize import TokenError
    
        from pint import UndefinedUnitError
    
        try:
>           quantity = unit.Quantity(quantity_string)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:200: 

It does appear that the toolkit attempts to convert all strings to quantities:

smirnoff_data = '[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1'

    def convert_all_strings_to_quantity(smirnoff_data):
        """
        Traverses a SMIRNOFF data structure, attempting to convert all
        quantity-defining strings into openff.units.unit.Quantity objects.
    
        Integers and floats are ignored and not converted into a dimensionless
        ``openff.units.unit.Quantity`` object.
    
        Parameters
        ----------
        smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec
    
        Returns
        -------
        converted_smirnoff_data : dict
            A hierarchical dict structured in compliance with the SMIRNOFF spec,
            with quantity-defining strings converted to openff.units.unit.Quantity objects
        """
        if isinstance(smirnoff_data, dict):
            for key, value in smirnoff_data.items():
                smirnoff_data[key] = convert_all_strings_to_quantity(value)
            obj_to_return = smirnoff_data
    
        elif isinstance(smirnoff_data, list):
            for index, item in enumerate(smirnoff_data):
                smirnoff_data[index] = convert_all_strings_to_quantity(item)
            obj_to_return = smirnoff_data
    
        elif isinstance(smirnoff_data, int) or isinstance(smirnoff_data, float):
            obj_to_return = smirnoff_data
    
        else:
            try:
>               obj_to_return = object_to_quantity(smirnoff_data)

../../../micromamba-root/envs/openmmforcefields-test/lib/python3.8/site-packages/openff/toolkit/utils/utils.py:246: 

Any idea why this would be failing here but not for you folks?

@jchodera
Copy link
Member Author

jchodera commented Dec 1, 2022

I have confirmed locally that I do get this pint.errors.DefinitionSyntaxError if I try to convert the string into a quantity with openff-toolkit 0.11.4:

>>> openff.units.Quantity('[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lila/home/chodera/miniconda/envs/hydration/lib/python3.10/site-packages/pint/facets/plain/quantity.py", line 209, in __new__
    inst = ureg.parse_expression(value)
  File "/lila/home/chodera/miniconda/envs/hydration/lib/python3.10/site-packages/pint/facets/plain/registry.py", line 1255, in parse_expression
    return build_eval_tree(gen).evaluate(
  File "/lila/home/chodera/miniconda/envs/hydration/lib/python3.10/site-packages/pint/pint_eval.py", line 114, in evaluate
    left = self.left.evaluate(define_op, bin_op, un_op)
  File "/lila/home/chodera/miniconda/envs/hydration/lib/python3.10/site-packages/pint/pint_eval.py", line 114, in evaluate
    left = self.left.evaluate(define_op, bin_op, un_op)
  File "/lila/home/chodera/miniconda/envs/hydration/lib/python3.10/site-packages/pint/pint_eval.py", line 114, in evaluate
    left = self.left.evaluate(define_op, bin_op, un_op)
  [Previous line repeated 4 more times]
  File "/lila/home/chodera/miniconda/envs/hydration/lib/python3.10/site-packages/pint/pint_eval.py", line 120, in evaluate
    raise DefinitionSyntaxError('missing unary operator "%s"' % op_text)
pint.errors.DefinitionSyntaxError: missing unary operator "*"

@mattwthompson
Copy link
Collaborator

This looks like the issue we ran into when Pint bumped up to 0.20 a few weeks ago (the paper trail can be found from openforcefield/openff-toolkit#1444). This was fixed in version 0.11.3 of the toolkit, whose builds include constraints on openff-units and pint that should be self-consistent. Unless there's a compelling reason to support old versions of Pint I'd just try updating this line

micromamba update -y -c conda-forge "openff-toolkit >=0.11.0"

to

         micromamba update -y -c conda-forge "openff-toolkit >=0.11.3" 

Hopefully there's not a compelling reason for the solver to pick 0.11.2 ...

@jchodera
Copy link
Member Author

jchodera commented Dec 1, 2022

@mattwthompson : Thanks for the pointers!
It looks like the solver picked 0.10.6. I'll have to figure out why that happened---one of our dependencies must be pinning to an earlier version still.

@codecov-commenter
Copy link

codecov-commenter commented Dec 1, 2022

Codecov Report

Base: 76.92% // Head: 76.92% // No change to project coverage 👍

Coverage data is based on head (f735bee) compared to base (d78f2b8).
Patch coverage: 100.00% of modified lines in pull request are covered.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #253   +/-   ##
=======================================
  Coverage   76.92%   76.92%           
=======================================
  Files           4        4           
  Lines         845      845           
=======================================
  Hits          650      650           
  Misses        195      195           
Impacted Files Coverage Δ
openmmforcefields/generators/system_generators.py 50.58% <100.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@jchodera jchodera merged commit 1b0dfe8 into main Dec 1, 2022
@jchodera jchodera deleted the fix-#252 branch December 1, 2022 08:07
@ijpulidos ijpulidos added this to the 0.11.3 milestone Oct 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MonteCarloBarostat is added to non-periodic systems if barostat is specified to SystemGenerator

5 participants