|
1 | 1 | import glob
|
2 | 2 | import itertools as it
|
3 | 3 | import os
|
4 |
| - |
| 4 | +from scipy.constants import N_A, k, calorie |
5 | 5 | import parmed as pmd
|
6 | 6 | from pkg_resources import resource_filename
|
7 | 7 | import pytest
|
8 | 8 |
|
9 | 9 | from foyer import Forcefield
|
10 | 10 | from foyer.tests.utils import atomtype
|
11 | 11 |
|
| 12 | +from numpy.testing import assert_almost_equal |
| 13 | + |
12 | 14 | TRAPPE_UA = Forcefield(name='trappe-ua')
|
13 | 15 |
|
14 | 16 | TRAPPE_TESTFILES_DIR = resource_filename('foyer', 'trappe_validation')
|
@@ -45,12 +47,199 @@ def find_correctly_implemented(self):
|
45 | 47 |
|
46 | 48 | @pytest.mark.parametrize('mol_name', correctly_implemented)
|
47 | 49 | def test_atomtyping(self, mol_name, testfiles_dir=TRAPPE_TESTFILES_DIR):
|
48 |
| - files = glob.glob(os.path.join(testfiles_dir, mol_name, '*')) |
| 50 | + files = glob.glob(os.path.join(testfiles_dir, mol_name, '*.mol2')) |
49 | 51 | for mol_file in files:
|
50 | 52 | _, ext = os.path.splitext(mol_file)
|
51 | 53 | mol2_path = os.path.join(testfiles_dir, mol_name, mol_file)
|
52 | 54 | structure = pmd.load_file(mol2_path, structure=True)
|
53 | 55 | atomtype(structure, TRAPPE_UA, non_atomistic=True)
|
54 | 56 |
|
| 57 | +@pytest.fixture |
| 58 | +def trappe_ref(request): |
| 59 | + # load raw trappe csv file and process slightly |
| 60 | + container = { |
| 61 | + '(pseudo)atom': [], |
| 62 | + 'stretch': [], |
| 63 | + 'bend': [], |
| 64 | + 'torsion': [], |
| 65 | + } |
| 66 | + # split csv file into sections |
| 67 | + csv_file = os.path.join(TRAPPE_TESTFILES_DIR, |
| 68 | + request.param) |
| 69 | + with open(csv_file, 'r') as inf: |
| 70 | + for line in inf: |
| 71 | + if line.startswith('#'): |
| 72 | + section_clue = line.split(",")[1] |
| 73 | + section = container[section_clue] |
| 74 | + line = next(inf) |
| 75 | + while not line == '\n': |
| 76 | + # slurp until newline hit |
| 77 | + section.append(line) |
| 78 | + try: |
| 79 | + line = next(inf) |
| 80 | + except StopIteration: |
| 81 | + break |
| 82 | + |
| 83 | + return container |
| 84 | + |
| 85 | + |
| 86 | +@pytest.fixture |
| 87 | +def mol2file(request): |
| 88 | + # load a mol2 file and apply foyer's trappe-ua forcefield |
| 89 | + fname = request.param |
| 90 | + molfile = glob.glob(os.path.join(TRAPPE_TESTFILES_DIR, fname, '*'))[0] |
| 91 | + structure = pmd.load_file(molfile, structure=True) |
| 92 | + |
| 93 | + return TRAPPE_UA.apply(structure, |
| 94 | + assert_angle_params=False, |
| 95 | + assert_dihedral_params=False, |
| 96 | + ) |
| 97 | + |
| 98 | + |
| 99 | +def parmed2trappe(val): |
| 100 | + # convert kcal/mol (parmed units) to K/k_B (trappe units) |
| 101 | + return val * calorie / (k * N_A / 1000.) |
| 102 | + |
| 103 | +def angle_parmed2trappe(val): |
| 104 | + # trappe angle units need doubling |
| 105 | + return 2 * parmed2trappe(val) |
| 106 | + |
| 107 | +def assert_dihedrals_match(foyer_d, reference_params): |
| 108 | + # Check that a dihedral from foyer matches the text in the csv file |
| 109 | + # foyer_d - parmed rb_torsion |
| 110 | + # reference_params - dihedral parameters from trappe csv file |
| 111 | + def conv1(t): |
| 112 | + # convert RB to original Trappe dihedral form: |
| 113 | + # V = c0 + c1(1 + cos phi) + c1(1 - cos 2phi) + c3(1 + cos 3phi) |
| 114 | + R0, R1, R2, R3 = t.c0, t.c1, t.c2, t.c3 |
| 115 | + c0 = - R0 + R1 + R2 + R3 |
| 116 | + c1 = - R1 - 3/4. * R3 |
| 117 | + c2 = - 0.5 * R2 |
| 118 | + c3 = - 1/4. * R3 |
| 119 | + |
| 120 | + return c0, c1, c2, c3 |
| 121 | + |
| 122 | + def conv2(t): |
| 123 | + # convert RB to second Trappe dihedral form: |
| 124 | + # V = c0 + c1 cos phi + c2 cos 2phi + c3 cos 3phi + c4 cos 4phi |
| 125 | + R0, R1, R2, R3, R4 = t.c0, t.c1, t.c2, t.c3, t.c4 |
| 126 | + print(list(map(lambda x: x * calorie, [R0, R1, R2, R3, R4]))) |
| 127 | + c0 = - R0 + 0.5 * R2 + 3/8. * R4 |
| 128 | + c1 = - R1 - 3/4. * R3 |
| 129 | + c2 = 0.5 * R2 + 0.5 * R4 |
| 130 | + c3 = - 1/4. * R3 |
| 131 | + c4 = 1/8. * R4 |
| 132 | + |
| 133 | + return [parmed2trappe(v) for v in [c0, c1, c2, c3, c4]] |
| 134 | + |
| 135 | + reference_params = list(map(float, reference_params)) |
| 136 | + # check parameters in *foyer_d* match the raw text from trappe |
| 137 | + # trappe's params are in one of two functional forms..... |
| 138 | + if len(reference_params) == 5: |
| 139 | + # using later form, has an extra column |
| 140 | + foyer_params = conv2(foyer_d.type) |
| 141 | + elif len(reference_params) == 4: |
| 142 | + # using original dihedral form |
| 143 | + foyer_params = conv1(foyer_d.type) |
| 144 | + |
| 145 | + assert_almost_equal(foyer_params, reference_params, decimal=3) |
| 146 | + |
| 147 | +class TestTrappeReferences(object): |
| 148 | + @staticmethod |
| 149 | + def check_atoms(ref, struc): |
| 150 | + ref_atoms = sorted(ref['(pseudo)atom'], |
| 151 | + key=lambda x: int(x.split(',')[0])) |
| 152 | + for ref_atom, foyer_atom in zip(ref_atoms, struc.atoms): |
| 153 | + _, _, _, epsilon, sigma, charge = ref_atom.strip().split(',') |
| 154 | + |
| 155 | + assert_almost_equal(foyer_atom.charge, float(charge)) |
| 156 | + assert_almost_equal(foyer_atom.sigma, float(sigma)) |
| 157 | + assert_almost_equal(parmed2trappe(foyer_atom.epsilon), |
| 158 | + float(epsilon), |
| 159 | + decimal=3) |
| 160 | + |
| 161 | + @staticmethod |
| 162 | + def check_bonds(ref, struc): |
| 163 | + ref_bonds = ref['stretch'] |
| 164 | + # organise foyer's bonds into dict of indices |
| 165 | + bond_dict = {} |
| 166 | + for b in struc.bonds: |
| 167 | + i, j = sorted((b.atom1.idx, b.atom2.idx)) |
| 168 | + bond_dict[i+1, j+1] = b |
| 169 | + |
| 170 | + # iterate reference file bonds |
| 171 | + # lookup foyer bond and compare length |
| 172 | + for ref_bond in ref_bonds: |
| 173 | + _, desc, _, length = ref_bond.strip().split(',') |
| 174 | + i, j = map(int, desc.strip("\"\'").split('-')) |
| 175 | + foyer_bond = bond_dict[i, j] |
| 176 | + |
| 177 | + assert_almost_equal(foyer_bond.type.req, float(length)) |
| 178 | + # trappe is fixed bond length, no point in K comparison |
| 179 | + |
| 180 | + @staticmethod |
| 181 | + def check_angles(ref, struc): |
| 182 | + ref_angles = ref['bend'] |
| 183 | + assert len(ref_angles) == len(struc.angles) |
| 184 | + |
| 185 | + angle_dict = {} |
| 186 | + for a in struc.angles: |
| 187 | + i, j, k = a.atom1.idx, a.atom2.idx, a.atom3.idx |
| 188 | + if k < i: |
| 189 | + i, j, k = k, j, i |
| 190 | + angle_dict[i+1, j+1, k+1] = a |
| 191 | + |
| 192 | + for ref_angle in ref_angles: |
| 193 | + _, desc, _, angle, k_angle = ref_angle.strip().split(',') |
| 194 | + i, j, k = map(int, desc.strip("\"\'").split('-')) |
| 195 | + |
| 196 | + foyer_angle = angle_dict[i, j, k] |
| 197 | + |
| 198 | + assert_almost_equal(foyer_angle.type.theteq, |
| 199 | + float(angle), |
| 200 | + decimal=4) |
| 201 | + assert_almost_equal(angle_parmed2trappe(foyer_angle.type.k), |
| 202 | + float(k_angle), |
| 203 | + decimal=3) |
| 204 | + |
| 205 | + @staticmethod |
| 206 | + def check_dihedrals(ref, struc): |
| 207 | + ref_dihedrals = ref['torsion'] |
| 208 | + |
| 209 | + assert len(ref_dihedrals) == len(struc.rb_torsions) |
| 210 | + |
| 211 | + dih_dict = {} |
| 212 | + for d in struc.rb_torsions: |
| 213 | + i, j, k, l = d.atom1.idx, d.atom2.idx, d.atom3.idx, d.atom4.idx |
| 214 | + |
| 215 | + if l < i: |
| 216 | + i, j, k, l = l, k, j, i |
| 217 | + dih_dict[i+1, j+1, k+1, l+1] = d |
| 218 | + |
| 219 | + for ref_d in ref_dihedrals: |
| 220 | + _, desc, _, *params = ref_d.strip().split(',') |
| 221 | + i, j, k, l = map(int, desc.strip("\"\'").split('-')) |
| 222 | + if l < i: |
| 223 | + i, j, k, l = l, k, j, i |
| 224 | + |
| 225 | + foyer_d = dih_dict[i, j, k, l] |
| 226 | + |
| 227 | + assert_dihedrals_match(foyer_d, params) |
| 228 | + |
| 229 | + @pytest.mark.parametrize('trappe_ref,mol2file', [ |
| 230 | + ('butadiene/trappe_parameters_94.csv', 'butadiene'), |
| 231 | + ('isoprene/trappe_parameters_95.csv', 'isoprene'), |
| 232 | + ('methyl_acrylate/trappe_parameters_96.csv', 'methyl_acrylate'), |
| 233 | + ], indirect=True) |
| 234 | + def test_trappe_reference(self, trappe_ref, mol2file): |
| 235 | + self.check_atoms(trappe_ref, mol2file) |
| 236 | + |
| 237 | + self.check_bonds(trappe_ref, mol2file) |
| 238 | + |
| 239 | + self.check_angles(trappe_ref, mol2file) |
| 240 | + |
| 241 | + self.check_dihedrals(trappe_ref, mol2file) |
| 242 | + |
| 243 | + |
55 | 244 | if __name__ == '__main__':
|
56 | 245 | TestTraPPE().find_correctly_implemented()
|
0 commit comments