Skip to content

Commit b3c492f

Browse files
committed
Initial untested implementation of LibraryChargeHandler
1 parent f49b43b commit b3c492f

File tree

3 files changed

+203
-53
lines changed

3 files changed

+203
-53
lines changed

openforcefield/tests/test_forcefield.py

Lines changed: 108 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,17 @@
156156
xml_toolkitam1bcc_ff = '''
157157
<SMIRNOFF version="0.3" aromaticity_model="OEAroModel_MDL">
158158
<ToolkitAM1BCC version="0.3"/>
159-
</SMIRNOFF>'''
159+
</SMIRNOFF>
160+
'''
160161

162+
xml_tip3p_library_charges_ff = '''
163+
<SMIRNOFF version="0.3" aromaticity_model="OEAroModel_MDL">
164+
<LibraryCharges version="0.3">
165+
<!-- TIP3P water oxygen with charge override -->
166+
<LibraryCharge name="TIP3P" smirks="[#1:1]-[#8X2H2+0:2]-[#1:3]" charge1="+0.417*elementary_charge" charge2="-0.834*elementary_charge" charge3="+0.417*elementary_charge"/>
167+
</LibraryCharges>
168+
</SMIRNOFF>
169+
'''
161170
#======================================================================
162171
# TEST UTILITY FUNCTIONS
163172
#======================================================================
@@ -806,6 +815,70 @@ def test_parameterize_ethanol_to_parmed(self):
806815

807816
parmed_system = forcefield.create_parmed_structure(topology, positions=pdbfile.getPositions())
808817

818+
819+
820+
821+
@pytest.mark.parametrize("toolkit_registry,registry_description", toolkit_registries)
822+
def test_pass_invalid_kwarg_to_create_openmm_system(self, toolkit_registry, registry_description):
823+
"""Test to ensure an exception is raised when an unrecognized kwarg is passed """
824+
from simtk.openmm import app
825+
826+
file_path = get_data_file_path('test_forcefields/smirnoff99Frosst.offxml')
827+
forcefield = ForceField(file_path)
828+
pdbfile = app.PDBFile(get_data_file_path('systems/test_systems/1_ethanol.pdb'))
829+
molecules = []
830+
molecules.append(Molecule.from_smiles('CCO'))
831+
topology = Topology.from_openmm(pdbfile.topology, unique_molecules=molecules)
832+
833+
with pytest.raises(ValueError, match=".* not used by any registered force Handler: {'invalid_kwarg'}.*") as e:
834+
omm_system = forcefield.create_openmm_system(topology, invalid_kwarg='aaa', toolkit_registry=toolkit_registry)
835+
836+
837+
@pytest.mark.parametrize("inputs", nonbonded_resolution_matrix)
838+
def test_nonbonded_method_resolution(self,
839+
inputs
840+
):
841+
"""Test predefined permutations of input options to ensure nonbonded handling is correctly resolved"""
842+
from simtk.openmm import app
843+
844+
vdw_method = inputs['vdw_method']
845+
electrostatics_method = inputs['electrostatics_method']
846+
has_periodic_box = inputs['has_periodic_box']
847+
omm_force = inputs['omm_force']
848+
exception = inputs['exception']
849+
exception_match= inputs['exception_match']
850+
851+
molecules = [create_ethanol()]
852+
forcefield = ForceField('test_forcefields/smirnoff99Frosst.offxml')
853+
854+
pdbfile = app.PDBFile(get_data_file_path('systems/test_systems/1_ethanol.pdb'))
855+
topology = Topology.from_openmm(pdbfile.topology, unique_molecules=molecules)
856+
857+
if not(has_periodic_box):
858+
topology.box_vectors = None
859+
860+
if exception is None:
861+
# The method is validated and may raise an exception if it's not supported.
862+
forcefield.get_parameter_handler('vdW', {}).method = vdw_method
863+
forcefield.get_parameter_handler('Electrostatics', {}).method = electrostatics_method
864+
omm_system = forcefield.create_openmm_system(topology)
865+
nonbond_method_matched = False
866+
for f_idx in range(omm_system.getNumForces()):
867+
force = omm_system.getForce(f_idx)
868+
if isinstance(force, openmm.NonbondedForce):
869+
if force.getNonbondedMethod() == omm_force:
870+
nonbond_method_matched = True
871+
assert nonbond_method_matched
872+
else:
873+
with pytest.raises(exception, match=exception_match) as excinfo:
874+
# The method is validated and may raise an exception if it's not supported.
875+
forcefield.get_parameter_handler('vdW', {}).method = vdw_method
876+
forcefield.get_parameter_handler('Electrostatics', {}).method = electrostatics_method
877+
omm_system = forcefield.create_openmm_system(topology)
878+
879+
880+
881+
class TestForceFieldChargeAssignment:
809882
@pytest.mark.parametrize("toolkit_registry,registry_description", toolkit_registries)
810883
def test_charges_from_molecule(self, toolkit_registry, registry_description):
811884
"""Test skipping charge generation and instead getting charges from the original Molecule"""
@@ -878,65 +951,49 @@ def test_some_charges_from_molecule(self, toolkit_registry, registry_description
878951
q, sigma, epsilon = nonbondedForce.getParticleParameters(particle_index)
879952
assert q != (0. * unit.elementary_charge)
880953

954+
def test_library_charges_to_single_water(self):
955+
"""Test assigning charges to one water molecule using library charges"""
956+
from simtk.openmm import NonbondedForce
881957

958+
ff = ForceField(simple_xml_ff, xml_tip3p_library_charges_ff)
959+
mol = Molecule.from_smiles('O')
960+
omm_system = ff.create_openmm_system(mol.to_topology())
961+
nonbondedForce = [f for f in omm_system.getForces() if type(f) == NonbondedForce][0]
962+
expected_charges = [-0.834, 0.417, 0.417] * unit.elementary_charge
963+
for particle_index, expected_charge in expected_charges:
964+
q, sigma, epsilon = nonbondedForce.getParticleParameters(particle_index)
965+
assert q == expected_charge
882966

883-
@pytest.mark.parametrize("toolkit_registry,registry_description", toolkit_registries)
884-
def test_pass_invalid_kwarg_to_create_openmm_system(self, toolkit_registry, registry_description):
885-
"""Test to ensure an exception is raised when an unrecognized kwarg is passed """
886-
from simtk.openmm import app
967+
def test_library_charges_to_two_waters(self):
968+
"""Test assigning charges to two water molecules using library charges"""
969+
pass
887970

888-
file_path = get_data_file_path('test_forcefields/smirnoff99Frosst.offxml')
889-
forcefield = ForceField(file_path)
890-
pdbfile = app.PDBFile(get_data_file_path('systems/test_systems/1_ethanol.pdb'))
891-
molecules = []
892-
molecules.append(Molecule.from_smiles('CCO'))
893-
topology = Topology.from_openmm(pdbfile.topology, unique_molecules=molecules)
971+
def test_library_charges_to_two_ethanols_different_atom_ordering(self):
972+
"""Test assigning charges to two ethanols with different atom orderings"""
973+
pass
894974

895-
with pytest.raises(ValueError, match=".* not used by any registered force Handler: {'invalid_kwarg'}.*") as e:
896-
omm_system = forcefield.create_openmm_system(topology, invalid_kwarg='aaa', toolkit_registry=toolkit_registry)
975+
def test_library_charges_to_only_some_molecules(self):
976+
"""Test assigning charges to a topology in which some molecules are covered by library charges
977+
but other aren't"""
978+
pass
897979

980+
def test_assign_charges_to_molecule_in_parts_using_multiple_library_charges(self):
981+
"""Test assigning charges to parts of a molecule using two library charge lines"""
982+
pass
898983

899-
@pytest.mark.parametrize("inputs", nonbonded_resolution_matrix)
900-
def test_nonbonded_method_resolution(self,
901-
inputs
902-
):
903-
"""Test predefined permutations of input options to ensure nonbonded handling is correctly resolved"""
904-
from simtk.openmm import app
984+
def test_assign_charges_using_library_charges_by_single_atoms(self):
985+
"""Test assigning charges to parts of a molecule using per-atom library charges"""
986+
pass
905987

906-
vdw_method = inputs['vdw_method']
907-
electrostatics_method = inputs['electrostatics_method']
908-
has_periodic_box = inputs['has_periodic_box']
909-
omm_force = inputs['omm_force']
910-
exception = inputs['exception']
911-
exception_match= inputs['exception_match']
988+
def test_library_charges_dont_parameterize_molecule_because_of_incomplete_coverage(self):
989+
"""Fail to assign charges to a molecule becau\se not all atoms can be assigned"""
990+
pass
912991

913-
molecules = [create_ethanol()]
914-
forcefield = ForceField('test_forcefields/smirnoff99Frosst.offxml')
992+
def library_charges_overridden_by_charge_from_molecules(self):
993+
"""Skip assigning charges to a molecule because it has already had charges assigned
994+
using charge_from_molecules"""
995+
pass
915996

916-
pdbfile = app.PDBFile(get_data_file_path('systems/test_systems/1_ethanol.pdb'))
917-
topology = Topology.from_openmm(pdbfile.topology, unique_molecules=molecules)
918-
919-
if not(has_periodic_box):
920-
topology.box_vectors = None
921-
922-
if exception is None:
923-
# The method is validated and may raise an exception if it's not supported.
924-
forcefield.get_parameter_handler('vdW', {}).method = vdw_method
925-
forcefield.get_parameter_handler('Electrostatics', {}).method = electrostatics_method
926-
omm_system = forcefield.create_openmm_system(topology)
927-
nonbond_method_matched = False
928-
for f_idx in range(omm_system.getNumForces()):
929-
force = omm_system.getForce(f_idx)
930-
if isinstance(force, openmm.NonbondedForce):
931-
if force.getNonbondedMethod() == omm_force:
932-
nonbond_method_matched = True
933-
assert nonbond_method_matched
934-
else:
935-
with pytest.raises(exception, match=exception_match) as excinfo:
936-
# The method is validated and may raise an exception if it's not supported.
937-
forcefield.get_parameter_handler('vdW', {}).method = vdw_method
938-
forcefield.get_parameter_handler('Electrostatics', {}).method = electrostatics_method
939-
omm_system = forcefield.create_openmm_system(topology)
940997

941998
#======================================================================
942999
# TEST CONSTRAINTS

openforcefield/tests/test_parameters.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from openforcefield.typing.engines.smirnoff.parameters import (
2222
ParameterAttribute, IndexedParameterAttribute, ParameterList,
2323
ParameterType, BondHandler, ParameterHandler, ProperTorsionHandler,
24-
ImproperTorsionHandler, GBSAHandler, SMIRNOFFSpecError,
24+
ImproperTorsionHandler, LibraryChargeHandler, GBSAHandler, SMIRNOFFSpecError,
2525
_ParameterAttributeHandler
2626
)
2727
from openforcefield.utils import detach_units, IncompatibleUnitError
@@ -931,6 +931,13 @@ def test_torsion_handler_charmm_potential(self):
931931
ph1 = ImproperTorsionHandler(potential='charmm', skip_version_check=True)
932932
ph1 = ImproperTorsionHandler(potential='k*(1+cos(periodicity*theta-phase))', skip_version_check=True)
933933

934+
class TestLibraryChargeHandler:
935+
def test_create_library_charge_handler(self):
936+
"""Test creation of an empty LibraryChargeHandler"""
937+
handler = LibraryChargeHandler()
938+
939+
940+
934941
class TestGBSAHandler:
935942
def test_create_default_gbsahandler(self):
936943
"""Test creation of an empty GBSAHandler, with all default attributes"""

openforcefield/typing/engines/smirnoff/parameters.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2568,7 +2568,7 @@ class ToolkitAM1BCCHandler(ParameterHandler):
25682568

25692569
_TAGNAME = 'ToolkitAM1BCC' # SMIRNOFF tag name to process
25702570
_OPENMMTYPE = openmm.NonbondedForce # OpenMM force class to create or utilize
2571-
_DEPENDENCIES = [vdWHandler] # vdWHandler must first run NonBondedForce.addParticle for each particle in the topology
2571+
_DEPENDENCIES = [vdWHandler, LibraryChargeHandler] # vdWHandler must first run NonBondedForce.addParticle for each particle in the topology
25722572
_KWARGS = ['charge_from_molecules', 'toolkit_registry'] # Kwargs to catch when create_force is called
25732573

25742574
def check_handler_compatibility(self,
@@ -2737,6 +2737,92 @@ def postprocess_system(self, system, topology, **kwargs):
27372737
# TODO: Calculate exceptions
27382738

27392739

2740+
2741+
class LibraryChargeHandler(ParameterHandler):
2742+
"""Handle SMIRNOFF ``<LibraryCharges>`` tags
2743+
2744+
.. warning :: This API is experimental and subject to change.
2745+
"""
2746+
2747+
class LibraryChargeType(ParameterType):
2748+
"""A SMIRNOFF Library Charge type.
2749+
2750+
.. warning :: This API is experimental and subject to change.
2751+
"""
2752+
_VALENCE_TYPE = 'Atom' # ChemicalEnvironment valence type expected for SMARTS
2753+
_ELEMENT_NAME = 'LibraryCharge'
2754+
2755+
charge = IndexedParameterAttribute(unit=unit.elementary_charge)
2756+
2757+
2758+
_TAGNAME = 'LibraryCharges' # SMIRNOFF tag name to process
2759+
_INFOTYPE = LibraryChargeType # info type to store
2760+
_OPENMMTYPE = openmm.NonbondedForce # OpenMM force class to create
2761+
_DEPENDENCIES = [vdWHandler] # vdWHandler must first run NonBondedForce.addParticle for each particle in the topology
2762+
2763+
2764+
def create_force(self, system, topology, **kwargs):
2765+
existing = [system.getForce(i) for i in range(system.getNumForces())]
2766+
existing = [f for f in existing if type(f) == self._OPENMMTYPE]
2767+
if len(existing) == 0:
2768+
force = self._OPENMMTYPE()
2769+
system.addForce(force)
2770+
else:
2771+
force = existing[0]
2772+
2773+
# Iterate over all defined library charge parameters, allowing later matches to override earlier ones.
2774+
atom_matches = self.find_matches(topology)
2775+
2776+
# Create a set of all the topology atom indices for which library charges can be applied
2777+
assignable_atoms = set()
2778+
atom_assignments = dict()
2779+
2780+
# TODO: This assumes that later matches should always override earlier ones. This may require more
2781+
# thought, since matches can be partially overlapping
2782+
for topology_indices, library_charge in atom_matches.items():
2783+
for charge_idx, top_idx in enumerate(topology_indices):
2784+
if top_idx in assignable_atoms:
2785+
logger.debug(f'Multiple library charge assignments found for atom {top_idx}')
2786+
assignable_atoms.add(top_idx)
2787+
atom_assignments[top_idx] = library_charge.charge[charge_idx]
2788+
#assignable_atoms = {top_idx for top_idx in top_idx_tuple for top_idx_tuple in atom_matches.keys()}
2789+
#atom_assignments =
2790+
2791+
# TODO: Should header include a residue separator delimiter? Maybe not, since it's not clear how having
2792+
# multiple LibraryChargeHandlers could return a single set of matches, while respecting different
2793+
# separators.
2794+
2795+
# Check to see whether the set contains any complete molecules, and remove the matches if not.
2796+
for top_mol in topology.topology_molecules:
2797+
# Check whether any atom in the molecule already has a charge. If so, skip it
2798+
for top_atom in top_mol.topology_atoms:
2799+
q, _, _2 = force.getParticleParameters(top_atom.topology_atom_index)
2800+
if q != 0 * unit.elementary_charge:
2801+
logger.debug('Original molecule has at least one atom with existing charge. Skipping library '
2802+
'charge assignment')
2803+
continue
2804+
2805+
# Ensure all of the atoms in this mol are covered, otherwise skip it
2806+
if set(top_mol.topology_atoms).intersection(assignable_atoms) != top_mol.n_atoms:
2807+
logger.debug('Entire molecule is not covered. Skipping library charge assignment.')
2808+
continue
2809+
2810+
# If we pass both tests above, go ahead and assign charges
2811+
# TODO: We could probably save a little time by looking up this TopologyMolecule's _reference molecule_
2812+
# and assigning charges to all other instances of it in this topoligy
2813+
for top_atom in top_mol.topology_atoms:
2814+
_, sigma, epsilon = force.getParticleParameters(top_atom.topology_atom_index)
2815+
force.setParticleParameters(top_atom.topology_atom_index,
2816+
atom_assignments[top_atom.topology_atom_index],
2817+
sigma,
2818+
epsilon)
2819+
2820+
2821+
# TODO: Can we express separate constraints for postprocessing and normal processing?
2822+
def postprocess_system(self, system, topology, **kwargs):
2823+
pass
2824+
2825+
27402826
class ChargeIncrementModelHandler(ParameterHandler):
27412827
"""Handle SMIRNOFF ``<ChargeIncrementModel>`` tags
27422828

0 commit comments

Comments
 (0)