Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ChargeIncrementModelHandler #471

Merged
merged 86 commits into from
Jun 19, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
28cb5a6
Fix typo in chargeincrementhandler example offxml
j-wags Dec 4, 2019
0c58167
Add basic tests for chargeincrementmodelhandler
j-wags Dec 4, 2019
c0308f0
Fix smirnoff docs CIM example
j-wags Dec 5, 2019
5f7dae8
Update basic CIMH tests
j-wags Dec 5, 2019
28680ba
Add method compatibility matrix to ATTKW
j-wags Dec 5, 2019
3a26190
Implemented partial charge method resolution test
j-wags Dec 7, 2019
643a750
standardize partial charge calc apis
j-wags Dec 7, 2019
1e369b4
standardize partial charge calc "raises"es
j-wags Dec 7, 2019
3ff5783
fix variable name
j-wags Dec 7, 2019
71a097f
Resolve #348
j-wags Dec 11, 2019
7268ec1
remove qc_method from CIM API, default n_confs=1
j-wags Dec 11, 2019
4d865f7
Resolve #465
j-wags Dec 11, 2019
c9d63b2
Update tests for #348
j-wags Dec 11, 2019
96629df
Correct error types in charge calc, add tests for new charge methods
j-wags Dec 11, 2019
c44d990
Add builtintoolkitwrapper and tests
j-wags Dec 11, 2019
6153fdd
pass partial_charge_method through in molecule.calc_part_charges
j-wags Dec 11, 2019
880faec
update release history
j-wags Dec 11, 2019
ddd69e8
update CIM spec docs to default n_confs 1 and remove quantum method
j-wags Dec 12, 2019
84fd4ea
Fix CIM spec example
j-wags Dec 14, 2019
2611145
Enforce use of dict to prevent chargeincrement SMIRKS from getting in…
j-wags Dec 14, 2019
0711222
Finish initial implementation of chargeincrementmodelhandler
j-wags Dec 14, 2019
e3e47fb
add create_reversed_ethanol and CIM test
j-wags Dec 14, 2019
8826d06
fix tests for changes in #465. Atom.formal_charge accepts int or Quant
j-wags Dec 19, 2019
3f6fbb8
clarify reason for overriding transformed_dict_cls in CIMH
j-wags Dec 19, 2019
f3840fb
Fix tests for new API/defaults to CIMH
j-wags Dec 19, 2019
6a22604
Update error handling in molecule.__init__ for more specific exceptions
j-wags Dec 19, 2019
12aba0e
Relax charge sum tolerance in tests
j-wags Dec 21, 2019
b8b7b31
Merge branch 'master' into chargeincrementhandler
j-wags Dec 21, 2019
8ac2cfb
Drafting release history for CIMH
j-wags Jan 6, 2020
8fb0cd7
Extend input validation tests for CIMH
j-wags Jan 6, 2020
cc600d5
Make ToolkitRegistry.call handle NotImplementedErrors gracefully
j-wags Jan 6, 2020
148b0eb
Merge remote-tracking branch 'origin/chargeincrementhandler' into cha…
j-wags Jan 6, 2020
be72b0a
Merge branch 'master' into chargeincrementhandler
j-wags Jan 7, 2020
8cc98dd
add compute_partial_charges kwarg strict_n_conformers
j-wags Jan 15, 2020
66f7309
fix syntax errors
j-wags Jan 16, 2020
5a42f4b
Merge branch 'master' into chargeincrementhandler
j-wags Feb 21, 2020
9eb15fd
Add compute_partial_charges_am1bcc deprecation warning
j-wags Feb 22, 2020
5ad3a44
switch ambertools partial charge calc to use subprocess
j-wags Feb 22, 2020
c762a3b
merge and resolve conflicts
j-wags Feb 24, 2020
5fb3fac
change compute_partial_charges to assign_partial_charges
j-wags Feb 25, 2020
0c3e5ec
Add rms_cutoff kwarg to generate_conformers
j-wags Feb 25, 2020
0b20a49
Fix to_qcschema formal charge unit
j-wags Feb 25, 2020
75c9e99
rename tests for assign_partial_charges, add tests for rms_cutoff
j-wags Feb 25, 2020
5293fbb
implement assign_partial_charges for toolkits, major refactors, tests
j-wags Feb 29, 2020
c0f7f70
Merge branch 'master' into chargeincrementhandler
j-wags Mar 3, 2020
14e951d
Merge branch 'master' into chargeincrementhandler
j-wags Apr 14, 2020
110a97d
document deduplication scheme for chargeincrementmodel
j-wags Apr 21, 2020
5d6c4c4
progress commit
j-wags Apr 24, 2020
ed09582
add tests for different behaviors of cimh matches/hierarchy
j-wags Apr 28, 2020
5fc2260
allow parameterhandlers to be initialized with no parameters
j-wags Apr 29, 2020
973b29c
change error to warning if CIMH or TKAM1BCCH cant assign charges
j-wags Apr 29, 2020
89cc2d2
add charge method hierarchy test for CIMH (comes after TKAM1BCCH)
j-wags Apr 29, 2020
1b55c4c
enforce n_tagged_atoms==idxed_attrs, behavior tests for CIMH and LibChgH
j-wags Apr 29, 2020
c79005c
add conformer dependence tests
j-wags May 1, 2020
9f898d6
Have individual toolkitwrappers raise specific exception types, but t…
j-wags May 28, 2020
9eceb31
update qcschema input to have unitless total charge
j-wags May 28, 2020
ba95d59
Merge branch 'master' into chargeincrementhandler
j-wags May 28, 2020
9bd877e
add raise_first_error kwarg to TKR.call
j-wags May 28, 2020
7021722
Update tests and properties for formal charge units
j-wags May 28, 2020
cca65e9
make toolkit read and write formats INSTANCE attributes of TKWs
j-wags May 28, 2020
2631a36
switch lc and ci validation to use minimal chemicalenvironment class
j-wags May 28, 2020
4d21f4f
drafting release notes
j-wags May 29, 2020
131d566
Further updates to releasenotes
j-wags May 30, 2020
f2a3398
fix LGTM complaints
j-wags Jun 2, 2020
c5758d2
molecule.gen_confs with kwarg n_confs=0 now doesn't use tkw, but inst…
j-wags Jun 2, 2020
73f9a6f
update expected error types for mol.assign_partial_charges
j-wags Jun 2, 2020
70ad705
fix exception types expected in oe tests
j-wags Jun 2, 2020
3278f78
refactored ToolkitRegistry.call to use raise_exception_types
j-wags Jun 3, 2020
783a11b
toolkit.py cleanup and fix test_partial_charge_res toolkit avail checks
j-wags Jun 3, 2020
1afcd66
attkw checks whether rdkit is available
j-wags Jun 3, 2020
cb3a423
fix some imports
j-wags Jun 3, 2020
216ac97
add temp_cd back (#595) for antechamber methods
j-wags Jun 4, 2020
dd79fb2
make toolkitam1bcc charge calc errors non fatal
j-wags Jun 4, 2020
61143f9
make it so compute_partial_charges_am1bcc still returns charges as we…
j-wags Jun 5, 2020
a59d77e
Cleanups in preparation for review
j-wags Jun 9, 2020
0a465fc
simplify release notes
j-wags Jun 9, 2020
e89687a
fix docstring example
j-wags Jun 9, 2020
3d3c9ef
Merge branch 'master' into chargeincrementhandler
j-wags Jun 9, 2020
92c0df4
Small cosmetic fixes
dotsdl Jun 16, 2020
982fa49
Merge branch 'master' into chargeincrementhandler
j-wags Jun 17, 2020
d7e092a
Update openforcefield/typing/engines/smirnoff/parameters.py
j-wags Jun 17, 2020
49d4dc3
add fixes per Dotson review
j-wags Jun 18, 2020
fff6051
Merge remote-tracking branch 'origin/chargeincrementhandler' into cha…
j-wags Jun 18, 2020
e363a70
add more fixes per Dotson review
j-wags Jun 19, 2020
e910488
Merge branch 'master' into chargeincrementhandler
j-wags Jun 19, 2020
efe98bb
Merge branch 'master' into chargeincrementhandler
j-wags Jun 19, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions The-SMIRNOFF-force-field-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,21 +231,20 @@ In keeping with the AMBER force field philosophy, especially as implemented in s

Here is an example:
```XML
<ChargeIncrementModel version="0.3" number_of_conformers="10" quantum_chemical_method="AM1" partial_charge_method="CM2">
<ChargeIncrementModel version="0.3" number_of_conformers="1" partial_charge_method="AM1-Mulliken">
j-wags marked this conversation as resolved.
Show resolved Hide resolved
<!-- A fractional charge can be moved along a single bond -->
<ChargeIncrement smirks="[#6X4:1]-[#6X3a:2]" charge_increment1="-0.0073*elementary_charge" charge_increment2="0.0073*elementary_charge"/>
<ChargeIncrement smirks="[#6X4:1]-[#6X3a:2]-[#7]" charge_increment1=+0.0943*elementary_charge" charge_increment2="-0.0943*elementary_charge"/>
<ChargeIncrement smirks="[#6X4:1]-[#6X3a:2]-[#7]" charge_increment1="0.0943*elementary_charge" charge_increment2="-0.0943*elementary_charge"/>
<ChargeIncrement smirks="[#6X4:1]-[#8:2]" charge_increment1="-0.0718*elementary_charge" charge_increment2="0.0718*elementary_charge"/>
<!--- Alternatively, fractional charges can be redistributed among any number of bonded atoms -->
<ChargeIncrement smirks="[N:1](H:2)(H:3)" charge_increment1="0.02*elementary_charge" charge_increment2="-0.01*elementary_charge" charge_increment3="-0.01*elementary_charge"/>
<ChargeIncrement smirks="[N:1]([H:2])([H:3])" charge_increment1="0.02*elementary_charge" charge_increment2="-0.01*elementary_charge" charge_increment3="-0.01*elementary_charge"/>
</ChargeIncrementModel>
```
The sum of formal charges for the molecule or fragment will be used to determine the total charge the molecule or fragment will possess.

`<ChargeIncrementModel>` provides several optional attributes to control its behavior:
* The `number_of_conformers` attribute (default: `"10"`) is used to specify how many conformers will be generated for the molecule (or capped fragment) prior to charging.
* The `quantum_chemical_method` attribute (default: `"AM1"`) is used to specify the quantum chemical method applied to the molecule or capped fragment.
* The `partial_charge_method` attribute (default: `"Mulliken"`) is used to specify how uncorrected partial charges are to be generated from the quantum chemical wavefunction. Later additions will add restrained electrostatic potential fitting (RESP) capabilities.
* The `number_of_conformers` attribute (default: `"1"`) is used to specify how many conformers will be generated for the molecule (or capped fragment) prior to charging.
* The `partial_charge_method` attribute (default: `"AM1-Mulliken"`) is used to specify how uncorrected partial charges are to be generated. Later additions will add restrained electrostatic potential fitting (RESP) capabilities.

The `<ChargeIncrement>` tags specify how the quantum chemical derived charges are to be corrected to produce the final charges.
The `charge_increment#` attributes specify how much the charge on the associated tagged atom index (replacing `#`) should be modified.
Expand Down
77 changes: 71 additions & 6 deletions openforcefield/tests/test_forcefield.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@

xml_spec_docs_charge_increment_model_xml = '''
<SMIRNOFF version="0.3" aromaticity_model="OEAroModel_MDL">
<ChargeIncrementModel version="0.3" number_of_conformers="10" quantum_chemical_method="AM1" partial_charge_method="CM2">
<ChargeIncrementModel version="0.3" number_of_conformers="1" partial_charge_method="AM1-Mulliken">
<!-- A fractional charge can be moved along a single bond -->
<ChargeIncrement smirks="[#6X4:1]-[#6X3a:2]" charge_increment1="-0.0073*elementary_charge" charge_increment2="0.0073*elementary_charge"/>
<ChargeIncrement smirks="[#6X4:1]-[#6X3a:2]-[#7]" charge_increment1="0.0943*elementary_charge" charge_increment2="-0.0943*elementary_charge"/>
Expand Down Expand Up @@ -284,6 +284,36 @@ def create_ethanol():
ethanol.partial_charges = charges
return ethanol


def create_reversed_ethanol():
"""
Creates an openforcefield.topology.Molecule representation of
ethanol without the use of a cheminformatics toolkit. This function
reverses the atom indexing of create_ethanol
"""
# Create an ethanol molecule without using a toolkit
ethanol = Molecule()
ethanol.add_atom(1, 0, False) # H0
ethanol.add_atom(1, 0, False) # H1
ethanol.add_atom(1, 0, False) # H2
ethanol.add_atom(1, 0, False) # H3
ethanol.add_atom(1, 0, False) # H4
ethanol.add_atom(1, 0, False) # H5
ethanol.add_atom(8, 0, False) # O6
ethanol.add_atom(6, 0, False) # C7
ethanol.add_atom(6, 0, False) # C8
ethanol.add_bond(8, 7, 1, False) # C8 - C7
ethanol.add_bond(7, 6, 1, False) # C7 - O6
ethanol.add_bond(8, 5, 1, False) # C8 - H5
ethanol.add_bond(8, 4, 1, False) # C8 - H4
ethanol.add_bond(8, 3, 1, False) # C8 - H3
ethanol.add_bond(7, 2, 1, False) # C7 - H2
ethanol.add_bond(7, 1, 1, False) # C7 - H1
ethanol.add_bond(6, 0, 1, False) # O6 - H0
charges = unit.Quantity(np.array([0.4, 0.3, 0.2, 0.1, 0.00001, -0.1, -0.2, -0.3, -0.4]), unit.elementary_charge)
ethanol.partial_charges = charges
return ethanol

def create_cyclohexane():
"""
Creates an openforcefield.topology.Molecule representation of
Expand Down Expand Up @@ -1155,6 +1185,30 @@ def test_parse_charge_increment_model_from_spec_docs(self):
# We should implement something like doctests for the XML snippets on the SMIRNOFF spec page.
ff = ForceField(xml_spec_docs_charge_increment_model_xml)

def test_charge_increment_model_zero_net_charge(self):
"""Test application of ChargeIncrements"""
test_charge_increment_model_ff = '''
<SMIRNOFF version="0.3" aromaticity_model="OEAroModel_MDL">
<Electrostatics version="0.3" method="PME" scale12="0.0" scale13="0.0" scale14="0.833333" cutoff="9.0 * angstrom"/>
<ChargeIncrementModel version="0.3" number_of_conformers="1" partial_charge_method="formal_charge">
<ChargeIncrement smirks="[#6X4:1]-[#8:2]" charge_increment1="-0.05*elementary_charge" charge_increment2="0.05*elementary_charge"/>
<ChargeIncrement smirks="[C:1][C:2][O:3]" charge_increment1="0.2*elementary_charge" charge_increment2="-0.1*elementary_charge" charge_increment3="-0.1*elementary_charge"/>
</ChargeIncrementModel>
</SMIRNOFF>
'''
file_path = get_data_file_path('test_forcefields/smirnoff99Frosst.offxml')

ff = ForceField(file_path, test_charge_increment_model_ff)
del ff._parameter_handlers['ToolkitAM1BCC']
top = Topology.from_molecules([create_ethanol(), create_reversed_ethanol()])
sys = ff.create_openmm_system(top)
nonbonded_force = [force for force in sys.getForces() if isinstance(force, openmm.NonbondedForce)][0]
expected_charges = [0.2, -0.15, -0.05, 0., 0., 0., 0., 0., 0.,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to walk through this one with you, just to make sure I'm thinking about it correctly @j-wags.

0., 0., 0., 0., 0., 0., -0.05, -0.15, 0.2] * unit.elementary_charge
for idx, expected_charge in enumerate(expected_charges):
charge, _, _ = nonbonded_force.getParticleParameters(idx)
assert abs(charge - expected_charge) < 1.e-6 * unit.elementary_charge

@pytest.mark.parametrize("inputs", partial_charge_method_resolution_matrix)
def test_partial_charge_resolution(self, inputs):
"""Check that the proper partial charge methods are available, and that unavailable partial charge methods
Expand Down Expand Up @@ -1223,8 +1277,8 @@ def test_library_charges_to_two_waters(self):
q, sigma, epsilon = nonbondedForce.getParticleParameters(particle_index)
assert q == expected_charge

def test_library_charges_to_two_ethanols_different_atom_ordering(self):
"""Test assigning charges to two ethanols with different atom orderings"""
def test_library_charges_to_three_ethanols_different_atom_ordering(self):
"""Test assigning charges to three ethanols with different atom orderings"""
from simtk.openmm import NonbondedForce

# Define a library charge parameter for ethanol (C1-C2-O3) where C1 has charge -0.2, and its Hs have -0.02,
Expand All @@ -1245,18 +1299,29 @@ def test_library_charges_to_two_ethanols_different_atom_ordering(self):
# H6 - C1 - C3 - O2 - H4
# | |
# H7 H9
#
# create_reversed_ethanol()
# H5 H2
# | |
# H4 - C8 - C7 - O6 - H0
# | |
# H3 H1


molecules = [Molecule.from_file(get_data_file_path('molecules/ethanol.sdf')),
Molecule.from_file(get_data_file_path('molecules/ethanol_reordered.sdf'))]
Molecule.from_file(get_data_file_path('molecules/ethanol_reordered.sdf')),
create_reversed_ethanol()]
top = Topology.from_molecules(molecules)
omm_system = ff.create_openmm_system(top)
nonbondedForce = [f for f in omm_system.getForces() if type(f) == NonbondedForce][0]
expected_charges = [-0.2, -0.1, 0.3, 0.08, -0.02, -0.02, -0.02, -0.01, -0.01, -0.2,
0.3, -0.1, 0.08, -0.02, -0.02, -0.02, -0.01, -0.01] * unit.elementary_charge
expected_charges = [-0.2, -0.1, 0.3, 0.08, -0.02, -0.02, -0.02, -0.01, -0.01,
-0.2, 0.3, -0.1, 0.08, -0.02, -0.02, -0.02, -0.01, -0.01,
0.08, -0.01, -0.01, -0.02, -0.02, -0.02, 0.3, -0.1, -0.2] * unit.elementary_charge
for particle_index, expected_charge in enumerate(expected_charges):
q, sigma, epsilon = nonbondedForce.getParticleParameters(particle_index)
assert q == expected_charge


def test_charge_method_hierarchy(self):
"""Ensure that molecules are parameterized by charge_from_molecules first, then library charges
if not applicable, then AM1BCC otherwise"""
Expand Down
68 changes: 49 additions & 19 deletions openforcefield/typing/engines/smirnoff/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2823,8 +2823,6 @@ def find_matches(self, entity):
matching the tuple of particle indices in ``entity``.
"""

# TODO: Right now, this method is only ever called with an entity that is a Topoogy.
# Should we reduce its scope and have a check here to make sure entity is a Topology?
return self._find_matches(entity, transformed_dict_cls=dict)

def create_force(self, system, topology, **kwargs):
Expand Down Expand Up @@ -3021,12 +3019,12 @@ class ChargeIncrementType(ParameterType):

_TAGNAME = 'ChargeIncrementModel' # SMIRNOFF tag name to process
_INFOTYPE = ChargeIncrementType # info type to store
# TODO: The structure of this is still undecided
_DEPENDENCIES = [vdWHandler, ElectrostaticsHandler, LibraryChargeHandler]

number_of_conformers = ParameterAttribute(default=1, converter=int)

partial_charge_method = ParameterAttribute(
default='AM1-Mulliken'
default='AM1-Mulliken', converter=str
)

def check_handler_compatibility(self,
Expand All @@ -3047,11 +3045,32 @@ def check_handler_compatibility(self,
"""

int_attrs_to_compare = ['number_of_conformers']
string_attrs_to_compare = ['quantum_chemical_method', 'partial_charge_method']
string_attrs_to_compare = ['partial_charge_method']

self._check_attributes_are_equal(other_handler,
identical_attrs=string_attrs_to_compare+int_attrs_to_compare)

def find_matches(self, entity):
"""Find the elements of the topology/molecule matched by a parameter type.

Parameters
----------
entity : openforcefield.topology.Topology
Topology to search.

Returns
---------
matches : ValenceDict[Tuple[int], ParameterHandler._Match]
``matches[particle_indices]`` is the ``ParameterType`` object
matching the tuple of particle indices in ``entity``.
"""
# It's necessary to overwrite the default behavior of this method to prevent it
# from using transformed_dict_class=ValenceDict, which could change the order
# in which chargeincrements match topology particles

return self._find_matches(entity, transformed_dict_cls=dict)



def create_force(self, system, topology, **kwargs):

Expand All @@ -3066,6 +3085,8 @@ def create_force(self, system, topology, **kwargs):
else:
force = existing[0]



for ref_mol in topology.reference_molecules:

# If charges were already assigned, skip this molecule
Expand All @@ -3079,14 +3100,7 @@ def create_force(self, system, topology, **kwargs):
temp_mol.generate_conformers(n_conformers=self.number_of_conformers)
temp_mol.compute_partial_charges(partial_charge_method=self.partial_charge_method)

bond_matches = self.find_matches(temp_mol)

for (atoms, charge_increment_match) in bond_matches.items():
charge_increment = charge_increment_match.parameter_type

for ref_mol_atom_idx, charge_increment in zip(atoms, charge_increment.charge_increment):
temp_mol.partial_charges[ref_mol_atom_idx] += charge_increment

charges_to_assign = {}

# Assign charges to relevant atoms
for topology_molecule in topology._reference_molecule_to_topology_molecules[ref_mol]:
Expand All @@ -3098,14 +3112,30 @@ def create_force(self, system, topology, **kwargs):
ref_mol_particle_index = topology_particle.virtual_site.molecule_particle_index
particle_charge = temp_mol._partial_charges[ref_mol_particle_index]

# Retrieve nonbonded parameters for reference atom (charge not set yet)
_, sigma, epsilon = force.getParticleParameters(topology_particle_index)
# Set the nonbonded force with the partial charge
force.setParticleParameters(topology_particle_index,
particle_charge, sigma,
epsilon)
charges_to_assign[topology_particle_index] = particle_charge
# # Retrieve nonbonded parameters for reference atom (charge not set yet)
# _, sigma, epsilon = force.getParticleParameters(topology_particle_index)
# # Set the nonbonded force with the partial charge
# force.setParticleParameters(topology_particle_index,
# # particle_charge, sigma,
# # epsilon)


charge_increment_matches = self.find_matches(topology)

for (atoms, charge_increment_match) in charge_increment_matches.items():
charge_increment = charge_increment_match.parameter_type

for top_particle_idx, charge_increment in zip(atoms, charge_increment.charge_increment):
#print(top_particle_idx, charge_increment)
if top_particle_idx in charges_to_assign:
charges_to_assign[top_particle_idx] += charge_increment
#1/0
for topology_particle_index, charge_to_assign in charges_to_assign.items():
_, sigma, epsilon = force.getParticleParameters(topology_particle_index)
force.setParticleParameters(topology_particle_index,
charge_to_assign, sigma,
epsilon)

# Finally, mark that charges were assigned for this reference molecule
self.mark_charges_assigned(ref_mol, topology)
Expand Down