-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathtopology.py
242 lines (220 loc) · 9.74 KB
/
topology.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
"""
I/O of topology parameters that are not molecules.
"""
import itertools
from collections import namedtuple
import textwrap
import vermouth
from vermouth.file_writer import deferred_open
from vermouth.citation_parser import citation_formatter
from ..log_helpers import StyleAdapter, get_logger
from .itp import _interaction_sorting_key
LOGGER = StyleAdapter(get_logger(__name__))
Atomtype = namedtuple('Atomtype', 'molecule node sigma epsilon meta')
NonbondParam = namedtuple('NonbondParam', 'atoms sigma epsilon meta')
def _group_by_conditionals(interactions):
interactions_group_sorted = sorted(interactions,
key=_interaction_sorting_key
)
interaction_grouped = itertools.groupby(
interactions_group_sorted,
key=_interaction_sorting_key
)
return interaction_grouped
def sigma_epsilon_to_C6_C12(sigma, epsilon):
"""
Convert the LJ potential from sigma epsilon
form to C6 C12 form.
"""
C6 = 4*sigma*epsilon**6
C12 = 4*sigma*epsilon**12
return C6, C12
def write_atomtypes(system, itp_path, C6C12=False):
"""
Writes the [atomtypes] directive to file.
All atomtypes are defined in system.gmx_topology_params.
Masses and further information are taken from the molecule
directly.
"""
conditional_keys = {True: '#ifdef', False: '#ifndef'}
with deferred_open(itp_path, "w") as itp_file:
itp_file.write("[ atomtypes ]\n")
grouped_types = _group_by_conditionals(system.gmx_topology_params['atomtypes'])
for (conditional, group), interactions_in_group in grouped_types:
# conditionals are things like #ifdef; for more details on how this
# works see the molecule_itp_writer
if conditional:
conditional_key = conditional_keys[conditional[1]]
itp_file.write('{} {}\n'.format(conditional_key, conditional[0]))
# groups are collections of interactions that are written bunched
# together and indicated by a comment line
if group:
itp_file.write('; {}\n'.format(group))
for atomtype in interactions_in_group:
atype = atomtype.molecule.nodes[atomtype.node]['atype']
charge = atomtype.molecule.nodes[atomtype.node]['charge']
mass = atomtype.molecule.nodes[atomtype.node]['mass']
if C6C12:
nb1, nb2 = sigma_epsilon_to_C6_C12(atomtype.sigma, atomtype.epsilon)
else:
nb1, nb2 = atomtype.sigma, atomtype.epsilon
if 'comment' in atomtype.meta:
comments = ";" + " ".join(atomtype.meta['comment'])
else:
comments = ""
itp_file.write(f"{atype} {mass} {charge} A {nb1:3.8F} {nb2:3.8F} {comments}\n")
def write_nonbond_params(system, itp_path, C6C12=False):
"""
Writes the [nonbond_params] directive to file.
All atomtypes are defined in system.gmx_topology_params.
Masses and further information are taken from the molecule
directly.
"""
conditional_keys = {True: '#ifdef', False: '#ifndef'}
with deferred_open(itp_path, "w") as itp_file:
itp_file.write("[ nonbond_params ]\n")
grouped_types = _group_by_conditionals(system.gmx_topology_params['nonbond_params'])
for (conditional, group), interactions_in_group in grouped_types:
if conditional:
conditional_key = conditional_keys[conditional[1]]
itp_file.write('{} {}\n'.format(conditional_key, conditional[0]))
if group:
itp_file.write('; {}\n'.format(group))
for nb_params in interactions_in_group:
if len(nb_params.atoms) == 2:
a1, a2 = nb_params.atoms
# self interaction
else:
a1 = nb_params.atoms[0]
a2 = nb_params.atoms[0]
if C6C12:
nb1, nb2 = sigma_epsilon_to_C6_C12(nb_params.sigma, nb_params.epsilon)
else:
nb1, nb2 = nb_params.sigma, nb_params.epsilon
if nb_params.meta.get('comment'):
comments = ";" + " ".join(nb_params.meta['comment'])
else:
comments = ""
itp_file.write(f"{a1} {a2} 1 {nb1:3.8F} {nb2:3.8F} {comments}\n")
def write_gmx_topology(system,
top_path,
itp_paths={"nonbond_params": "extra_nbparams.itp",
"atomtypes": "extra_atomtypes.itp"},
C6C12=False,
defines=(),
header=()):
"""
Writes a Gromacs .top file for the specified system. Gromacs topology
files are defined by directives for example `[ atomtypes ]`. However,
Gromacs supports writing parts of the topology to so called .itp
files which can be inculded into a toplevel topology file with the
extension .top using #include statements. The topology writer will
generate such a toplevel topology file where the different directives
are written to seperate .itp files and included into the toplevel
file.
Parameters
----------
system: vermouth.system.System
top_path: pathlib.Path
path for topology file
itp_paths: dict[str, pathlib.Path]
list of paths for writing the topology parameters
like atomtypes with the key being the name of the
directive.
C6C12: bool
write non-bonded interaction parameters using LJ
C6C12 form
defines: tuple(str)
define statments to include in the topology
header: tuple(str)
any comment lines to include at the beginning
"""
if not system.molecules:
raise ValueError("No molecule in the system. Nothing to write.")
include_string = ""
# First we write the atomtypes directive
if "atomtypes" in system.gmx_topology_params:
_path = itp_paths['atomtypes']
write_atomtypes(system, _path, C6C12)
include_string += f'\n #include "{_path}"'
# Next we write the nonbond_params directive
if "nonbond_params" in system.gmx_topology_params:
_path = itp_paths['nonbond_params']
write_nonbond_params(system, _path, C6C12)
include_string += f'\n #include "{_path}"\n'
# Write the ITP files for the molecule types, and prepare writing the
# [ molecules ] section of the top file.
# * We write one ITP file for each different moltype in the system, the
# moltype being defined by the name provided under the "moltype" meta of
# the molecules. If more than one molecule share the same moltype, we use
# the first one to write the ITP file.
moltype_written = set()
# * We keep track of the length of the longer moltype name, to align the
# [ molecules ] section.
max_name_length = 0
# * We keep track of groups of successive molecules with the same moltypes.
moltype_count = [] # items will be [moltype, number of molecules]
# Iterate over groups of successive molecules with the same moltypes. We
# shall *NOT* sort the molecules before hand, as groups of successive
# molecules with the same moltype can be interupted by other moltypes, and
# we want to reflect these interuptions in the [ molecules ] section of the
# top file.
molecule_groups = itertools.groupby(
system.molecules, key=lambda x: x.meta["moltype"]
)
for moltype, molecules in molecule_groups:
molecule = next(molecules)
if moltype not in moltype_written:
# A given moltype can appear more than once in the sequence of
# molecules, without being uninterupted by other moltypes. Even in
# that case, we want to write the ITP only once.
with deferred_open("{}.itp".format(moltype), "w") as outfile:
# here we format and merge all citations
header[-1] = header[-1] + "\n"
header.append("Please cite the following papers:")
for citation in molecule.citations:
cite_string = citation_formatter(
molecule.force_field.citations[citation]
)
LOGGER.info("Please cite: " + cite_string)
header.append(cite_string)
vermouth.gmx.itp.write_molecule_itp(molecule, outfile, header=header)
this_moltype_len = len(molecule.meta["moltype"])
if this_moltype_len > max_name_length:
max_name_length = this_moltype_len
moltype_written.add(moltype)
# We already removed one element from the "molecules" generator, do not
# forget to count it in the number of molecules in that group.
moltype_count.append([moltype, 1 + len(list(molecules))])
# Write the top file
template = textwrap.dedent(
"""\
{defines}
#include "martini.itp"
{includes}
[ system ]
Title of the system
[ molecules ]
{molecules}
"""
)
include_string = include_string + "\n".join(
'#include "{}.itp"'.format(molecule_type) for molecule_type, _ in moltype_count
)
molecule_string = "\n".join(
"{mtype:<{length}} {num}".format(
mtype=mtype, num=num, length=max_name_length
)
for mtype, num in moltype_count
)
define_string = "\n".join("#define {}".format(define) for define in defines)
with deferred_open(str(top_path), "w") as outfile:
outfile.write(
textwrap.dedent(
template.format(
includes=include_string,
molecules=molecule_string,
defines=define_string,
)
)
)