diff --git a/docs/releasehistory.rst b/docs/releasehistory.rst index 9c838a263..155ef6027 100644 --- a/docs/releasehistory.rst +++ b/docs/releasehistory.rst @@ -7,6 +7,28 @@ Releases follow the ``major.minor.micro`` scheme recommended by `PEP440 `_: Fixes a bug in which calling + :py:meth:`RDKitToolkitWrapper.from_file ` directly + would not load files correctly if passed lowercase `file_format`. Note that this bug did not occur when calling + :`Molecule.from_file` `. +- `PR #631 `_: Fixes a bug in which calling + :py:meth:`openforcefield.utils.utils.utils.unit_to_string ` returned + ``None`` when the unit is dimensionless. Now ``"dimensionless"`` is returned. +- `PR #630 `_: Closes issue `Issue #629 + `_ in which the wrong exception is raised when + attempting to instantiate a ``ForceField`` from an unparsable string. + +New features +"""""""""""" +- `PR #632 `_: Adds + :py:meth:`ForceField.registered_parameter_handlers + ` + 0.7.0 - Charge Increment Model, Proper Torsion interpolation, and new Molecule methods -------------------------------------------------------------------------------------- diff --git a/openforcefield/tests/test_forcefield.py b/openforcefield/tests/test_forcefield.py index c3419c1e4..eaf81e6ff 100644 --- a/openforcefield/tests/test_forcefield.py +++ b/openforcefield/tests/test_forcefield.py @@ -617,6 +617,12 @@ def test_create_forcefield_from_file(self): assert len(forcefield._parameter_handlers['ImproperTorsions']._parameters) == 4 assert len(forcefield._parameter_handlers['vdW']._parameters) == 35 + def test_load_bad_string(self): + with pytest.raises(IOError) as exception_info: + ForceField('1234') + assert 'Source 1234 could not be read.' in str(exception_info.value) + assert 'syntax error' in str(exception_info.value) + @pytest.mark.skip(reason='Needs to be updated for 0.2.0 syntax') def test_create_forcefield_from_file_list(self): # These offxml files are located in package data path, which is automatically installed and searched @@ -1211,6 +1217,26 @@ def test_nonbonded_method_resolution(self, forcefield.get_parameter_handler('Electrostatics', {}).method = electrostatics_method omm_system = forcefield.create_openmm_system(topology) + def test_registered_parameter_handlers(self): + """Test registered_parameter_handlers property""" + forcefield = ForceField('test_forcefields/smirnoff99Frosst.offxml') + registered_handlers = forcefield.registered_parameter_handlers + + expected_handlers = [ + 'Bonds', + 'Angles', + 'ProperTorsions', + 'ImproperTorsions', + 'vdW', + 'Electrostatics', + 'ToolkitAM1BCC', + ] + + for expected_handler in expected_handlers: + assert expected_handler in registered_handlers + + assert 'LibraryChrages' not in registered_handlers + def test_parameter_handler_lookup(self): """Ensure __getitem__ lookups work""" forcefield = ForceField('test_forcefields/smirnoff99Frosst.offxml') diff --git a/openforcefield/tests/test_toolkits.py b/openforcefield/tests/test_toolkits.py index 4601cd55f..9e876d154 100644 --- a/openforcefield/tests/test_toolkits.py +++ b/openforcefield/tests/test_toolkits.py @@ -469,6 +469,21 @@ def test_load_multiconformer_sdf_as_separate_molecules_properties(self): np.testing.assert_allclose(molecules[1].partial_charges / unit.elementary_charge, [0.027170, 0.027170, 0.027170, 0.027170, -0.108680]) + @pytest.mark.skipif(not OpenEyeToolkitWrapper.is_available(), reason='OpenEye Toolkit not available') + def test_file_extension_case(self): + """ + Test round-trips of some file extensions when called directly from the toolkit wrappers, + including lower- and uppercase file extensions. Note that this test does not ensure + accuracy, it only tests that reading/writing without raising an exception. + """ + mols_in = OpenEyeToolkitWrapper().from_file(file_path=get_data_file_path('molecules/ethanol.sdf'), file_format='sdf') + + assert len(mols_in) > 0 + + mols_in = OpenEyeToolkitWrapper().from_file(file_path=get_data_file_path('molecules/ethanol.sdf'), file_format='SDF') + + assert len(mols_in) > 0 + @pytest.mark.skipif(not OpenEyeToolkitWrapper.is_available(), reason='OpenEye Toolkit not available') def test_write_sdf_charges(self): """Test OpenEyeToolkitWrapper for writing partial charges to a sdf file""" @@ -1397,7 +1412,22 @@ def test_to_from_rdkit_core_props_unset(self): assert molecule2.partial_charges is None assert molecule2.to_smiles(toolkit_registry=toolkit_wrapper) == expected_output_smiles - + + @pytest.mark.skipif(not RDKitToolkitWrapper.is_available(), reason='RDKit Toolkit not available') + def test_file_extension_case(self): + """ + Test round-trips of some file extensions when called directly from the toolkit wrappers, + including lower- and uppercase file extensions. Note that this test does not ensure + accuracy, it only tests that reading/writing without raising an exception. + """ + mols_in = RDKitToolkitWrapper().from_file(file_path=get_data_file_path('molecules/ethanol.sdf'), file_format='sdf') + + assert len(mols_in) > 0 + + mols_in = RDKitToolkitWrapper().from_file(file_path=get_data_file_path('molecules/ethanol.sdf'), file_format='SDF') + + assert len(mols_in) > 0 + @pytest.mark.skipif(not RDKitToolkitWrapper.is_available(), reason='RDKit Toolkit not available') def test_get_sdf_coordinates(self): """Test RDKitToolkitWrapper for importing a single set of coordinates from a sdf file""" diff --git a/openforcefield/tests/test_utils.py b/openforcefield/tests/test_utils.py index 24b5d9112..d8874737e 100644 --- a/openforcefield/tests/test_utils.py +++ b/openforcefield/tests/test_utils.py @@ -71,3 +71,11 @@ def test_ast_eval(unit_string, expected_unit): ast_root_node = ast.parse(unit_string, mode='eval').body parsed_units = _ast_eval(ast_root_node) assert parsed_units == expected_unit + +def test_dimensionless_units(): + assert utils.string_to_unit('dimensionless') == unit.dimensionless + + unit_string = utils.unit_to_string(unit.dimensionless) + unit_value = utils.string_to_unit(unit_string) + + assert unit_value == unit.dimensionless diff --git a/openforcefield/typing/engines/smirnoff/forcefield.py b/openforcefield/typing/engines/smirnoff/forcefield.py index 6ab10266a..6739d84d0 100644 --- a/openforcefield/typing/engines/smirnoff/forcefield.py +++ b/openforcefield/typing/engines/smirnoff/forcefield.py @@ -556,6 +556,19 @@ def register_parameter_io_handler(self, parameter_io_handler): self._parameter_io_handlers[io_format])) self._parameter_io_handlers[io_format] = parameter_io_handler + @property + def registered_parameter_handlers(self): + """ + Return the list of registered parameter handlers by name + + .. warning :: This API is experimental and subject to change. + + Returns + ------- + registered_parameter_handlers: iterable of names of ParameterHandler objects in this ForceField + + """ + return [*self._parameter_handlers.keys()] # TODO: Do we want to make this optional? @@ -970,14 +983,14 @@ def parse_smirnoff_from_source(self, source): smirnoff_data = parameter_io_handler.parse_file(source) return smirnoff_data except ParseError as e: - exception_msg = str(e) + exception_msg = e.msg except (FileNotFoundError, OSError): # If this is not a file path or a file handle, attempt parsing as a string. try: smirnoff_data = parameter_io_handler.parse_string(source) return smirnoff_data except ParseError as e: - exception_msg = str(e) + exception_msg = e.msg # If we haven't returned by now, the parsing was unsuccessful valid_formats = [ diff --git a/openforcefield/typing/engines/smirnoff/io.py b/openforcefield/typing/engines/smirnoff/io.py index 9f0173138..fb8e1dcb9 100644 --- a/openforcefield/typing/engines/smirnoff/io.py +++ b/openforcefield/typing/engines/smirnoff/io.py @@ -204,7 +204,7 @@ def parse_string(self, data): smirnoff_data = xmltodict.parse(data, attr_prefix='') return smirnoff_data except ExpatError as e: - raise ParseError(e) + raise ParseError(str(e)) def to_file(self, file_path, smirnoff_data): """Write the current forcefield parameter set to a file. diff --git a/openforcefield/utils/toolkits.py b/openforcefield/utils/toolkits.py index ec3fddf6a..e4b6f1963 100644 --- a/openforcefield/utils/toolkits.py +++ b/openforcefield/utils/toolkits.py @@ -677,7 +677,7 @@ def to_file(self, molecule, file_path, file_format): from openeye import oechem oemol = self.to_openeye(molecule) ofs = oechem.oemolostream(file_path) - openeye_format = getattr(oechem, 'OEFormat_' + file_format) + openeye_format = getattr(oechem, 'OEFormat_' + file_format.upper()) ofs.SetFormat(openeye_format) # OFFTK strictly treats SDF as a single-conformer format. @@ -2364,6 +2364,9 @@ def from_file(self, """ from rdkit import Chem + + file_format = file_format.upper() + mols = list() if (file_format == 'MOL') or (file_format == 'SDF'): for rdmol in Chem.SupplierFromFilename(file_path, removeHs=False, sanitize=False, strictParsing=True): diff --git a/openforcefield/utils/utils.py b/openforcefield/utils/utils.py index 0cea73c16..074e87c5e 100644 --- a/openforcefield/utils/utils.py +++ b/openforcefield/utils/utils.py @@ -146,6 +146,8 @@ def unit_to_string(input_unit): The serialized unit. """ + if input_unit == unit.dimensionless: + return "dimensionless" # Decompose output_unit into a tuples of (base_dimension_unit, exponent) unit_string = None