diff --git a/.github/workflows/test_charmm.yaml b/.github/workflows/test_charmm.yaml index b2ae185a..16524619 100644 --- a/.github/workflows/test_charmm.yaml +++ b/.github/workflows/test_charmm.yaml @@ -25,7 +25,7 @@ jobs: matrix: os: [ubuntu-latest] python-version: ["3.13"] - openmm-version: ["8.4.0"] + openmm-version: ["8.5.0beta"] steps: - uses: actions/checkout@v6 diff --git a/charmm/convert_charmm_anisotropy_script.txt b/charmm/convert_charmm_anisotropy_script.txt index d5f211dc..2c4739f6 100644 --- a/charmm/convert_charmm_anisotropy_script.txt +++ b/charmm/convert_charmm_anisotropy_script.txt @@ -23,13 +23,11 @@ def extract_atom_name(raw_atom_name, templates): residue_data = [] -for residue_index, residue in enumerate(topology.residues()): +for residue in topology.residues(): # Extract the residue or patch names and atom names. - template_data = templateForResidue[residue_index] templates = set() - if template_data is None: - # Multi-residue patch; fall back to parsing atom names because OpenMM - # leaves None in templateForResidue in this case. + if residue not in templateForResidue: + # Multi-residue patch; fall back to parsing atom names. atom_names = [] skip_residue = False for atom in residue.atoms(): @@ -45,6 +43,7 @@ for residue_index, residue in enumerate(topology.residues()): continue else: # Extract names from template name. + template_data = templateForResidue[residue] for template_index, template_name in enumerate(template_data.name.split("-")): if template_index: # Patch name. diff --git a/charmm/convert_charmm_improper_script.txt b/charmm/convert_charmm_improper_script.txt index bfc83153..6a498b3f 100644 --- a/charmm/convert_charmm_improper_script.txt +++ b/charmm/convert_charmm_improper_script.txt @@ -48,13 +48,11 @@ def find_improper(improper_types, atom_classes): residue_data = [] -for residue_index, residue in enumerate(topology.residues()): +for residue in topology.residues(): # Extract the residue or patch names and atom names. - template_data = templateForResidue[residue_index] templates = set() - if template_data is None: - # Multi-residue patch; fall back to parsing atom names because OpenMM - # leaves None in templateForResidue in this case. + if residue not in templateForResidue: + # Multi-residue patch; fall back to parsing atom names. atom_names = [] skip_residue = False for atom in residue.atoms(): @@ -70,6 +68,7 @@ for residue_index, residue in enumerate(topology.residues()): continue else: # Extract names from template name. + template_data = templateForResidue[residue] for template_index, template_name in enumerate(template_data.name.split("-")): if template_index: # Patch name. diff --git a/devtools/conda-envs/test_charmm_env.yaml b/devtools/conda-envs/test_charmm_env.yaml index 8406d02b..6173b084 100644 --- a/devtools/conda-envs/test_charmm_env.yaml +++ b/devtools/conda-envs/test_charmm_env.yaml @@ -1,5 +1,6 @@ name: openmmforcefields-test-charmm channels: + - conda-forge/label/openmm_rc - conda-forge dependencies: - numpy <2.3 diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index f4d2ffec..ca00dfb5 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -5,7 +5,6 @@ channels: dependencies: - ambertools >=24 - lxml - - networkx - numpy <2.3 - openff-toolkit >=0.11 - openmm diff --git a/openmmforcefields/data/test-aa.pdb b/openmmforcefields/data/test-aa.pdb new file mode 100644 index 00000000..202b0636 --- /dev/null +++ b/openmmforcefields/data/test-aa.pdb @@ -0,0 +1,459 @@ +REMARK 1 CREATED WITH OPENMM 8.5, 2026-03-09 +HETATM 1 H1 ACE A 1 -0.291 -6.027 -4.982 1.00 0.00 H +HETATM 2 CH3 ACE A 1 -0.613 -6.923 -5.569 1.00 0.00 C +HETATM 3 H2 ACE A 1 0.406 -7.365 -5.614 1.00 0.00 H +HETATM 4 H3 ACE A 1 -0.978 -6.634 -6.533 1.00 0.00 H +HETATM 5 C ACE A 1 -1.513 -7.985 -4.938 1.00 0.00 C +HETATM 6 O ACE A 1 -1.095 -8.528 -3.926 1.00 0.00 O +ATOM 7 N ALA A 2 -2.688 -8.128 -5.527 1.00 0.00 N +ATOM 8 H ALA A 2 -3.023 -7.504 -6.225 1.00 0.00 H +ATOM 9 CA ALA A 2 -3.669 -9.095 -5.014 1.00 0.00 C +ATOM 10 HA ALA A 2 -3.908 -8.747 -4.093 1.00 0.00 H +ATOM 11 CB ALA A 2 -4.914 -9.067 -5.824 1.00 0.00 C +ATOM 12 HB1 ALA A 2 -4.788 -9.603 -6.751 1.00 0.00 H +ATOM 13 HB2 ALA A 2 -5.747 -9.433 -5.180 1.00 0.00 H +ATOM 14 HB3 ALA A 2 -5.227 -8.065 -6.036 1.00 0.00 H +ATOM 15 C ALA A 2 -3.237 -10.611 -4.898 1.00 0.00 C +ATOM 16 O ALA A 2 -2.487 -11.151 -5.741 1.00 0.00 O +ATOM 17 N ASP A 3 -3.632 -11.361 -3.824 1.00 0.00 N +ATOM 18 H ASP A 3 -3.302 -12.301 -3.721 1.00 0.00 H +ATOM 19 CA ASP A 3 -4.353 -10.958 -2.607 1.00 0.00 C +ATOM 20 HA ASP A 3 -4.573 -9.904 -2.619 1.00 0.00 H +ATOM 21 CB ASP A 3 -5.719 -11.587 -2.462 1.00 0.00 C +ATOM 22 HB2 ASP A 3 -6.421 -10.810 -2.193 1.00 0.00 H +ATOM 23 HB3 ASP A 3 -6.187 -11.854 -3.417 1.00 0.00 H +ATOM 24 CG ASP A 3 -5.733 -12.811 -1.577 1.00 0.00 C +ATOM 25 OD1 ASP A 3 -5.734 -12.703 -0.356 1.00 0.00 O +ATOM 26 OD2 ASP A 3 -5.861 -14.015 -2.217 1.00 0.00 O +ATOM 27 HD2 ASP A 3 -5.995 -13.990 -3.183 1.00 0.00 H +ATOM 28 C ASP A 3 -3.406 -11.117 -1.360 1.00 0.00 C +ATOM 29 O ASP A 3 -2.537 -11.993 -1.453 1.00 0.00 O +ATOM 30 N ASN A 4 -3.620 -10.395 -0.256 1.00 0.00 N +ATOM 31 H ASN A 4 -2.980 -10.725 0.401 1.00 0.00 H +ATOM 32 CA ASN A 4 -4.109 -8.967 0.062 1.00 0.00 C +ATOM 33 HA ASN A 4 -4.987 -9.000 0.730 1.00 0.00 H +ATOM 34 CB ASN A 4 -2.981 -8.368 0.914 1.00 0.00 C +ATOM 35 HB2 ASN A 4 -3.261 -7.353 1.140 1.00 0.00 H +ATOM 36 HB3 ASN A 4 -2.909 -8.956 1.825 1.00 0.00 H +ATOM 37 CG ASN A 4 -1.592 -8.358 0.333 1.00 0.00 C +ATOM 38 OD1 ASN A 4 -0.564 -8.233 0.969 1.00 0.00 O +ATOM 39 ND2 ASN A 4 -1.403 -8.569 -0.932 1.00 0.00 N +ATOM 40 HD21 ASN A 4 -0.436 -8.740 -1.259 1.00 0.00 H +ATOM 41 HD22 ASN A 4 -2.107 -8.510 -1.658 1.00 0.00 H +ATOM 42 C ASN A 4 -4.632 -8.016 -1.062 1.00 0.00 C +ATOM 43 O ASN A 4 -3.902 -7.779 -1.988 1.00 0.00 O +ATOM 44 N ASP A 5 -5.853 -7.441 -0.950 1.00 0.00 N +ATOM 45 H ASP A 5 -6.447 -7.821 -0.212 1.00 0.00 H +ATOM 46 CA ASP A 5 -6.387 -6.304 -1.788 1.00 0.00 C +ATOM 47 HA ASP A 5 -6.923 -6.789 -2.594 1.00 0.00 H +ATOM 48 CB ASP A 5 -7.215 -5.375 -0.888 1.00 0.00 C +ATOM 49 HB2 ASP A 5 -7.445 -4.373 -1.333 1.00 0.00 H +ATOM 50 HB3 ASP A 5 -8.170 -5.819 -0.741 1.00 0.00 H +ATOM 51 CG ASP A 5 -6.744 -5.108 0.603 1.00 0.00 C +ATOM 52 OD1 ASP A 5 -6.911 -3.913 1.097 1.00 0.00 O +ATOM 53 OD2 ASP A 5 -6.404 -6.055 1.298 1.00 0.00 O +ATOM 54 C ASP A 5 -5.354 -5.447 -2.623 1.00 0.00 C +ATOM 55 O ASP A 5 -4.577 -4.661 -2.035 1.00 0.00 O +HETATM 56 N CYM A 6 -5.464 -5.531 -3.973 1.00 0.00 N +HETATM 57 H CYM A 6 -6.162 -6.168 -4.224 1.00 0.00 H +HETATM 58 CA CYM A 6 -4.633 -4.791 -4.933 1.00 0.00 C +HETATM 59 HA CYM A 6 -3.667 -5.216 -4.701 1.00 0.00 H +HETATM 60 CB CYM A 6 -4.981 -5.135 -6.382 1.00 0.00 C +HETATM 61 HB3 CYM A 6 -5.329 -4.224 -6.874 1.00 0.00 H +HETATM 62 HB2 CYM A 6 -5.721 -5.858 -6.377 1.00 0.00 H +HETATM 63 SG CYM A 6 -3.600 -5.561 -7.396 1.00 0.00 S +HETATM 64 C CYM A 6 -4.670 -3.231 -4.690 1.00 0.00 C +HETATM 65 O CYM A 6 -5.746 -2.668 -4.570 1.00 0.00 O +ATOM 66 N CYS A 7 -3.568 -2.473 -4.657 1.00 0.00 N +ATOM 67 H CYS A 7 -3.705 -1.523 -4.361 1.00 0.00 H +ATOM 68 CA CYS A 7 -2.214 -3.014 -4.867 1.00 0.00 C +ATOM 69 HA CYS A 7 -2.174 -3.906 -5.483 1.00 0.00 H +ATOM 70 CB CYS A 7 -1.364 -2.071 -5.731 1.00 0.00 C +ATOM 71 HB2 CYS A 7 -1.605 -1.047 -5.462 1.00 0.00 H +ATOM 72 HB3 CYS A 7 -0.257 -2.254 -5.551 1.00 0.00 H +ATOM 73 SG CYS A 7 -1.876 -2.449 -7.501 1.00 0.00 S +ATOM 74 HG CYS A 7 -2.153 -3.801 -7.377 1.00 0.00 H +ATOM 75 C CYS A 7 -1.574 -3.278 -3.493 1.00 0.00 C +ATOM 76 O CYS A 7 -0.972 -2.502 -2.806 1.00 0.00 O +ATOM 77 N GLU A 8 -1.643 -4.576 -3.152 1.00 0.00 N +ATOM 78 H GLU A 8 -2.263 -5.148 -3.669 1.00 0.00 H +ATOM 79 CA GLU A 8 -1.215 -5.258 -1.907 1.00 0.00 C +ATOM 80 HA GLU A 8 -1.853 -6.162 -1.873 1.00 0.00 H +ATOM 81 CB GLU A 8 0.235 -5.862 -1.949 1.00 0.00 C +ATOM 82 HB2 GLU A 8 0.603 -6.080 -0.976 1.00 0.00 H +ATOM 83 HB3 GLU A 8 0.155 -6.817 -2.431 1.00 0.00 H +ATOM 84 CG GLU A 8 1.365 -5.097 -2.728 1.00 0.00 C +ATOM 85 HG2 GLU A 8 1.282 -5.132 -3.810 1.00 0.00 H +ATOM 86 HG3 GLU A 8 1.266 -4.077 -2.458 1.00 0.00 H +ATOM 87 CD GLU A 8 2.794 -5.569 -2.471 1.00 0.00 C +ATOM 88 OE1 GLU A 8 3.752 -5.086 -3.069 1.00 0.00 O +ATOM 89 OE2 GLU A 8 3.094 -6.434 -1.429 1.00 0.00 O +ATOM 90 HE2 GLU A 8 2.250 -6.678 -1.006 1.00 0.00 H +ATOM 91 C GLU A 8 -1.515 -4.516 -0.565 1.00 0.00 C +ATOM 92 O GLU A 8 -0.584 -3.925 -0.030 1.00 0.00 O +ATOM 93 N GLN A 9 -2.688 -4.741 0.030 1.00 0.00 N +ATOM 94 H GLN A 9 -3.333 -5.106 -0.700 1.00 0.00 H +ATOM 95 CA GLN A 9 -3.292 -4.359 1.287 1.00 0.00 C +ATOM 96 HA GLN A 9 -4.323 -4.668 1.284 1.00 0.00 H +ATOM 97 CB GLN A 9 -2.662 -5.069 2.511 1.00 0.00 C +ATOM 98 HB2 GLN A 9 -1.996 -5.811 2.399 1.00 0.00 H +ATOM 99 HB3 GLN A 9 -2.209 -4.345 3.109 1.00 0.00 H +ATOM 100 CG GLN A 9 -3.825 -5.731 3.336 1.00 0.00 C +ATOM 101 HG2 GLN A 9 -4.452 -6.226 2.731 1.00 0.00 H +ATOM 102 HG3 GLN A 9 -3.379 -6.364 4.158 1.00 0.00 H +ATOM 103 CD GLN A 9 -4.702 -4.669 4.030 1.00 0.00 C +ATOM 104 OE1 GLN A 9 -4.213 -3.908 4.843 1.00 0.00 O +ATOM 105 NE2 GLN A 9 -5.962 -4.613 3.697 1.00 0.00 N +ATOM 106 HE21 GLN A 9 -6.553 -3.846 3.958 1.00 0.00 H +ATOM 107 HE22 GLN A 9 -6.333 -5.267 3.009 1.00 0.00 H +ATOM 108 C GLN A 9 -3.373 -2.842 1.463 1.00 0.00 C +ATOM 109 O GLN A 9 -2.374 -2.120 1.513 1.00 0.00 O +ATOM 110 N GLU A 10 -4.572 -2.348 1.378 1.00 0.00 N +ATOM 111 H GLU A 10 -5.384 -2.951 1.300 1.00 0.00 H +ATOM 112 CA GLU A 10 -4.834 -0.965 1.236 1.00 0.00 C +ATOM 113 HA GLU A 10 -3.923 -0.492 0.902 1.00 0.00 H +ATOM 114 CB GLU A 10 -5.838 -0.779 0.104 1.00 0.00 C +ATOM 115 HB2 GLU A 10 -6.747 -1.316 0.372 1.00 0.00 H +ATOM 116 HB3 GLU A 10 -6.273 0.194 -0.042 1.00 0.00 H +ATOM 117 CG GLU A 10 -5.320 -1.231 -1.246 1.00 0.00 C +ATOM 118 HG2 GLU A 10 -4.977 -2.268 -1.131 1.00 0.00 H +ATOM 119 HG3 GLU A 10 -6.229 -1.171 -1.990 1.00 0.00 H +ATOM 120 CD GLU A 10 -4.286 -0.260 -1.851 1.00 0.00 C +ATOM 121 OE1 GLU A 10 -4.243 -0.213 -3.067 1.00 0.00 O +ATOM 122 OE2 GLU A 10 -3.559 0.496 -1.101 1.00 0.00 O +ATOM 123 C GLU A 10 -5.286 -0.178 2.461 1.00 0.00 C +ATOM 124 O GLU A 10 -5.746 0.973 2.401 1.00 0.00 O +ATOM 125 N GLY A 11 -5.414 -0.918 3.583 1.00 0.00 N +ATOM 126 H GLY A 11 -5.212 -1.890 3.516 1.00 0.00 H +ATOM 127 CA GLY A 11 -5.957 -0.450 4.863 1.00 0.00 C +ATOM 128 HA2 GLY A 11 -5.226 0.145 5.398 1.00 0.00 H +ATOM 129 HA3 GLY A 11 -6.775 0.180 4.674 1.00 0.00 H +ATOM 130 C GLY A 11 -6.316 -1.687 5.705 1.00 0.00 C +ATOM 131 O GLY A 11 -7.270 -2.391 5.407 1.00 0.00 O +ATOM 132 N HIS A 12 -5.536 -2.012 6.723 1.00 0.00 N +ATOM 133 H HIS A 12 -5.690 -3.021 6.933 1.00 0.00 H +ATOM 134 CA HIS A 12 -4.593 -1.227 7.555 1.00 0.00 C +ATOM 135 HA HIS A 12 -5.103 -0.366 8.050 1.00 0.00 H +ATOM 136 CB HIS A 12 -4.114 -2.093 8.795 1.00 0.00 C +ATOM 137 HB2 HIS A 12 -3.823 -1.394 9.590 1.00 0.00 H +ATOM 138 HB3 HIS A 12 -4.987 -2.692 9.179 1.00 0.00 H +ATOM 139 CG HIS A 12 -2.976 -3.083 8.510 1.00 0.00 C +ATOM 140 ND1 HIS A 12 -3.000 -4.348 7.881 1.00 0.00 N +ATOM 141 HD1 HIS A 12 -3.775 -4.697 7.386 1.00 0.00 H +ATOM 142 CE1 HIS A 12 -1.736 -4.837 7.818 1.00 0.00 C +ATOM 143 HE1 HIS A 12 -1.547 -5.744 7.456 1.00 0.00 H +ATOM 144 NE2 HIS A 12 -0.869 -3.992 8.337 1.00 0.00 N +ATOM 145 CD2 HIS A 12 -1.609 -2.861 8.742 1.00 0.00 C +ATOM 146 HD2 HIS A 12 -1.090 -1.988 9.133 1.00 0.00 H +ATOM 147 C HIS A 12 -3.384 -0.755 6.793 1.00 0.00 C +ATOM 148 O HIS A 12 -2.766 0.148 7.276 1.00 0.00 O +ATOM 149 N HIS A 13 -3.080 -1.354 5.621 1.00 0.00 N +ATOM 150 H HIS A 13 -3.785 -2.071 5.490 1.00 0.00 H +ATOM 151 CA HIS A 13 -2.242 -0.918 4.563 1.00 0.00 C +ATOM 152 HA HIS A 13 -2.806 -1.196 3.666 1.00 0.00 H +ATOM 153 CB HIS A 13 -2.116 0.636 4.503 1.00 0.00 C +ATOM 154 HB2 HIS A 13 -2.887 1.093 5.066 1.00 0.00 H +ATOM 155 HB3 HIS A 13 -1.190 0.920 5.079 1.00 0.00 H +ATOM 156 CG HIS A 13 -2.202 1.267 3.153 1.00 0.00 C +ATOM 157 ND1 HIS A 13 -2.990 2.366 2.810 1.00 0.00 N +ATOM 158 CE1 HIS A 13 -3.086 2.398 1.493 1.00 0.00 C +ATOM 159 HE1 HIS A 13 -3.733 3.035 0.922 1.00 0.00 H +ATOM 160 NE2 HIS A 13 -2.232 1.489 0.993 1.00 0.00 N +ATOM 161 HE2 HIS A 13 -2.422 1.231 -0.007 1.00 0.00 H +ATOM 162 CD2 HIS A 13 -1.642 0.743 2.020 1.00 0.00 C +ATOM 163 HD2 HIS A 13 -1.006 -0.123 1.993 1.00 0.00 H +ATOM 164 C HIS A 13 -0.863 -1.622 4.493 1.00 0.00 C +ATOM 165 O HIS A 13 0.088 -0.963 4.041 1.00 0.00 O +ATOM 166 N HIS A 14 -0.992 -2.913 4.865 1.00 0.00 N +ATOM 167 H HIS A 14 -1.843 -3.151 5.317 1.00 0.00 H +ATOM 168 CA HIS A 14 -0.016 -4.005 4.725 1.00 0.00 C +ATOM 169 HA HIS A 14 -0.654 -4.915 4.772 1.00 0.00 H +ATOM 170 CB HIS A 14 0.575 -4.010 3.356 1.00 0.00 C +ATOM 171 HB2 HIS A 14 -0.156 -3.726 2.600 1.00 0.00 H +ATOM 172 HB3 HIS A 14 1.219 -3.178 3.357 1.00 0.00 H +ATOM 173 CG HIS A 14 1.447 -5.190 2.862 1.00 0.00 C +ATOM 174 ND1 HIS A 14 2.828 -5.317 3.120 1.00 0.00 N +ATOM 175 HD1 HIS A 14 3.275 -4.759 3.823 1.00 0.00 H +ATOM 176 CE1 HIS A 14 3.341 -6.045 2.082 1.00 0.00 C +ATOM 177 HE1 HIS A 14 4.371 -6.114 1.808 1.00 0.00 H +ATOM 178 NE2 HIS A 14 2.375 -6.424 1.263 1.00 0.00 N +ATOM 179 HE2 HIS A 14 2.520 -6.717 0.243 1.00 0.00 H +ATOM 180 CD2 HIS A 14 1.190 -5.864 1.671 1.00 0.00 C +ATOM 181 HD2 HIS A 14 0.258 -5.941 1.124 1.00 0.00 H +ATOM 182 C HIS A 14 0.955 -4.354 5.956 1.00 0.00 C +ATOM 183 O HIS A 14 1.094 -5.524 6.371 1.00 0.00 O +ATOM 184 N ILE A 15 1.671 -3.483 6.661 1.00 0.00 N +ATOM 185 H ILE A 15 1.925 -3.793 7.565 1.00 0.00 H +ATOM 186 CA ILE A 15 2.101 -2.164 6.232 1.00 0.00 C +ATOM 187 HA ILE A 15 1.229 -1.593 6.026 1.00 0.00 H +ATOM 188 CB ILE A 15 2.825 -1.470 7.365 1.00 0.00 C +ATOM 189 HB ILE A 15 2.154 -1.438 8.168 1.00 0.00 H +ATOM 190 CG2 ILE A 15 4.151 -2.139 7.844 1.00 0.00 C +ATOM 191 HG21 ILE A 15 4.529 -1.635 8.664 1.00 0.00 H +ATOM 192 HG22 ILE A 15 3.880 -3.127 8.188 1.00 0.00 H +ATOM 193 HG23 ILE A 15 4.855 -2.215 7.028 1.00 0.00 H +ATOM 194 CG1 ILE A 15 3.137 0.034 7.032 1.00 0.00 C +ATOM 195 HG12 ILE A 15 3.536 0.489 7.832 1.00 0.00 H +ATOM 196 HG13 ILE A 15 3.951 -0.018 6.284 1.00 0.00 H +ATOM 197 CD1 ILE A 15 1.957 0.922 6.559 1.00 0.00 C +ATOM 198 HD11 ILE A 15 1.616 0.626 5.548 1.00 0.00 H +ATOM 199 HD12 ILE A 15 1.013 0.871 7.194 1.00 0.00 H +ATOM 200 HD13 ILE A 15 2.337 1.891 6.464 1.00 0.00 H +ATOM 201 C ILE A 15 2.986 -2.213 4.943 1.00 0.00 C +ATOM 202 O ILE A 15 3.703 -3.176 4.734 1.00 0.00 O +ATOM 203 N LEU A 16 2.818 -1.235 4.038 1.00 0.00 N +ATOM 204 H LEU A 16 1.993 -0.742 4.333 1.00 0.00 H +ATOM 205 CA LEU A 16 3.362 -1.006 2.693 1.00 0.00 C +ATOM 206 HA LEU A 16 4.455 -1.148 2.676 1.00 0.00 H +ATOM 207 CB LEU A 16 2.555 -1.863 1.708 1.00 0.00 C +ATOM 208 HB2 LEU A 16 2.773 -2.947 1.961 1.00 0.00 H +ATOM 209 HB3 LEU A 16 1.511 -1.755 1.847 1.00 0.00 H +ATOM 210 CG LEU A 16 2.878 -1.673 0.201 1.00 0.00 C +ATOM 211 HG LEU A 16 3.572 -0.802 0.173 1.00 0.00 H +ATOM 212 CD1 LEU A 16 3.614 -2.888 -0.363 1.00 0.00 C +ATOM 213 HD11 LEU A 16 2.743 -3.591 -0.430 1.00 0.00 H +ATOM 214 HD12 LEU A 16 3.946 -2.771 -1.462 1.00 0.00 H +ATOM 215 HD13 LEU A 16 4.461 -3.241 0.228 1.00 0.00 H +ATOM 216 CD2 LEU A 16 1.612 -1.427 -0.593 1.00 0.00 C +ATOM 217 HD21 LEU A 16 1.097 -0.480 -0.280 1.00 0.00 H +ATOM 218 HD22 LEU A 16 1.827 -1.345 -1.699 1.00 0.00 H +ATOM 219 HD23 LEU A 16 0.990 -2.291 -0.466 1.00 0.00 H +ATOM 220 C LEU A 16 3.236 0.556 2.377 1.00 0.00 C +ATOM 221 O LEU A 16 4.178 1.344 2.398 1.00 0.00 O +HETATM 222 N LYN A 17 2.011 0.933 2.022 1.00 0.00 N +HETATM 223 H LYN A 17 1.277 0.251 2.083 1.00 0.00 H +HETATM 224 CA LYN A 17 1.585 2.311 1.703 1.00 0.00 C +HETATM 225 HA LYN A 17 0.787 2.211 0.907 1.00 0.00 H +HETATM 226 CB LYN A 17 1.059 2.776 3.103 1.00 0.00 C +HETATM 227 HB2 LYN A 17 0.547 1.876 3.528 1.00 0.00 H +HETATM 228 HB3 LYN A 17 1.926 2.876 3.788 1.00 0.00 H +HETATM 229 CG LYN A 17 0.178 3.976 3.045 1.00 0.00 C +HETATM 230 HG2 LYN A 17 0.745 4.849 2.742 1.00 0.00 H +HETATM 231 HG3 LYN A 17 -0.619 3.831 2.346 1.00 0.00 H +HETATM 232 CD LYN A 17 -0.447 4.066 4.417 1.00 0.00 C +HETATM 233 HD2 LYN A 17 -1.071 3.169 4.647 1.00 0.00 H +HETATM 234 HD3 LYN A 17 0.342 4.149 5.170 1.00 0.00 H +HETATM 235 CE LYN A 17 -1.256 5.387 4.394 1.00 0.00 C +HETATM 236 HE2 LYN A 17 -1.382 5.818 5.320 1.00 0.00 H +HETATM 237 HE3 LYN A 17 -0.832 6.167 3.815 1.00 0.00 H +HETATM 238 NZ LYN A 17 -2.566 5.134 3.880 1.00 0.00 N +HETATM 239 HZ2 LYN A 17 -2.407 4.482 3.104 1.00 0.00 H +HETATM 240 HZ3 LYN A 17 -3.066 4.567 4.554 1.00 0.00 H +HETATM 241 C LYN A 17 2.651 3.262 1.068 1.00 0.00 C +HETATM 242 O LYN A 17 2.892 4.371 1.578 1.00 0.00 O +ATOM 243 N LYS A 18 3.237 3.051 -0.161 1.00 0.00 N +ATOM 244 H LYS A 18 3.755 3.814 -0.546 1.00 0.00 H +ATOM 245 CA LYS A 18 3.241 1.911 -1.111 1.00 0.00 C +ATOM 246 HA LYS A 18 2.656 1.129 -0.700 1.00 0.00 H +ATOM 247 CB LYS A 18 2.753 2.324 -2.553 1.00 0.00 C +ATOM 248 HB2 LYS A 18 2.288 3.229 -2.432 1.00 0.00 H +ATOM 249 HB3 LYS A 18 3.666 2.508 -3.143 1.00 0.00 H +ATOM 250 CG LYS A 18 1.835 1.273 -3.130 1.00 0.00 C +ATOM 251 HG2 LYS A 18 1.790 1.514 -4.234 1.00 0.00 H +ATOM 252 HG3 LYS A 18 2.111 0.333 -2.906 1.00 0.00 H +ATOM 253 CD LYS A 18 0.381 1.537 -2.627 1.00 0.00 C +ATOM 254 HD2 LYS A 18 0.441 1.357 -1.577 1.00 0.00 H +ATOM 255 HD3 LYS A 18 0.083 2.619 -2.831 1.00 0.00 H +ATOM 256 CE LYS A 18 -0.618 0.535 -3.257 1.00 0.00 C +ATOM 257 HE2 LYS A 18 -0.212 0.230 -4.202 1.00 0.00 H +ATOM 258 HE3 LYS A 18 -0.652 -0.385 -2.623 1.00 0.00 H +ATOM 259 NZ LYS A 18 -2.020 1.110 -3.291 1.00 0.00 N +ATOM 260 HZ1 LYS A 18 -2.095 2.017 -3.760 1.00 0.00 H +ATOM 261 HZ2 LYS A 18 -2.658 0.440 -3.801 1.00 0.00 H +ATOM 262 HZ3 LYS A 18 -2.447 1.107 -2.412 1.00 0.00 H +ATOM 263 C LYS A 18 4.572 1.165 -1.154 1.00 0.00 C +ATOM 264 O LYS A 18 4.928 0.523 -2.126 1.00 0.00 O +ATOM 265 N MET A 19 5.360 1.244 -0.037 1.00 0.00 N +ATOM 266 H MET A 19 4.933 1.743 0.746 1.00 0.00 H +ATOM 267 CA MET A 19 6.668 0.715 0.301 1.00 0.00 C +ATOM 268 HA MET A 19 6.774 1.099 1.338 1.00 0.00 H +ATOM 269 CB MET A 19 6.640 -0.867 0.361 1.00 0.00 C +ATOM 270 HB2 MET A 19 5.889 -1.133 1.081 1.00 0.00 H +ATOM 271 HB3 MET A 19 6.305 -1.414 -0.559 1.00 0.00 H +ATOM 272 CG MET A 19 7.980 -1.465 0.862 1.00 0.00 C +ATOM 273 HG2 MET A 19 8.640 -1.664 -0.027 1.00 0.00 H +ATOM 274 HG3 MET A 19 8.514 -0.759 1.605 1.00 0.00 H +ATOM 275 SD MET A 19 7.815 -3.133 1.628 1.00 0.00 S +ATOM 276 CE MET A 19 6.437 -2.975 2.775 1.00 0.00 C +ATOM 277 HE1 MET A 19 5.445 -2.892 2.267 1.00 0.00 H +ATOM 278 HE2 MET A 19 6.403 -3.800 3.457 1.00 0.00 H +ATOM 279 HE3 MET A 19 6.553 -2.022 3.292 1.00 0.00 H +ATOM 280 C MET A 19 7.859 1.327 -0.446 1.00 0.00 C +ATOM 281 O MET A 19 8.396 0.635 -1.328 1.00 0.00 O +ATOM 282 N PHE A 20 8.385 2.478 0.002 1.00 0.00 N +ATOM 283 H PHE A 20 8.972 2.863 -0.728 1.00 0.00 H +ATOM 284 CA PHE A 20 7.772 3.489 0.881 1.00 0.00 C +ATOM 285 HA PHE A 20 6.688 3.301 1.007 1.00 0.00 H +ATOM 286 CB PHE A 20 8.523 3.507 2.282 1.00 0.00 C +ATOM 287 HB2 PHE A 20 8.511 4.468 2.821 1.00 0.00 H +ATOM 288 HB3 PHE A 20 9.604 3.213 2.185 1.00 0.00 H +ATOM 289 CG PHE A 20 7.887 2.543 3.236 1.00 0.00 C +ATOM 290 CD1 PHE A 20 6.721 2.846 3.876 1.00 0.00 C +ATOM 291 HD1 PHE A 20 6.263 3.815 3.750 1.00 0.00 H +ATOM 292 CE1 PHE A 20 6.026 1.868 4.658 1.00 0.00 C +ATOM 293 HE1 PHE A 20 5.061 2.106 5.123 1.00 0.00 H +ATOM 294 CZ PHE A 20 6.599 0.563 4.769 1.00 0.00 C +ATOM 295 HZ PHE A 20 6.086 -0.215 5.339 1.00 0.00 H +ATOM 296 CE2 PHE A 20 7.853 0.291 4.216 1.00 0.00 C +ATOM 297 HE2 PHE A 20 8.399 -0.669 4.245 1.00 0.00 H +ATOM 298 CD2 PHE A 20 8.455 1.266 3.453 1.00 0.00 C +ATOM 299 HD2 PHE A 20 9.343 0.937 2.919 1.00 0.00 H +ATOM 300 C PHE A 20 7.909 4.774 0.063 1.00 0.00 C +ATOM 301 O PHE A 20 8.609 4.868 -0.939 1.00 0.00 O +ATOM 302 N PRO A 21 6.960 5.725 0.352 1.00 0.00 N +ATOM 303 CD PRO A 21 6.428 6.017 1.733 1.00 0.00 C +ATOM 304 HD2 PRO A 21 7.208 6.061 2.451 1.00 0.00 H +ATOM 305 HD3 PRO A 21 5.648 5.299 1.966 1.00 0.00 H +ATOM 306 CG PRO A 21 5.914 7.450 1.555 1.00 0.00 C +ATOM 307 HG2 PRO A 21 6.778 8.123 1.438 1.00 0.00 H +ATOM 308 HG3 PRO A 21 5.254 7.654 2.416 1.00 0.00 H +ATOM 309 CB PRO A 21 5.177 7.272 0.201 1.00 0.00 C +ATOM 310 HB2 PRO A 21 4.947 8.264 -0.203 1.00 0.00 H +ATOM 311 HB3 PRO A 21 4.247 6.735 0.385 1.00 0.00 H +ATOM 312 CA PRO A 21 6.065 6.374 -0.644 1.00 0.00 C +ATOM 313 HA PRO A 21 6.632 6.963 -1.310 1.00 0.00 H +ATOM 314 C PRO A 21 5.287 5.275 -1.428 1.00 0.00 C +ATOM 315 O PRO A 21 5.602 4.101 -1.278 1.00 0.00 O +ATOM 316 N SER A 22 4.149 5.415 -2.075 1.00 0.00 N +ATOM 317 H SER A 22 3.797 4.518 -2.418 1.00 0.00 H +ATOM 318 CA SER A 22 3.364 6.573 -2.470 1.00 0.00 C +ATOM 319 HA SER A 22 2.810 6.088 -3.262 1.00 0.00 H +ATOM 320 CB SER A 22 2.181 7.013 -1.623 1.00 0.00 C +ATOM 321 HB2 SER A 22 2.525 7.408 -0.676 1.00 0.00 H +ATOM 322 HB3 SER A 22 1.497 7.739 -2.129 1.00 0.00 H +ATOM 323 OG SER A 22 1.431 5.910 -1.472 1.00 0.00 O +ATOM 324 HG SER A 22 1.825 5.316 -0.830 1.00 0.00 H +ATOM 325 C SER A 22 4.061 7.638 -3.327 1.00 0.00 C +ATOM 326 O SER A 22 3.873 8.852 -3.125 1.00 0.00 O +ATOM 327 N THR A 23 4.848 7.179 -4.270 1.00 0.00 N +ATOM 328 H THR A 23 5.148 6.268 -4.191 1.00 0.00 H +ATOM 329 CA THR A 23 4.650 7.540 -5.729 1.00 0.00 C +ATOM 330 HA THR A 23 4.855 8.578 -5.976 1.00 0.00 H +ATOM 331 CB THR A 23 5.574 6.629 -6.608 1.00 0.00 C +ATOM 332 HB THR A 23 5.337 6.717 -7.635 1.00 0.00 H +ATOM 333 CG2 THR A 23 7.092 6.970 -6.342 1.00 0.00 C +ATOM 334 HG21 THR A 23 7.256 8.076 -6.350 1.00 0.00 H +ATOM 335 HG22 THR A 23 7.461 6.546 -5.371 1.00 0.00 H +ATOM 336 HG23 THR A 23 7.777 6.468 -7.078 1.00 0.00 H +ATOM 337 OG1 THR A 23 5.284 5.311 -6.283 1.00 0.00 O +ATOM 338 HG1 THR A 23 5.569 5.203 -5.373 1.00 0.00 H +ATOM 339 C THR A 23 3.211 7.309 -6.109 1.00 0.00 C +ATOM 340 O THR A 23 2.570 8.166 -6.708 1.00 0.00 O +ATOM 341 N TRP A 24 2.692 6.125 -5.853 1.00 0.00 N +ATOM 342 H TRP A 24 3.439 5.494 -5.731 1.00 0.00 H +ATOM 343 CA TRP A 24 1.390 5.458 -6.154 1.00 0.00 C +ATOM 344 HA TRP A 24 1.535 5.286 -7.236 1.00 0.00 H +ATOM 345 CB TRP A 24 1.352 4.084 -5.469 1.00 0.00 C +ATOM 346 HB2 TRP A 24 2.402 3.679 -5.539 1.00 0.00 H +ATOM 347 HB3 TRP A 24 1.147 4.167 -4.424 1.00 0.00 H +ATOM 348 CG TRP A 24 0.391 3.113 -6.136 1.00 0.00 C +ATOM 349 CD1 TRP A 24 0.787 2.010 -6.808 1.00 0.00 C +ATOM 350 HD1 TRP A 24 1.813 1.787 -7.035 1.00 0.00 H +ATOM 351 NE1 TRP A 24 -0.318 1.172 -7.081 1.00 0.00 N +ATOM 352 HE1 TRP A 24 -0.262 0.272 -7.589 1.00 0.00 H +ATOM 353 CE2 TRP A 24 -1.484 1.726 -6.609 1.00 0.00 C +ATOM 354 CZ2 TRP A 24 -2.763 1.277 -6.477 1.00 0.00 C +ATOM 355 HZ2 TRP A 24 -3.035 0.345 -6.962 1.00 0.00 H +ATOM 356 CH2 TRP A 24 -3.713 2.028 -5.799 1.00 0.00 C +ATOM 357 HH2 TRP A 24 -4.713 1.679 -5.638 1.00 0.00 H +ATOM 358 CZ3 TRP A 24 -3.309 3.141 -5.059 1.00 0.00 C +ATOM 359 HZ3 TRP A 24 -4.121 3.680 -4.614 1.00 0.00 H +ATOM 360 CE3 TRP A 24 -2.009 3.638 -5.232 1.00 0.00 C +ATOM 361 HE3 TRP A 24 -1.666 4.496 -4.673 1.00 0.00 H +ATOM 362 CD2 TRP A 24 -1.046 2.916 -5.976 1.00 0.00 C +ATOM 363 C TRP A 24 0.196 6.378 -5.826 1.00 0.00 C +ATOM 364 O TRP A 24 -0.389 6.936 -6.764 1.00 0.00 O +ATOM 365 N TYR A 25 0.136 6.687 -4.535 1.00 0.00 N +ATOM 366 H TYR A 25 0.659 6.147 -3.896 1.00 0.00 H +ATOM 367 CA TYR A 25 -0.675 7.657 -3.862 1.00 0.00 C +ATOM 368 HA TYR A 25 -0.434 7.636 -2.831 1.00 0.00 H +ATOM 369 CB TYR A 25 -0.216 9.075 -4.320 1.00 0.00 C +ATOM 370 HB2 TYR A 25 0.880 9.044 -4.587 1.00 0.00 H +ATOM 371 HB3 TYR A 25 -0.857 9.298 -5.165 1.00 0.00 H +ATOM 372 CG TYR A 25 -0.419 10.126 -3.279 1.00 0.00 C +ATOM 373 CD1 TYR A 25 -1.643 10.757 -3.038 1.00 0.00 C +ATOM 374 HD1 TYR A 25 -2.511 10.557 -3.661 1.00 0.00 H +ATOM 375 CE1 TYR A 25 -1.789 11.703 -1.987 1.00 0.00 C +ATOM 376 HE1 TYR A 25 -2.774 12.224 -1.877 1.00 0.00 H +ATOM 377 CZ TYR A 25 -0.767 11.873 -1.032 1.00 0.00 C +ATOM 378 OH TYR A 25 -0.973 12.647 0.005 1.00 0.00 O +ATOM 379 HH TYR A 25 -0.159 12.643 0.550 1.00 0.00 H +ATOM 380 CE2 TYR A 25 0.498 11.154 -1.149 1.00 0.00 C +ATOM 381 HE2 TYR A 25 1.283 11.359 -0.387 1.00 0.00 H +ATOM 382 CD2 TYR A 25 0.676 10.360 -2.347 1.00 0.00 C +ATOM 383 HD2 TYR A 25 1.622 9.910 -2.589 1.00 0.00 H +ATOM 384 C TYR A 25 -2.204 7.425 -4.006 1.00 0.00 C +ATOM 385 O TYR A 25 -2.885 7.965 -4.870 1.00 0.00 O +ATOM 386 N VAL A 26 -2.782 6.757 -2.985 1.00 0.00 N +ATOM 387 H VAL A 26 -3.771 6.739 -3.200 1.00 0.00 H +ATOM 388 CA VAL A 26 -2.227 6.002 -1.918 1.00 0.00 C +ATOM 389 HA VAL A 26 -1.189 5.896 -1.970 1.00 0.00 H +ATOM 390 CB VAL A 26 -2.449 6.624 -0.483 1.00 0.00 C +ATOM 391 HB VAL A 26 -3.443 6.587 -0.165 1.00 0.00 H +ATOM 392 CG1 VAL A 26 -1.930 5.670 0.567 1.00 0.00 C +ATOM 393 HG11 VAL A 26 -0.895 5.449 0.450 1.00 0.00 H +ATOM 394 HG12 VAL A 26 -1.966 6.214 1.518 1.00 0.00 H +ATOM 395 HG13 VAL A 26 -2.604 4.789 0.716 1.00 0.00 H +ATOM 396 CG2 VAL A 26 -1.825 8.022 -0.186 1.00 0.00 C +ATOM 397 HG21 VAL A 26 -2.247 8.827 -0.822 1.00 0.00 H +ATOM 398 HG22 VAL A 26 -2.120 8.309 0.820 1.00 0.00 H +ATOM 399 HG23 VAL A 26 -0.765 7.960 -0.211 1.00 0.00 H +ATOM 400 C VAL A 26 -2.857 4.625 -1.974 1.00 0.00 C +ATOM 401 O VAL A 26 -2.154 3.664 -2.157 1.00 0.00 O +HETATM 402 N NME A 27 -4.170 4.494 -1.956 1.00 0.00 N +HETATM 403 H NME A 27 -4.701 5.350 -1.859 1.00 0.00 H +HETATM 404 C NME A 27 -4.911 3.200 -1.969 1.00 0.00 C +HETATM 405 H1 NME A 27 -5.002 2.715 -0.971 1.00 0.00 H +HETATM 406 H2 NME A 27 -6.043 3.388 -2.237 1.00 0.00 H +HETATM 407 H3 NME A 27 -4.527 2.449 -2.684 1.00 0.00 H +TER 408 NME A 27 +CONECT 1 2 +CONECT 2 5 1 3 4 +CONECT 3 2 +CONECT 4 2 +CONECT 5 2 6 7 +CONECT 6 5 +CONECT 7 5 +CONECT 54 56 +CONECT 56 54 57 58 +CONECT 57 56 +CONECT 58 56 59 60 64 +CONECT 59 58 +CONECT 60 58 61 62 63 +CONECT 61 60 +CONECT 62 60 +CONECT 63 60 +CONECT 64 66 58 65 +CONECT 65 64 +CONECT 66 64 +CONECT 220 222 +CONECT 222 220 223 224 +CONECT 223 222 +CONECT 224 222 225 226 241 +CONECT 225 224 +CONECT 226 224 227 228 229 +CONECT 227 226 +CONECT 228 226 +CONECT 229 226 230 231 232 +CONECT 230 229 +CONECT 231 229 +CONECT 232 229 233 234 235 +CONECT 233 232 +CONECT 234 232 +CONECT 235 232 236 237 238 +CONECT 236 235 +CONECT 237 235 +CONECT 238 235 239 240 +CONECT 239 238 +CONECT 240 238 +CONECT 241 243 224 242 +CONECT 242 241 +CONECT 243 241 +CONECT 400 402 +CONECT 402 400 404 403 +CONECT 403 402 +CONECT 404 405 406 407 402 +CONECT 405 404 +CONECT 406 404 +CONECT 407 404 +END diff --git a/openmmforcefields/data/test-ala-3.pdb b/openmmforcefields/data/test-ala-3.pdb new file mode 100644 index 00000000..fe67d921 --- /dev/null +++ b/openmmforcefields/data/test-ala-3.pdb @@ -0,0 +1,36 @@ +REMARK 1 CREATED WITH OPENMM 8.4, 2026-02-10 +ATOM 1 N ALA A 1 0.024 -0.103 -0.101 1.00 0.00 N +ATOM 2 H ALA A 1 0.027 -1.132 -0.239 1.00 0.00 H +ATOM 3 H2 ALA A 1 -0.805 0.163 0.471 1.00 0.00 H +ATOM 4 H3 ALA A 1 -0.059 0.384 -1.019 1.00 0.00 H +ATOM 5 CA ALA A 1 1.247 0.375 0.636 1.00 0.00 C +ATOM 6 HA ALA A 1 0.814 0.861 1.495 1.00 0.00 H +ATOM 7 CB ALA A 1 2.057 -0.772 1.289 1.00 0.00 C +ATOM 8 HB1 ALA A 1 3.136 -0.752 1.032 1.00 0.00 H +ATOM 9 HB2 ALA A 1 1.990 -0.641 2.395 1.00 0.00 H +ATOM 10 HB3 ALA A 1 1.656 -1.782 1.063 1.00 0.00 H +ATOM 11 C ALA A 1 1.956 1.579 0.036 1.00 0.00 C +ATOM 12 O ALA A 1 1.219 2.525 -0.201 1.00 0.00 O +ATOM 13 N ALA A 2 3.289 1.631 -0.202 1.00 0.00 N +ATOM 14 H ALA A 2 3.939 0.868 -0.174 1.00 0.00 H +ATOM 15 CA ALA A 2 3.990 2.909 -0.215 1.00 0.00 C +ATOM 16 HA ALA A 2 3.742 3.440 0.695 1.00 0.00 H +ATOM 17 CB ALA A 2 3.662 3.802 -1.434 1.00 0.00 C +ATOM 18 HB1 ALA A 2 4.192 4.778 -1.358 1.00 0.00 H +ATOM 19 HB2 ALA A 2 3.956 3.311 -2.382 1.00 0.00 H +ATOM 20 HB3 ALA A 2 2.577 4.027 -1.467 1.00 0.00 H +ATOM 21 C ALA A 2 5.487 2.654 -0.128 1.00 0.00 C +ATOM 22 O ALA A 2 5.889 1.489 -0.137 1.00 0.00 O +ATOM 23 C ALA A 3 8.018 5.323 0.136 1.00 0.00 C +ATOM 24 O ALA A 3 7.032 6.119 0.127 1.00 0.00 O +ATOM 25 OXT ALA A 3 9.219 5.692 0.188 1.00 0.00 O +ATOM 26 N ALA A 3 6.275 3.733 -0.037 1.00 0.00 N +ATOM 27 H ALA A 3 5.963 4.691 -0.028 1.00 0.00 H +ATOM 28 CA ALA A 3 7.707 3.802 0.068 1.00 0.00 C +ATOM 29 HA ALA A 3 8.160 3.418 -0.833 1.00 0.00 H +ATOM 30 CB ALA A 3 8.233 3.093 1.333 1.00 0.00 C +ATOM 31 HB1 ALA A 3 9.342 3.149 1.356 1.00 0.00 H +ATOM 32 HB2 ALA A 3 7.835 3.593 2.240 1.00 0.00 H +ATOM 33 HB3 ALA A 3 7.923 2.030 1.332 1.00 0.00 H +TER 34 ALA A 3 +END diff --git a/openmmforcefields/ffxml/charmm/charmm36_nowaters.xml b/openmmforcefields/ffxml/charmm/charmm36_nowaters.xml index e465c7e8..f87b0b5d 100644 --- a/openmmforcefields/ffxml/charmm/charmm36_nowaters.xml +++ b/openmmforcefields/ffxml/charmm/charmm36_nowaters.xml @@ -1,6 +1,6 @@ - 2025-06-23 + 2026-03-04 toppar/top_all36_prot.rtf toppar/par_all36m_prot.prm toppar/top_all36_na.rtf @@ -259286,13 +259286,11 @@ def find_improper(improper_types, atom_classes): residue_data = [] -for residue_index, residue in enumerate(topology.residues()): +for residue in topology.residues(): # Extract the residue or patch names and atom names. - template_data = templateForResidue[residue_index] templates = set() - if template_data is None: - # Multi-residue patch; fall back to parsing atom names because OpenMM - # leaves None in templateForResidue in this case. + if residue not in templateForResidue: + # Multi-residue patch; fall back to parsing atom names. atom_names = [] skip_residue = False for atom in residue.atoms(): @@ -259308,6 +259306,7 @@ for residue_index, residue in enumerate(topology.residues()): continue else: # Extract names from template name. + template_data = templateForResidue[residue] for template_index, template_name in enumerate(template_data.name.split("-")): if template_index: # Patch name. diff --git a/openmmforcefields/ffxml/charmm/charmm36_protein.xml b/openmmforcefields/ffxml/charmm/charmm36_protein.xml index b0495ee5..caebac7c 100644 --- a/openmmforcefields/ffxml/charmm/charmm36_protein.xml +++ b/openmmforcefields/ffxml/charmm/charmm36_protein.xml @@ -1,6 +1,6 @@ - 2025-06-23 + 2026-03-04 toppar/top_all36_prot.rtf toppar/par_all36m_prot.prm toppar/stream/prot/toppar_all36_prot_aldehydes.str @@ -8760,13 +8760,11 @@ def find_improper(improper_types, atom_classes): residue_data = [] -for residue_index, residue in enumerate(topology.residues()): +for residue in topology.residues(): # Extract the residue or patch names and atom names. - template_data = templateForResidue[residue_index] templates = set() - if template_data is None: - # Multi-residue patch; fall back to parsing atom names because OpenMM - # leaves None in templateForResidue in this case. + if residue not in templateForResidue: + # Multi-residue patch; fall back to parsing atom names. atom_names = [] skip_residue = False for atom in residue.atoms(): @@ -8782,6 +8780,7 @@ for residue_index, residue in enumerate(topology.residues()): continue else: # Extract names from template name. + template_data = templateForResidue[residue] for template_index, template_name in enumerate(template_data.name.split("-")): if template_index: # Patch name. diff --git a/openmmforcefields/ffxml/charmm/charmm_polar_2023.xml b/openmmforcefields/ffxml/charmm/charmm_polar_2023.xml index ac9e60b3..7a9bbb5b 100644 --- a/openmmforcefields/ffxml/charmm/charmm_polar_2023.xml +++ b/openmmforcefields/ffxml/charmm/charmm_polar_2023.xml @@ -1,6 +1,6 @@ - 2025-06-23 + 2026-03-04 toppar/drude/drude_toppar_2023/toppar_drude_main_protein_2023a.str toppar/drude/drude_toppar_2023/toppar_drude_d_aminoacids_2023a.str toppar/drude/drude_toppar_2023/toppar_drude_carbohydrate_2023a.str @@ -30887,13 +30887,11 @@ def find_improper(improper_types, atom_classes): residue_data = [] -for residue_index, residue in enumerate(topology.residues()): +for residue in topology.residues(): # Extract the residue or patch names and atom names. - template_data = templateForResidue[residue_index] templates = set() - if template_data is None: - # Multi-residue patch; fall back to parsing atom names because OpenMM - # leaves None in templateForResidue in this case. + if residue not in templateForResidue: + # Multi-residue patch; fall back to parsing atom names. atom_names = [] skip_residue = False for atom in residue.atoms(): @@ -30909,6 +30907,7 @@ for residue_index, residue in enumerate(topology.residues()): continue else: # Extract names from template name. + template_data = templateForResidue[residue] for template_index, template_name in enumerate(template_data.name.split("-")): if template_index: # Patch name. @@ -31531,13 +31530,11 @@ def extract_atom_name(raw_atom_name, templates): residue_data = [] -for residue_index, residue in enumerate(topology.residues()): +for residue in topology.residues(): # Extract the residue or patch names and atom names. - template_data = templateForResidue[residue_index] templates = set() - if template_data is None: - # Multi-residue patch; fall back to parsing atom names because OpenMM - # leaves None in templateForResidue in this case. + if residue not in templateForResidue: + # Multi-residue patch; fall back to parsing atom names. atom_names = [] skip_residue = False for atom in residue.atoms(): @@ -31553,6 +31550,7 @@ for residue_index, residue in enumerate(topology.residues()): continue else: # Extract names from template name. + template_data = templateForResidue[residue] for template_index, template_name in enumerate(template_data.name.split("-")): if template_index: # Patch name. diff --git a/openmmforcefields/generators/template_generators.py b/openmmforcefields/generators/template_generators.py index bcd1c7ea..fecc1c17 100644 --- a/openmmforcefields/generators/template_generators.py +++ b/openmmforcefields/generators/template_generators.py @@ -6,7 +6,9 @@ # IMPORTS ################################################################################ +import collections import contextlib +import copy import hashlib import io import logging @@ -48,14 +50,12 @@ def __init__(self, molecules=None, cache=None): """ Create a template generator with some OpenFF toolkit molecules - Requies the openff-toolkit toolkit: http://openforcefield.org + Requires the openff-toolkit toolkit: http://openforcefield.org Parameters ---------- molecules : openff.toolkit.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. + Can be a Molecule or a list of Molecule objects. If specified, these molecules will be recognized and parameterized as needed. The parameters will be cached in case they are encountered again the future. cache : str, optional, default=None @@ -67,9 +67,11 @@ def __init__(self, molecules=None, cache=None): self._molecules = dict() self.add_molecules(molecules) - # Set up cache - self._cache = cache - self._smiles_added_to_db = set() # set of SMILES added to the database this session + # Set up in-memory cache + self._ffxml_cache = {} + + # Set up on-disk cache + self._cache_path = cache self._database_table_name = None # this must be set by subclasses for cache to function # Name of the force field (or a descriptive string if not a named force field) @@ -93,7 +95,7 @@ def _open_db(self): "indent": 4, "separators": (",", ": "), } # for pretty-printing - db = TinyDB(self._cache, **tinydb_kwargs) + db = TinyDB(self._cache_path, **tinydb_kwargs) try: yield db finally: @@ -106,9 +108,7 @@ def add_molecules(self, molecules=None): Parameters ---------- molecules : openff.toolkit.Molecule or list of Molecules, 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. + Can be a Molecule or a list of Molecule objects. If specified, these molecules will be recognized and parameterized as needed. The parameters will be cached in case they are encountered again the future. @@ -124,6 +124,8 @@ def add_molecules(self, molecules=None): >>> generator.add_molecules([mol2, mol3]) # doctest: +SKIP """ + from openmm.app import Element, ForceField + # Return if empty if not molecules: return @@ -134,98 +136,69 @@ def add_molecules(self, molecules=None): except TypeError: molecules = [molecules] - # Create copies - # TODO: Do we need to try to construct molecules with other schemes, such as Molecule.from_smiles(), if needed? - import copy + for molecule in molecules: + # Create our own copy of this molecule + molecule = copy.deepcopy(molecule) - molecules = [copy.deepcopy(molecule) for molecule in molecules] + # Make a template (not a forcefield-specific one with, e.g., virtual + # sites, but only used to check for isomorphism to input residues). + matching_template = ForceField._TemplateData("") + for atom in molecule.atoms: + matching_template.addAtom( + ForceField._TemplateAtomData("", "", Element.getByAtomicNumber(atom.atomic_number)) + ) + for bond in molecule.bonds: + matching_template.addBond(bond.atom1_index, bond.atom2_index) - # Cache molecules - self._molecules.update({molecule.to_smiles(): molecule for molecule in molecules}) + # Store the molecule by its formula for faster lookup + formula = tuple(sorted(collections.Counter(atom.atomic_number for atom in molecule.atoms).items())) + self._molecules.setdefault(formula, []).append((molecule, matching_template)) - @staticmethod - def _match_residue(residue, molecule_template): + def _preprocess_residue(self, residue): """ - Determine whether a residue matches a Molecule template and return a list of corresponding atoms. - - This implementation uses NetworkX for graph isomorphism determination. - - Parameters - ---------- - residue : openmm.app.topology.Residue - The residue to check - molecule_template : openff.toolkit.Molecule - The Molecule template to compare it to - - Returns - ------- - matches : dict of int : int - matches[residue_atom_index] is the corresponding Molecule template atom index - or None if it does not match the template - - .. todo :: Can this be replaced by an isomorphism matching call to the OpenFF toolkit? + Finds the whole molecule that a `Residue` is part of, and if the + `Residue` is not already a whole molecule, constructs a `MergedResidue` + corresponding to all of the residues in this whole molecule. Also + returns "bondedToAtom" data for the residue that can be passed to OpenMM + for template matching. """ - # TODO: Speed this up by rejecting molecules that do not have the same chemical formula - - # TODO: Can this NetworkX implementation be replaced by an isomorphism - # matching call to the OpenFF toolkit? - - import networkx as nx - - # Build list of external bonds for residue - number_of_external_bonds = {atom: 0 for atom in residue.atoms()} - for bond in residue.external_bonds(): - if bond[0] in number_of_external_bonds: - number_of_external_bonds[bond[0]] += 1 - if bond[1] in number_of_external_bonds: - number_of_external_bonds[bond[1]] += 1 - - # Residue graph - residue_graph = nx.Graph() - for atom in residue.atoms(): - residue_graph.add_node( - atom, - element=atom.element.atomic_number, - number_of_external_bonds=number_of_external_bonds[atom], - ) - for bond in residue.internal_bonds(): - residue_graph.add_edge(bond[0], bond[1]) - - # Template graph - # TODO: We can support templates with "external" bonds or atoms using attached string data in future - # See https://docs.eyesopen.com/toolkits/python/oechemtk/OEChemClasses/OEAtomBase.html - template_graph = nx.Graph() - for atom_index, atom in enumerate(molecule_template.atoms): - template_graph.add_node(atom_index, element=atom.atomic_number, number_of_external_bonds=0) - for bond in molecule_template.bonds: - template_graph.add_edge(bond.atom1_index, bond.atom2_index) - - # DEBUG - # print(f'residue_graph: nodes {len(list(residue_graph.nodes))} edges {len(list(residue_graph.edges))}') - # print(f'template_graph: nodes {len(list(template_graph.nodes))} edges {len(list(template_graph.edges))}') - - # Determine graph isomorphism - from networkx.algorithms import isomorphism - - def node_match(n1, n2): - """Return True if nodes match, and False if not""" - return (n1["element"] == n2["element"]) and ( - n1["number_of_external_bonds"] == n2["number_of_external_bonds"] - ) - - graph_matcher = isomorphism.GraphMatcher(residue_graph, template_graph, node_match=node_match) - if not graph_matcher.is_isomorphic(): - return None - - # Translate to local residue atom indices - atom_index_within_residue = {atom: index for (index, atom) in enumerate(residue.atoms())} - matches = { - atom_index_within_residue[residue_atom]: template_atom - for (residue_atom, template_atom) in graph_matcher.mapping.items() - } - - return matches + from openmm.app.topology import MergedResidue + + bondedResidues = collections.defaultdict(set) + for atom1, atom2 in residue.chain.topology.bonds(): + if atom1.residue != atom2.residue: + bondedResidues[atom1.residue].add(atom2.residue) + bondedResidues[atom2.residue].add(atom1.residue) + bondedResidues = {x: list(y) for x, y in bondedResidues.items()} + + if residue in bondedResidues: + visited = set() + molecule = set() + residueStack = [residue] + neighborStack = [0] + while len(residueStack) > 0: + currentRes = residueStack[-1] + bonded = bondedResidues[currentRes] + molecule.add(currentRes) + visited.add(currentRes) + while neighborStack[-1] < len(bonded) and bonded[neighborStack[-1]] in visited: + neighborStack[-1] += 1 + if neighborStack[-1] < len(bonded): + residueStack.append(bonded[neighborStack[-1]]) + neighborStack.append(0) + else: + residueStack.pop() + neighborStack.pop() + residue = MergedResidue(list(molecule)) + + bondedToAtom = {atom.index: set() for atom in residue.atoms()} + for bond in residue.bonds(): + bondedToAtom[bond.atom1.index].add(bond.atom2.index) + bondedToAtom[bond.atom2.index].add(bond.atom1.index) + bondedToAtom = {index: sorted(bonded) for index, bonded in bondedToAtom.items()} + + return residue, bondedToAtom def _molecule_has_user_charges(self, molecule): """ @@ -273,9 +246,7 @@ def _generate_unique_atom_names(self, molecule): The molecule whose atom names are to be modified in-place """ - from collections import defaultdict - - element_counts = defaultdict(int) + element_counts = collections.defaultdict(int) for atom in molecule.atoms: symbol = atom.symbol element_counts[symbol] += 1 @@ -299,76 +270,60 @@ def generator(self, forcefield, residue): If the generator cannot parameterize the residue, it should return `False` and not modify `forcefield`. """ + from openmm.app.internal import compiled + if self._database_table_name is None: raise NotImplementedError( "SmallMoleculeTemplateGenerator is an abstract base class and cannot be used directly." ) - from io import StringIO + residue, bonded_to_atom = self._preprocess_residue(residue) + formula = tuple( + sorted( + collections.Counter( + atom.element.atomic_number for atom in residue.atoms() if atom.element is not None + ).items() + ) + ) + for molecule, matching_template in self._molecules.get(formula, []): + if compiled.matchResidueToTemplate(residue, matching_template, bonded_to_atom): + # We have a topology match to a known molecule; try to find an + # FFXML string already generated for it. + target_smiles = molecule.to_smiles() + ffxml_contents = None + + # See if we have an FFXML in the in-memory cache. + if target_smiles in self._ffxml_cache: + ffxml_contents = self._ffxml_cache[target_smiles] + + # If not, try to look one up in the on-disk cache. + if ffxml_contents is None and self._cache_path is not None: + with self._open_db() as db: + for entry in db.table(self._database_table_name): + if entry["smiles"] == target_smiles: + ffxml_contents = entry["ffxml"] + # Store it in the in-memory cache. + self._ffxml_cache[target_smiles] = ffxml_contents + break + + # If we still couldn't find one, we have to generate it. + if ffxml_contents is None: + ffxml_contents = self.generate_residue_template(molecule) + # Store it in the in-memory cache and the on-disk cache. + self._ffxml_cache[target_smiles] = ffxml_contents + if self._cache_path is not None: + with self._open_db() as db: + db.table(self._database_table_name).insert( + {"smiles": target_smiles, "ffxml": ffxml_contents} + ) - # TODO: Refactor to reduce code duplication - - _logger.info(f"Requested to generate parameters for residue {residue}") - - # If a database is specified, check against molecules in the database - if self._cache is not None: - with self._open_db() as db: - table = db.table(self._database_table_name) - for entry in table: - # Skip any molecules we've added to the database this session - if entry["smiles"] in self._smiles_added_to_db: - continue - - # See if the template matches - from openff.toolkit import Molecule - - molecule_template = Molecule.from_smiles(entry["smiles"], allow_undefined_stereo=True) - _logger.debug(f"Checking against {entry['smiles']}") - if self._match_residue(residue, molecule_template): - ffxml_contents = entry["ffxml"] - - # Write to debug file if requested - if self.debug_ffxml_filename is not None: - with open(self.debug_ffxml_filename, "w") as outfile: - _logger.debug(f"writing ffxml to {self.debug_ffxml_filename}") - outfile.write(ffxml_contents) - - # Add parameters and residue template for this residue - forcefield.loadFile(StringIO(ffxml_contents)) - # Signal success - return True - - # Check against the molecules we know about - for smiles, molecule in self._molecules.items(): - # See if the template matches - if self._match_residue(residue, molecule): - # Generate template and parameters. - ffxml_contents = self.generate_residue_template(molecule) - - # Write to debug file if requested if self.debug_ffxml_filename is not None: with open(self.debug_ffxml_filename, "w") as outfile: _logger.debug(f"writing ffxml to {self.debug_ffxml_filename}") outfile.write(ffxml_contents) - # Add the parameters and residue definition - forcefield.loadFile(StringIO(ffxml_contents)) - # If a cache is specified, add this molecule - if self._cache is not None: - with self._open_db() as db: - table = db.table(self._database_table_name) - _logger.debug(f"Writing residue template for {smiles} to cache {self._cache}") - record = {"smiles": smiles, "ffxml": ffxml_contents} - # Add the IUPAC name for convenience if we can - try: - record["iupac"] = molecule.to_iupac() - except Exception: - pass - # Store the record - table.insert(record) - self._smiles_added_to_db.add(smiles) - - # Signal success + forcefield.loadFile(io.StringIO(ffxml_contents)) + return True # Report that we have failed to parameterize the residue @@ -443,14 +398,12 @@ def __init__(self, molecules=None, forcefield=None, cache=None, template_generat """ Create a GAFFTemplateGenerator with some OpenFF toolkit molecules - Requies the OpenFF toolkit: http://openforcefield.org + Requires the OpenFF toolkit: http://openforcefield.org Parameters ---------- - molecules : openff.toolkit.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. + molecules : openff.toolkit.Molecule or list of Molecules, optional, default=None + Can be a Molecule or a list of Molecule objects. If specified, these molecules will be recognized and parameterized with antechamber as needed. The parameters will be cached in case they are encountered again the future. forcefield : str, optional, default=None @@ -1345,9 +1298,8 @@ def __init__(self, molecules=None, cache=None, forcefield=None, template_generat Parameters ---------- - molecules : openff.toolkit.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. + molecules : openff.toolkit.Molecule or list of Molecules, optional, default=None + Can be a Molecule or a list of Molecule objects. 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 @@ -1673,15 +1625,13 @@ def __init__( """ Create an EspalomaTemplateGenerator with some OpenFF toolkit molecules - Requies the OpenFF toolkit: http://openforcefield.org + Requires the OpenFF toolkit: http://openforcefield.org and espaloma: http://github.com/choderalab/espaloma Parameters ---------- - molecules : openff.toolkit.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. + molecules : openff.toolkit.Molecule or list of Molecules, optional, default=None + Can be a Molecule or a list of Molecule objects. If specified, these molecules will be recognized and parameterized with espaloma as needed. The parameters will be cached in case they are encountered again the future. cache : str, optional, default=None diff --git a/openmmforcefields/tests/test_system_generator.py b/openmmforcefields/tests/test_system_generator.py index 8edfc4d6..b0a4ca51 100644 --- a/openmmforcefields/tests/test_system_generator.py +++ b/openmmforcefields/tests/test_system_generator.py @@ -498,7 +498,7 @@ def test_cache(self, test_systems, small_molecule_forcefield): # Add molecules for each test system separately for name, testsystem in test_systems.items(): molecules = testsystem["molecules"] - # We don't need to add molecules that are already defined in the cache + generator.add_molecules(molecules) # Parameterize molecules for molecule in molecules: diff --git a/openmmforcefields/tests/test_template_generators.py b/openmmforcefields/tests/test_template_generators.py index eab80b3d..0b4d813a 100644 --- a/openmmforcefields/tests/test_template_generators.py +++ b/openmmforcefields/tests/test_template_generators.py @@ -21,7 +21,7 @@ GAFFTemplateGenerator, SMIRNOFFTemplateGenerator, ) -from openmmforcefields.utils import get_data_filename +from openmmforcefields.utils import Timer, get_data_filename if TYPE_CHECKING: import parmed @@ -231,14 +231,14 @@ def parameterize_with_charges(self, molecule, partial_charges): generator.add_molecules(molecule) return forcefield.createSystem(molecule.to_topology().to_openmm(), nonbondedMethod=NoCutoff) - def _make_template_generator(self): + def _make_template_generator(self, **kwargs): """ Makes a template generator for generic testing. By default, calls - `TEMPLATE_GENERATOR` with no arguments. Override in derived classes to - customize this behavior. + `TEMPLATE_GENERATOR` with the provided keyword arguments. Override in + derived classes to customize this behavior. """ - return self.TEMPLATE_GENERATOR() + return self.TEMPLATE_GENERATOR(**kwargs) @classmethod def get_permutation_indices(cls, system_1, system_2): @@ -555,6 +555,79 @@ def propagate_dynamics_single(self, molecule, system): return new_molecule + def test_cache(self): + """Test template generator cache capability""" + if type(self) is TemplateGeneratorBaseCase: + return # Can only run in derived classes + + with tempfile.TemporaryDirectory() as tmpdirname: + # Create a generator that also has a database cache + cache = os.path.join(tmpdirname, "db.json") + generator = self._make_template_generator(molecules=self.molecules, cache=cache) + # Create a ForceField + forcefield = ForceField() + # Register the template generator + forcefield.registerTemplateGenerator(generator.generator) + # Parameterize the molecules + timings = [] + for molecule in self.molecules: + openmm_topology = molecule.to_topology().to_openmm() + with Timer() as timer: + forcefield.createSystem(openmm_topology, nonbondedMethod=NoCutoff) + timings.append(timer.interval()) + + # Check database contents + def check_cache(generator, n_expected): + """ + Check database contains number of expected records + + Parameters + ---------- + generator : SmallMoleculeTemplateGenerator + The generator whose cache should be examined + n_expected : int + Number of expected records + """ + from tinydb import TinyDB + + db = TinyDB(generator._cache_path) + table = db.table(generator._database_table_name) + db_entries = table.all() + db.close() + n_entries = len(db_entries) + assert n_entries == n_expected, ( + f"Expected {n_expected} entries but database has {n_entries}\n db contents: {db_entries}" + ) + + check_cache(generator, len(self.molecules)) + + # Clean up, forcing closure of database + del forcefield, generator + + # Create a generator that also uses the database cache but has no molecules + print("Creating new generator with just cache...") + generator = self._make_template_generator(cache=cache) + # Check database still contains the molecules we expect + check_cache(generator, len(self.molecules)) + # Create a ForceField + forcefield = ForceField() + # Register the template generator + forcefield.registerTemplateGenerator(generator.generator) + # Parameterize the molecules; this should fail since we haven't added the molecules to the generator + for molecule in self.molecules: + openmm_topology = molecule.to_topology().to_openmm() + with pytest.raises(ValueError, match="No template found for residue"): + forcefield.createSystem(openmm_topology, nonbondedMethod=NoCutoff) + # Add the molecules and try again; this should succeed and be faster once the molecules have been added + generator.add_molecules(self.molecules) + timings_cached = [] + for molecule in self.molecules: + openmm_topology = molecule.to_topology().to_openmm() + with Timer() as timer: + forcefield.createSystem(openmm_topology, nonbondedMethod=NoCutoff) + timings_cached.append(timer.interval()) + assert all(timing > timing_cached for timing, timing_cached in zip(timings, timings_cached, strict=True)) + @pytest.mark.gaff class TestGAFFTemplateGenerator(TemplateGeneratorBaseCase): @@ -746,63 +819,6 @@ def test_debug_ffxml(self): # TODO: Test that systems are equivalent assert system.getNumParticles() == system2.getNumParticles() - def test_cache(self): - """Test template generator cache capability""" - with tempfile.TemporaryDirectory() as tmpdirname: - # Create a generator that also has a database cache - cache = os.path.join(tmpdirname, "db.json") - generator = self.TEMPLATE_GENERATOR(molecules=self.molecules, cache=cache) - # Create a ForceField - forcefield = ForceField() - # Register the template generator - forcefield.registerTemplateGenerator(generator.generator) - # Parameterize the molecules - for molecule in self.molecules: - openmm_topology = molecule.to_topology().to_openmm() - forcefield.createSystem(openmm_topology, nonbondedMethod=NoCutoff) - - # Check database contents - def check_cache(generator, n_expected): - """ - Check database contains number of expected records - - Parameters - ---------- - generator : SmallMoleculeTemplateGenerator - The generator whose cache should be examined - n_expected : int - Number of expected records - """ - from tinydb import TinyDB - - db = TinyDB(generator._cache) - table = db.table(generator._database_table_name) - db_entries = table.all() - db.close() - n_entries = len(db_entries) - assert n_entries == n_expected, ( - f"Expected {n_expected} entries but database has {n_entries}\n db contents: {db_entries}" - ) - - check_cache(generator, len(self.molecules)) - - # Clean up, forcing closure of database - del forcefield, generator - - # Create a generator that also uses the database cache but has no molecules - print("Creating new generator with just cache...") - generator = self.TEMPLATE_GENERATOR(cache=cache) - # Check database still contains the molecules we expect - check_cache(generator, len(self.molecules)) - # Create a ForceField - forcefield = ForceField() - # Register the template generator - forcefield.registerTemplateGenerator(generator.generator) - # Parameterize the molecules; this should succeed - for molecule in self.molecules: - openmm_topology = molecule.to_topology().to_openmm() - forcefield.createSystem(openmm_topology, nonbondedMethod=NoCutoff) - def test_add_solvent(self): """Test using openmm.app.Modeller to add solvent to a small molecule parameterized by template generator""" # Select a molecule to add solvent around @@ -1043,12 +1059,12 @@ def test_multiple_registration(self): class TestSMIRNOFFTemplateGenerator(TemplateGeneratorBaseCase): TEMPLATE_GENERATOR = SMIRNOFFTemplateGenerator - def _make_template_generator(self): + def _make_template_generator(self, **kwargs): """ Makes a `SMIRNOFFTemplateGenerator` for generic testing. """ - return SMIRNOFFTemplateGenerator(forcefield="openff-2.3.0") + return SMIRNOFFTemplateGenerator(forcefield="openff-2.3.0", **kwargs) def test_INSTALLED_FORCEFIELDS(self): """Test that names in INSTALLED_FORCEFIELDS resolve correctly""" @@ -1515,6 +1531,82 @@ def test_energies_virtual_sites(self): new_positions = self.propagate_dynamics(new_positions, openmm_system) self.compare_energies(molecules[0].to_hill_formula(), new_positions, openmm_system, smirnoff_system) + def test_energies_multiple_residue(self): + """Test parameterizing a multi-residue molecule""" + + for test_file in ("test-ala-3.pdb", "test-aa.pdb"): + data_file = get_data_filename(test_file) + ff_file = "openff_unconstrained-2.3.0.offxml" + + pdb = PDBFile(data_file) + off_top = Topology.from_pdb(data_file) + + molecules = [off_top.molecule(0)] + generator = SMIRNOFFTemplateGenerator(molecules=molecules, forcefield=ff_file) + openmm_forcefield = openmm.app.ForceField() + openmm_forcefield.registerTemplateGenerator(generator.generator) + + smirnoff_system = OFFForceField(ff_file).create_openmm_system(off_top) + openmm_system = openmm_forcefield.createSystem(pdb.topology, nonbondedMethod=NoCutoff) + + new_positions = self.propagate_dynamics(pdb.positions, openmm_system) + self.compare_energies("test_energies_multiple_residue", new_positions, openmm_system, smirnoff_system) + new_positions = self.propagate_dynamics(new_positions, openmm_system) + self.compare_energies("test_energies_multiple_residue", new_positions, openmm_system, smirnoff_system) + + def test_virtual_site_spans_residues(self): + """Test parameterizing a multi-residue molecule with a virtual site""" + from openff.toolkit.typing.engines.smirnoff.parameters import VirtualSiteType + + data_file = get_data_filename("test-ala-3.pdb") + ff_file = "openff_unconstrained-2.3.0.offxml" + + pdb = PDBFile(data_file) + off_top = Topology.from_pdb(data_file) + + openff_forcefield = OFFForceField(ff_file) + openff_forcefield.get_parameter_handler("VirtualSites") + openff_forcefield["VirtualSites"].add_parameter( + parameter=VirtualSiteType( + smirks="[#6X4:2][#7X3:1]([#1:3])[#6X3:4]", + type="TrivalentLonePair", + match="once", + distance="0.5 * nanometer ** 1", + outOfPlaneAngle="None", + inPlaneAngle="None", + epsilon="1.0 * kilocalories_per_mole", + sigma="3 * angstrom", + charge_increment1="0.01 * elementary_charge ** 1", + charge_increment2="-0.01 * elementary_charge ** 1", + charge_increment3="-0.02 * elementary_charge ** 1", + charge_increment4="-0.03 * elementary_charge ** 1", + ), + ) + smirnoff_system = openff_forcefield.create_openmm_system(off_top) + # Assert there's at least one vsite in the output + assert 0 != sum([smirnoff_system.isVirtualSite(i) for i in range(smirnoff_system.getNumParticles())]) + + # Reverse the order of atoms in the molecule that will be fed to + # generate the template to test that parameters still get assigned + # correctly when the template and topology atom orders don't match + molecule = off_top.molecule(0) + molecule = molecule.remap({i: molecule.n_atoms - i - 1 for i in range(molecule.n_atoms)}) + + generator = SMIRNOFFTemplateGenerator(molecules=[molecule], forcefield=openff_forcefield.to_string()) + openmm_forcefield = openmm.app.ForceField() + openmm_forcefield.registerTemplateGenerator(generator.generator) + + # Add the virtual sites to the OpenMM topology before creating the + # system through the template generator + modeller = openmm.app.Modeller(pdb.topology, pdb.positions) + modeller.addExtraParticles(openmm_forcefield) + openmm_system = openmm_forcefield.createSystem(modeller.topology, nonbondedMethod=NoCutoff) + + new_positions = self.propagate_dynamics(modeller.positions, openmm_system) + self.compare_energies("test_energies_multiple_residue", new_positions, openmm_system, smirnoff_system) + new_positions = self.propagate_dynamics(new_positions, openmm_system) + self.compare_energies("test_energies_multiple_residue", new_positions, openmm_system, smirnoff_system) + def test_bespoke_force_field(self): """ Make sure a molecule can be parameterised using a bespoke force field passed as a string to