Skip to content

Commit 4f9228d

Browse files
authored
Merge pull request #386 from epretti/charmm-naming-patching
Fixes and improvements for CHARMM force fields
2 parents 730a101 + 34c07f0 commit 4f9228d

21 files changed

+18303
-973
lines changed

charmm/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,22 @@ generated, the TOPPAR files should be reviewed to locate the inconsistency. You
8686
may wish to exclude one or another set of parameters at this point to ensure
8787
that the resulting force field is correct.
8888

89+
### Notice
90+
91+
Due to various outstanding issues in ParmEd, including:
92+
93+
* [ParmEd/ParmEd#1383](https://github.com/ParmEd/ParmEd/issues/1383)
94+
* [ParmEd/ParmEd#1384](https://github.com/ParmEd/ParmEd/issues/1384)
95+
* [ParmEd/ParmEd#1395](https://github.com/ParmEd/ParmEd/issues/1395)
96+
* [ParmEd/ParmEd#1396](https://github.com/ParmEd/ParmEd/issues/1396)
97+
* [ParmEd/ParmEd#1397](https://github.com/ParmEd/ParmEd/issues/1397)
98+
99+
it is not currently possible to use a prepackaged version of ParmEd or a version
100+
from its GitHub repository to perform the CHARMM conversion. Until these issues
101+
are fixed, a custom patched version of ParmEd containing workarounds for these
102+
problems specific to this conversion workflow can be found at
103+
[`patched-for-charmm-conversion` branch of the epretti/ParmEd repository](https://github.com/epretti/ParmEd/tree/patched-for-charmm-conversion).
104+
89105
CHARMM docker image
90106
===================
91107

charmm/convert_charmm.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,12 @@ def convert_yaml(yaml_filename, ffxml_dir):
119119
if name_collisions:
120120
raise ValueError(f"Collision between residue/patch names {sorted(name_collisions)}")
121121

122-
# Ensure that no atom names contain hyphens, as this will confuse the
123-
# improper handling script (by the time these names reach the script,
124-
# they will have been prepended with residue or patch names joined with
125-
# a hyphen).
122+
# Ensure that no names contain hyphens, as this will confuse the
123+
# improper/anisotropy handling scripts.
126124
for templates in (params.residues.values(), params.patches.values()):
127125
for template in templates:
126+
if "-" in template.name:
127+
raise ValueError(f"Forbidden character in template {template.name}")
128128
for atom in template.atoms:
129129
if "-" in atom.name:
130130
raise ValueError(f"Forbidden character in template {template.name} atom {atom.name}")
@@ -485,13 +485,13 @@ def preprocess_improper_types(improper_types, prepare_parameters):
485485

486486
script = etree.SubElement(ffxml_tree.getroot(), "Script")
487487
script.text = script_template.format(
488-
residue_impropers=repr(residue_impropers),
489-
patch_impropers=repr(patch_impropers),
490-
delete_impropers=repr(delete_impropers),
491-
residue_order=repr(residue_order),
492-
patch_order=repr(patch_order),
493-
periodic_improper_types=repr(periodic_improper_types),
494-
harmonic_improper_types=repr(harmonic_improper_types),
488+
residue_impropers=format_dict(residue_impropers),
489+
patch_impropers=format_dict(patch_impropers),
490+
delete_impropers=format_dict(delete_impropers),
491+
residue_order=format_dict(residue_order),
492+
patch_order=format_dict(patch_order),
493+
periodic_improper_types=format_dict(periodic_improper_types),
494+
harmonic_improper_types=format_dict(harmonic_improper_types),
495495
)
496496

497497

@@ -533,13 +533,17 @@ def preprocess_template_anisotropies(template_anisotropies):
533533

534534
script = etree.SubElement(ffxml_tree.getroot(), "Script")
535535
script.text = script_template.format(
536-
residue_anisotropies=repr(residue_anisotropies),
537-
patch_anisotropies=repr(patch_anisotropies),
538-
delete_anisotropies=repr(delete_anisotropies),
539-
residue_order=repr(residue_order),
540-
patch_order=repr(patch_order),
536+
residue_anisotropies=format_dict(residue_anisotropies),
537+
patch_anisotropies=format_dict(patch_anisotropies),
538+
delete_anisotropies=format_dict(delete_anisotropies),
539+
residue_order=format_dict(residue_order),
540+
patch_order=format_dict(patch_order),
541541
)
542542

543543

544+
def format_dict(d):
545+
return "\n".join(["{"] + [f" {k!r}: {v!r}," for k, v in d.items()] + ["}"])
546+
547+
544548
if __name__ == "__main__":
545549
main()

charmm/convert_charmm_anisotropy_script.txt

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,54 @@ delete_anisotropies = {delete_anisotropies}
77
residue_order = {residue_order}
88
patch_order = {patch_order}
99

10+
def extract_atom_name(raw_atom_name, templates):
11+
# Process Drude particles.
12+
atom_name_prefix = ""
13+
if raw_atom_name.startswith("Drude-"):
14+
raw_atom_name = raw_atom_name[len("Drude-"):]
15+
atom_name_prefix = "D"
16+
17+
# Return None to indicate an atom without template name information.
18+
atom_name_parts = raw_atom_name.rsplit("-", maxsplit=1)
19+
if len(atom_name_parts) < 2:
20+
return None
21+
templates.add(atom_name_parts[0])
22+
return f"{{atom_name_prefix}}{{atom_name_parts[1]}}"
23+
1024
residue_data = []
1125

12-
for residue in topology.residues():
13-
# Extract the residue or patch names and atom names from the atom type names.
14-
templates = []
15-
atom_names = []
16-
for atom in residue.atoms():
17-
raw_atom_name = data.atomType[atom]
18-
atom_is_drude = raw_atom_name.startswith("Drude-")
19-
if atom_is_drude:
20-
atom_name_parts = raw_atom_name[len("Drude-"):].rsplit("-", maxsplit=1)
21-
atom_names.append(f"D{{atom_name_parts[1]}}")
22-
else:
23-
atom_name_parts = raw_atom_name.rsplit("-", maxsplit=1)
24-
atom_names.append(atom_name_parts[1])
25-
templates.append(atom_name_parts[0])
26+
for residue_index, residue in enumerate(topology.residues()):
27+
# Extract the residue or patch names and atom names.
28+
template_data = templateForResidue[residue_index]
29+
templates = set()
30+
if template_data is None:
31+
# Multi-residue patch; fall back to parsing atom names because OpenMM
32+
# leaves None in templateForResidue in this case.
33+
atom_names = []
34+
skip_residue = False
35+
for atom in residue.atoms():
36+
atom_name = extract_atom_name(data.atomType[atom], templates)
37+
if atom_name is None:
38+
# Skip residues containing atoms without template name data
39+
# (they might be from, e.g., another solvent force field file).
40+
skip_residue = True
41+
break
42+
atom_names.append(atom_name)
43+
if skip_residue:
44+
residue_data.append(([], [], []))
45+
continue
46+
else:
47+
# Extract names from template name.
48+
for template_index, template_name in enumerate(template_data.name.split("-")):
49+
if template_index:
50+
# Patch name.
51+
templates.add(template_name.rsplit("_", maxsplit=1)[0])
52+
else:
53+
# Residue name.
54+
templates.add(template_name)
55+
atom_names = [template_data.atoms[data.atomTemplateIndexes[atom]].name for atom in residue.atoms()]
2656

2757
# Get all unique residue or patch names that have anisotropies.
28-
templates = set(templates)
2958
residue_templates = sorted(
3059
(template for template in templates if template in residue_anisotropies), key=residue_order.get
3160
)

charmm/convert_charmm_improper_script.txt

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ patch_order = {patch_order}
1212
periodic_improper_types = {periodic_improper_types}
1313
harmonic_improper_types = {harmonic_improper_types}
1414

15+
def extract_atom_name(raw_atom_name, templates):
16+
# Process Drude particles.
17+
atom_name_prefix = ""
18+
if raw_atom_name.startswith("Drude-"):
19+
raw_atom_name = raw_atom_name[len("Drude-"):]
20+
atom_name_prefix = "D"
21+
22+
# Return None to indicate an atom without template name information.
23+
atom_name_parts = raw_atom_name.rsplit("-", maxsplit=1)
24+
if len(atom_name_parts) < 2:
25+
return None
26+
templates.add(atom_name_parts[0])
27+
return f"{{atom_name_prefix}}{{atom_name_parts[1]}}"
28+
1529
def is_valid_improper(atom_indices):
1630
index_1, index_2, index_3, index_4 = atom_indices
1731
bonded_to_1 = data.bondedToAtom[index_1]
@@ -34,23 +48,38 @@ def find_improper(improper_types, atom_classes):
3448

3549
residue_data = []
3650

37-
for residue in topology.residues():
38-
# Extract the residue or patch names and atom names from the atom type names.
39-
templates = []
40-
atom_names = []
41-
for atom in residue.atoms():
42-
raw_atom_name = data.atomType[atom]
43-
atom_is_drude = raw_atom_name.startswith("Drude-")
44-
if atom_is_drude:
45-
atom_name_parts = raw_atom_name[len("Drude-"):].rsplit("-", maxsplit=1)
46-
atom_names.append(f"D{{atom_name_parts[1]}}")
47-
else:
48-
atom_name_parts = raw_atom_name.rsplit("-", maxsplit=1)
49-
atom_names.append(atom_name_parts[1])
50-
templates.append(atom_name_parts[0])
51+
for residue_index, residue in enumerate(topology.residues()):
52+
# Extract the residue or patch names and atom names.
53+
template_data = templateForResidue[residue_index]
54+
templates = set()
55+
if template_data is None:
56+
# Multi-residue patch; fall back to parsing atom names because OpenMM
57+
# leaves None in templateForResidue in this case.
58+
atom_names = []
59+
skip_residue = False
60+
for atom in residue.atoms():
61+
atom_name = extract_atom_name(data.atomType[atom], templates)
62+
if atom_name is None:
63+
# Skip residues containing atoms without template name data
64+
# (they might be from, e.g., another solvent force field file).
65+
skip_residue = True
66+
break
67+
atom_names.append(atom_name)
68+
if skip_residue:
69+
residue_data.append(([], [], []))
70+
continue
71+
else:
72+
# Extract names from template name.
73+
for template_index, template_name in enumerate(template_data.name.split("-")):
74+
if template_index:
75+
# Patch name.
76+
templates.add(template_name.rsplit("_", maxsplit=1)[0])
77+
else:
78+
# Residue name.
79+
templates.add(template_name)
80+
atom_names = [template_data.atoms[data.atomTemplateIndexes[atom]].name for atom in residue.atoms()]
5181

5282
# Get all unique residue or patch names that have impropers.
53-
templates = set(templates)
5483
residue_templates = sorted(
5584
(template for template in templates if template in residue_impropers), key=residue_order.get
5685
)

charmm/files/charmm36.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@
7777
# that causes conflicting atom type names to be generated
7878
- DMPR
7979

80+
# Names are incompatible with improper lookup logic
81+
- SB3-10
82+
- SB3-12
83+
- SB3-14
84+
8085
exclude_patches:
8186
# Exclude CNH3 (CYTP to protonated cytosine) as it is incorrectly marked
8287
# as applying elsewhere
@@ -133,6 +138,14 @@
133138
- ./Residues/Residue/AllowPatch[@name='DEO5_0']
134139

135140
fixes:
141+
# Remove proline-specific patch from non-proline residues
142+
- action: remove
143+
target:
144+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='PROP_0']
145+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='PROP_1']
146+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='PROP_2']
147+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='PROP_3']
148+
136149
# Here we manually set nucleic acid external bonds because CHARMM doesn't
137150
# provide a reliable way to get at this information and ParmEd fails to
138151
# guess it correctly for nucleic acids; we also fix RNA-to-DNA patches

charmm/files/charmm36_protein.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
output: charmm36_protein_d.xml
3030

3131
fixes:
32+
# Remove proline-specific patch from non-proline residues
33+
- action: remove
34+
target:
35+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='PROP_0']
36+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='PROP_1']
37+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='PROP_2']
38+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='PROP_3']
39+
3240
# Here we manually add in the DISU (cysteine disulfide) patch to the force
3341
# field since ParmEd doesn't convert it automatically
3442
- action: append

charmm/files/drude2023.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,20 @@
2121
# Won't support single amino acids since the terminal patches are
2222
# different and cause ambiguity for assigning templates to polypeptides,
2323
# but this shouldn't be a significant use case
24+
- CNES
25+
- CT2S
2426
- CTES
27+
- GNTS
2528
- NTES
2629
split:
2730
- input:
2831
- toppar/drude/drude_toppar_2023/toppar_drude_d_aminoacids_2023a.str
2932
output: charmm_polar_2023_d.xml
3033
fixes:
3134
- action: remove
32-
target: ./Residues/Residue[@name!='PRO']/AllowPatch[@name='CTEP_0']
35+
target:
36+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='CTEP_0']
37+
- ./Residues/Residue[@name!='PRO']/AllowPatch[@name='PROP_0']
3338
References:
3439
protein:
3540
- >-

0 commit comments

Comments
 (0)