mdinterface 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mdinterface/__init__.py +10 -0
- mdinterface/core/__init__.py +8 -0
- mdinterface/core/specie.py +507 -0
- mdinterface/core/topology.py +220 -0
- mdinterface/io/__init__.py +8 -0
- mdinterface/io/lammpswriter.py +261 -0
- mdinterface/io/packmol.py +35 -0
- mdinterface/io/read.py +126 -0
- mdinterface/simulationbox.py +621 -0
- mdinterface/utils/__init__.py +8 -0
- mdinterface/utils/auxiliary.py +262 -0
- mdinterface/utils/database.py +111 -0
- mdinterface/utils/map.py +269 -0
- mdinterface-1.0.0.dist-info/LICENSE +201 -0
- mdinterface-1.0.0.dist-info/METADATA +76 -0
- mdinterface-1.0.0.dist-info/RECORD +18 -0
- mdinterface-1.0.0.dist-info/WHEEL +5 -0
- mdinterface-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Created on Tue Oct 24 15:14:41 2023
|
|
5
|
+
|
|
6
|
+
@author: roncoroni
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from mdinterface.utils.auxiliary import label_to_element, as_list, find_smallest_missing
|
|
10
|
+
from mdinterface.io.lammpswriter import DATAWriter
|
|
11
|
+
from mdinterface.io.packmol import header, box_place, fix_place
|
|
12
|
+
|
|
13
|
+
import MDAnalysis as mda
|
|
14
|
+
|
|
15
|
+
import ase
|
|
16
|
+
from ase import units
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
import subprocess
|
|
20
|
+
import shutil
|
|
21
|
+
|
|
22
|
+
import warnings
|
|
23
|
+
warnings.filterwarnings('ignore')
|
|
24
|
+
|
|
25
|
+
#%%
|
|
26
|
+
|
|
27
|
+
class SimulationBox():
|
|
28
|
+
|
|
29
|
+
def __init__(self, solvent=None, solute=None, interface=None,
|
|
30
|
+
enderface=None):
|
|
31
|
+
|
|
32
|
+
# start species
|
|
33
|
+
self._setup_species(solvent, solute, interface, enderface)
|
|
34
|
+
|
|
35
|
+
# check interface indexing
|
|
36
|
+
self._make_sandwich()
|
|
37
|
+
|
|
38
|
+
# fix indexes of topology elements
|
|
39
|
+
self._update_topology_indexes()
|
|
40
|
+
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _setup_species(self, solvent, solute, interface, enderface):
|
|
45
|
+
|
|
46
|
+
self._solvent = None
|
|
47
|
+
self._solute = None
|
|
48
|
+
self._interface = None
|
|
49
|
+
self._enderface = None
|
|
50
|
+
|
|
51
|
+
# assign variables
|
|
52
|
+
if solvent is not None:
|
|
53
|
+
self._solvent = solvent.copy()
|
|
54
|
+
if solute is not None:
|
|
55
|
+
self._solute = [ii.copy() for ii in as_list(solute)]
|
|
56
|
+
if interface is not None:
|
|
57
|
+
self._interface = interface.copy()
|
|
58
|
+
if enderface is not None:
|
|
59
|
+
self._enderface = enderface.copy() # use this to make a good sandwich!
|
|
60
|
+
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
def _make_sandwich(self):
|
|
64
|
+
|
|
65
|
+
if self._interface is not None:
|
|
66
|
+
for atom in self._interface._stype:
|
|
67
|
+
atom.set_label(atom.label + "_i")
|
|
68
|
+
|
|
69
|
+
if self._enderface is not None:
|
|
70
|
+
for atom in self._enderface._stype:
|
|
71
|
+
atom.set_label(atom.label + "_e")
|
|
72
|
+
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _update_topology_indexes(self):
|
|
77
|
+
|
|
78
|
+
nitems = {
|
|
79
|
+
"_btype" : [],
|
|
80
|
+
"_atype" : [],
|
|
81
|
+
"_dtype" : [],
|
|
82
|
+
"_itype" : [],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for attribute in nitems:
|
|
86
|
+
for specie in self._species:
|
|
87
|
+
for attr in specie.__getattribute__(attribute):
|
|
88
|
+
if attr.id not in nitems[attribute]:
|
|
89
|
+
nitems[attribute].append(attr.id)
|
|
90
|
+
else:
|
|
91
|
+
idx = find_smallest_missing(nitems[attribute], start=1)
|
|
92
|
+
attr.set_id(idx)
|
|
93
|
+
nitems[attribute].append(attr.id)
|
|
94
|
+
|
|
95
|
+
#resort atom types by alph order
|
|
96
|
+
atom_types = []
|
|
97
|
+
for specie in self._species:
|
|
98
|
+
atom_types.extend([stype.extended_label for stype in specie._stype])
|
|
99
|
+
atom_types.sort()
|
|
100
|
+
|
|
101
|
+
for specie in self._species:
|
|
102
|
+
for stype in specie._stype:
|
|
103
|
+
idx = np.argwhere(stype.extended_label == np.array(atom_types))[0][0]
|
|
104
|
+
stype.set_id(idx+1)
|
|
105
|
+
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# populate a box with solvent and ions
|
|
110
|
+
@staticmethod
|
|
111
|
+
def populate_box(volume, instructions, input_file="input_packmol.in",
|
|
112
|
+
output_file="system.pdb"):
|
|
113
|
+
|
|
114
|
+
if not instructions:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
# check volume
|
|
118
|
+
assert len(volume) == 3, "Check volume!"
|
|
119
|
+
|
|
120
|
+
# generate box boundaries with 1 AA padding
|
|
121
|
+
box = np.concatenate(([1,1,1], np.asarray(volume)-1)).tolist()
|
|
122
|
+
|
|
123
|
+
tmp_files = ["packmol.log", "input_packmol.in", "system.pdb"]
|
|
124
|
+
with open(input_file, "w") as fout:
|
|
125
|
+
|
|
126
|
+
fout.write(header.format(output_file, np.random.randint(100000)))
|
|
127
|
+
|
|
128
|
+
for cc, instruction in enumerate(instructions):
|
|
129
|
+
|
|
130
|
+
# unpack instructions
|
|
131
|
+
mol = instruction[0]
|
|
132
|
+
rep = instruction[1]
|
|
133
|
+
typ = instruction[2]
|
|
134
|
+
|
|
135
|
+
if isinstance(rep, int):
|
|
136
|
+
if not rep:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
if typ == "box": # normal add
|
|
140
|
+
fout.write(box_place.format(cc, rep, " ".join(map(str, box))))
|
|
141
|
+
|
|
142
|
+
elif typ == "fixed": # coordinate -> fixed point
|
|
143
|
+
|
|
144
|
+
fout.write(fix_place.format(cc, *rep))
|
|
145
|
+
# make temp box to fill
|
|
146
|
+
# tbox = box.copy()
|
|
147
|
+
# tbox[2] = rep - 1
|
|
148
|
+
# tbox[-1] = rep + 1
|
|
149
|
+
|
|
150
|
+
# fout.write(box_place.format(cc, 1, " ".join(map(str, tbox))))
|
|
151
|
+
|
|
152
|
+
else:
|
|
153
|
+
raise "Wrong instructions"
|
|
154
|
+
|
|
155
|
+
# write tmp pdb file and store info
|
|
156
|
+
mol.atoms.write("mol_{}.pdb".format(cc))
|
|
157
|
+
tmp_files.append("mol_{}.pdb".format(cc))
|
|
158
|
+
|
|
159
|
+
# run packmol
|
|
160
|
+
try:
|
|
161
|
+
subprocess.run(['packmol < {} > packmol.log'.format(input_file)],
|
|
162
|
+
shell=True, check=True, text=True)
|
|
163
|
+
|
|
164
|
+
except:
|
|
165
|
+
print("WARNING: packmol might not have worked, check system.")
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
universe = mda.Universe(output_file)
|
|
169
|
+
except:
|
|
170
|
+
universe = None
|
|
171
|
+
|
|
172
|
+
# remove temp mol files and packmol files
|
|
173
|
+
subprocess.call(['rm'] + tmp_files)
|
|
174
|
+
|
|
175
|
+
return universe
|
|
176
|
+
|
|
177
|
+
# generate a slab from a unit cell
|
|
178
|
+
@staticmethod
|
|
179
|
+
def make_interface_slab(interface_uc, xsize, ysize, layers=1):
|
|
180
|
+
|
|
181
|
+
if layers == 0 or interface_uc is None:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
xrep = int(np.round(xsize/interface_uc.atoms.get_cell()[0][0]))
|
|
185
|
+
yrep = int(np.round(ysize/interface_uc.atoms.get_cell()[1][1]))
|
|
186
|
+
|
|
187
|
+
slab = interface_uc.copy()
|
|
188
|
+
|
|
189
|
+
if not np.isclose(np.dot(slab.atoms.cell[0], [1,0,0]), slab.atoms.cell[0][0]):
|
|
190
|
+
xrep +=1
|
|
191
|
+
print("WARNING: check interface if pattern matches")
|
|
192
|
+
|
|
193
|
+
if not np.isclose(np.dot(slab.atoms.cell[1], [0,1,0]), slab.atoms.cell[1][1]):
|
|
194
|
+
yrep +=1
|
|
195
|
+
print("WARNING: check interface if pattern matches")
|
|
196
|
+
|
|
197
|
+
slab.repeat((xrep, yrep, 1), make_cubic=True)
|
|
198
|
+
|
|
199
|
+
if layers > 1: # helps with indexing
|
|
200
|
+
slab.repeat([1,1,layers])
|
|
201
|
+
|
|
202
|
+
slab.atoms.center()
|
|
203
|
+
# slab.atoms.rattle()
|
|
204
|
+
|
|
205
|
+
return slab
|
|
206
|
+
|
|
207
|
+
@staticmethod #THANKS CHATGPT
|
|
208
|
+
def populate_with_ions(ions, nions, volume, ion_pos=False):
|
|
209
|
+
volume = np.array(volume)
|
|
210
|
+
ion_coords = []
|
|
211
|
+
instructions = []
|
|
212
|
+
|
|
213
|
+
to_center = False
|
|
214
|
+
|
|
215
|
+
if ion_pos == "left":
|
|
216
|
+
volume[2] /= 2
|
|
217
|
+
elif ion_pos == "center":
|
|
218
|
+
if not isinstance(nions, int) and len(nions) != 1:
|
|
219
|
+
raise ValueError("Center positioning can only be used with a single ion")
|
|
220
|
+
to_center = True
|
|
221
|
+
elif ion_pos == "box":
|
|
222
|
+
for cc, ion in enumerate(ions):
|
|
223
|
+
nrep = nions if isinstance(nions, int) else nions[cc]
|
|
224
|
+
instructions.append((ion.to_universe(), nrep, "box"))
|
|
225
|
+
return instructions
|
|
226
|
+
|
|
227
|
+
for cc, ion in enumerate(ions):
|
|
228
|
+
ion_radius = ion.estimate_sphere_radius()
|
|
229
|
+
nrep = nions[cc] if isinstance(nions, list) else nions
|
|
230
|
+
|
|
231
|
+
for _ in range(nrep):
|
|
232
|
+
max_attempts = 100 # Limit placement attempts to avoid infinite loop
|
|
233
|
+
for _ in range(max_attempts):
|
|
234
|
+
if to_center:
|
|
235
|
+
new_coord = volume / 2
|
|
236
|
+
else:
|
|
237
|
+
new_coord = ion_radius + 1 + np.random.rand(3) * (volume - 2 * (ion_radius + 1))
|
|
238
|
+
|
|
239
|
+
if ion_coords:
|
|
240
|
+
distances = np.linalg.norm(ion_coords - new_coord, axis=1)
|
|
241
|
+
if np.all(distances > 3):
|
|
242
|
+
break
|
|
243
|
+
else:
|
|
244
|
+
break
|
|
245
|
+
else:
|
|
246
|
+
print(f"Warning: Failed to place ion {ion} after {max_attempts} attempts")
|
|
247
|
+
|
|
248
|
+
ion_coords.append(new_coord)
|
|
249
|
+
instructions.append((ion.to_universe(), new_coord, "fixed"))
|
|
250
|
+
|
|
251
|
+
return instructions
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def make_solvent_box(self, solvent, ions, volume, density, nions, concentration,
|
|
255
|
+
conmodel, ion_pos):
|
|
256
|
+
|
|
257
|
+
# make sure info is sound
|
|
258
|
+
assert not( nions is not None and concentration is not None),\
|
|
259
|
+
"'nions' and 'concentration' cannot both be not None"
|
|
260
|
+
|
|
261
|
+
# convert concentration to number of ions
|
|
262
|
+
if concentration is not None:
|
|
263
|
+
nions = int(concentration*np.prod(volume)*units.mol/((units.m/10)**3))
|
|
264
|
+
|
|
265
|
+
# define instructions for packmol
|
|
266
|
+
instructions = []
|
|
267
|
+
|
|
268
|
+
# populate according to continuum model
|
|
269
|
+
if conmodel is not None:
|
|
270
|
+
z_positions = conmodel.discretize_profile(volume)
|
|
271
|
+
|
|
272
|
+
for cc, ion in enumerate(ions):
|
|
273
|
+
for z_pos in z_positions[cc]:
|
|
274
|
+
|
|
275
|
+
# radius = conmodel.species[cc].radius
|
|
276
|
+
instructions.append([ion, z_pos, "fixed"])
|
|
277
|
+
|
|
278
|
+
# populate according to fixed number of ions
|
|
279
|
+
elif nions is not None and ions is not None:
|
|
280
|
+
|
|
281
|
+
ion_instr = self.populate_with_ions(ions, nions, volume, ion_pos=ion_pos)
|
|
282
|
+
instructions.extend(ion_instr)
|
|
283
|
+
|
|
284
|
+
# add solvent
|
|
285
|
+
if solvent is not None:
|
|
286
|
+
solvent_volume = 1e-24*np.prod(volume)
|
|
287
|
+
mass = solvent.atoms.masses.sum()
|
|
288
|
+
|
|
289
|
+
# number of solvent molecules
|
|
290
|
+
nummols = int(units.mol*density*(1.0/mass)*solvent_volume)
|
|
291
|
+
|
|
292
|
+
instructions.append([solvent, nummols, "box"])
|
|
293
|
+
|
|
294
|
+
# generate universe file
|
|
295
|
+
universe = self.populate_box(volume, instructions)
|
|
296
|
+
|
|
297
|
+
if universe is None:
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
# Create a dictionary for quick lookup of species by residue name
|
|
301
|
+
species_dict = {specie.residues.resnames[0]: specie for specie in self.species}
|
|
302
|
+
|
|
303
|
+
alist = []
|
|
304
|
+
for res in universe.residues:
|
|
305
|
+
resname = res.resname
|
|
306
|
+
if resname in species_dict:
|
|
307
|
+
nmol = species_dict[resname].copy()
|
|
308
|
+
nmol.atoms.positions = res.atoms.positions
|
|
309
|
+
alist.append(nmol.atoms)
|
|
310
|
+
|
|
311
|
+
solution = mda.Merge(*alist)
|
|
312
|
+
solution.dimensions = volume + [90,90,90]
|
|
313
|
+
|
|
314
|
+
return solution
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def _get_size_from_slab(slab):
|
|
318
|
+
|
|
319
|
+
xsize = [1,0,0]@slab.atoms.cell@[1,0,0]
|
|
320
|
+
ysize = [0,1,0]@slab.atoms.cell@[0,1,0]
|
|
321
|
+
slab_depth = [0,0,1]@slab.atoms.cell@[0,0,1]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
return xsize, ysize, slab_depth
|
|
325
|
+
|
|
326
|
+
def make_simulation_box(self, solvent_vol, solvent_rho, nions=None,
|
|
327
|
+
concentration=None, conmodel=None, layers=1,
|
|
328
|
+
padding=1.5, to_ase=False, mirror=False,
|
|
329
|
+
write_data=False, filename="data.lammps",
|
|
330
|
+
center_electrode=False, vacuum=None, layered=False,
|
|
331
|
+
ion_pos=None, hijack=None):
|
|
332
|
+
|
|
333
|
+
# solvent volume
|
|
334
|
+
xsize, ysize, zsize = solvent_vol
|
|
335
|
+
|
|
336
|
+
# make slab
|
|
337
|
+
islab = self.make_interface_slab(self._interface, xsize, ysize, layers=layers)
|
|
338
|
+
eslab = self.make_interface_slab(self._enderface, xsize, ysize, layers=layers)
|
|
339
|
+
|
|
340
|
+
xi, yi, sdi, xe, ye, sde = 0, 0, 0, 0, 0, 0
|
|
341
|
+
# update the volume with multiples of UC
|
|
342
|
+
if islab is not None:
|
|
343
|
+
xi, yi, sdi = self._get_size_from_slab(islab)
|
|
344
|
+
islab = islab.to_universe(layered=layered)
|
|
345
|
+
if eslab is not None:
|
|
346
|
+
xe, ye, sde = self._get_size_from_slab(eslab)
|
|
347
|
+
eslab = eslab.to_universe(layered=layered)
|
|
348
|
+
|
|
349
|
+
if eslab is not None and islab is not None: # check they have same size
|
|
350
|
+
assert xi == xe
|
|
351
|
+
assert yi == ye
|
|
352
|
+
else:
|
|
353
|
+
# slab_depth = 0
|
|
354
|
+
padding = 0
|
|
355
|
+
|
|
356
|
+
if eslab is not None or islab is not None:
|
|
357
|
+
xsize = np.maximum(xi, xe)
|
|
358
|
+
ysize = np.maximum(yi, ye)
|
|
359
|
+
|
|
360
|
+
# make solvent box
|
|
361
|
+
solvent = self.make_solvent_box(self.solvent, self._solute,
|
|
362
|
+
[xsize, ysize, zsize], solvent_rho,
|
|
363
|
+
nions, concentration, conmodel, ion_pos)
|
|
364
|
+
|
|
365
|
+
# now build system
|
|
366
|
+
system = None
|
|
367
|
+
zdim = 0
|
|
368
|
+
|
|
369
|
+
if islab is not None:
|
|
370
|
+
system = islab
|
|
371
|
+
zdim += sdi
|
|
372
|
+
|
|
373
|
+
if solvent is not None:
|
|
374
|
+
if system is None:
|
|
375
|
+
system = solvent
|
|
376
|
+
zdim += zsize
|
|
377
|
+
else:
|
|
378
|
+
solvent.atoms.translate([0, 0, sdi + padding])
|
|
379
|
+
system = mda.Merge(system.atoms, solvent.atoms)
|
|
380
|
+
zdim += zsize + padding
|
|
381
|
+
|
|
382
|
+
if eslab is not None:
|
|
383
|
+
if system is None:
|
|
384
|
+
system = eslab
|
|
385
|
+
zdim += sde
|
|
386
|
+
else:
|
|
387
|
+
if solvent is None:
|
|
388
|
+
eslab.atoms.translate([0, 0, zdim])
|
|
389
|
+
system = mda.Merge(system.atoms, eslab.atoms)
|
|
390
|
+
zdim += sde
|
|
391
|
+
else:
|
|
392
|
+
eslab.atoms.translate([0, 0, zdim + padding])
|
|
393
|
+
system = mda.Merge(system.atoms, eslab.atoms)
|
|
394
|
+
zdim += sde + padding
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
system.dimensions = [xsize, ysize, zdim] + [90, 90, 90] #TODO not like this
|
|
398
|
+
|
|
399
|
+
if vacuum is not None:
|
|
400
|
+
system.dimensions[2] += vacuum
|
|
401
|
+
system.atoms.translate([0,0,+vacuum/2])
|
|
402
|
+
zdim += vacuum
|
|
403
|
+
|
|
404
|
+
if center_electrode:
|
|
405
|
+
system.atoms.translate([0,0,zdim/2])
|
|
406
|
+
_ = system.atoms.wrap()
|
|
407
|
+
|
|
408
|
+
# give ase atoms to override positions
|
|
409
|
+
if hijack is not None:
|
|
410
|
+
system.dimensions = hijack.get_cell_lengths_and_angles()
|
|
411
|
+
system.atoms.positions = hijack.get_positions()
|
|
412
|
+
|
|
413
|
+
if write_data:
|
|
414
|
+
self.write_lammps_file(system, filename=filename)
|
|
415
|
+
|
|
416
|
+
if to_ase:
|
|
417
|
+
return self.to_ase(system)
|
|
418
|
+
|
|
419
|
+
return system
|
|
420
|
+
|
|
421
|
+
def write_lammps_file(self, system, write_coeff=True, filename="data.lammps"):
|
|
422
|
+
|
|
423
|
+
# first write data file
|
|
424
|
+
with DATAWriter(filename) as dt:
|
|
425
|
+
dt.write(system.atoms)
|
|
426
|
+
|
|
427
|
+
# now write coeff where they belong
|
|
428
|
+
if write_coeff:
|
|
429
|
+
|
|
430
|
+
temp_file = 'tmp_data.lammps'
|
|
431
|
+
|
|
432
|
+
with open(filename, 'r') as ffile, open(temp_file, 'w') as tfile:
|
|
433
|
+
for ln, fl in enumerate(ffile):
|
|
434
|
+
if fl.startswith("Atoms"):
|
|
435
|
+
|
|
436
|
+
# write coefficients
|
|
437
|
+
self.write_coefficients(system, fout=tfile)
|
|
438
|
+
|
|
439
|
+
tfile.write(fl)
|
|
440
|
+
|
|
441
|
+
shutil.move(temp_file, filename)
|
|
442
|
+
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
def write_coefficients(self, system, fname="tmp.coeff", fout=None):
|
|
446
|
+
|
|
447
|
+
remember_to_close = False
|
|
448
|
+
if fout is None:
|
|
449
|
+
fout = open(fname, "w")
|
|
450
|
+
remember_to_close = True
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
fout.write("Pair Coeffs\n\n")
|
|
454
|
+
|
|
455
|
+
idx = 1
|
|
456
|
+
for cc, atom in enumerate(self.get_sorted_attribute("atoms")):
|
|
457
|
+
|
|
458
|
+
if atom.extended_label not in np.unique(system.atoms.types):
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
eps = atom.eps if atom.eps is not None else 0
|
|
462
|
+
sig = atom.sig if atom.sig is not None else 0
|
|
463
|
+
|
|
464
|
+
fout.write("{:>5} {:>12.8f} {:>12.8f} # {}\n".format(
|
|
465
|
+
idx, eps, sig, atom.extended_label))
|
|
466
|
+
idx += 1
|
|
467
|
+
|
|
468
|
+
if self.get_sorted_attribute("bonds"):
|
|
469
|
+
fout.write("\n")
|
|
470
|
+
fout.write("Bond Coeffs\n\n")
|
|
471
|
+
|
|
472
|
+
for bond in self.get_sorted_attribute("bonds"):
|
|
473
|
+
|
|
474
|
+
if bond.id not in np.array(system.bonds.types(), dtype=int):
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
kr = bond.kr if bond.kr is not None else 0
|
|
478
|
+
r0 = bond.r0 if bond.r0 is not None else 0
|
|
479
|
+
|
|
480
|
+
btype = "{}-{}".format(*bond.symbols)
|
|
481
|
+
|
|
482
|
+
fout.write("{:>5} {:>10.6f} {:>10.6f} # {:<5} | {}\n".format(
|
|
483
|
+
bond.id, kr, r0, btype, bond.resname))
|
|
484
|
+
|
|
485
|
+
if self.get_sorted_attribute("angles"):
|
|
486
|
+
fout.write("\n")
|
|
487
|
+
fout.write("Angle Coeffs\n\n")
|
|
488
|
+
|
|
489
|
+
for angle in self.get_sorted_attribute("angles"):
|
|
490
|
+
|
|
491
|
+
if angle.id not in np.array(system.angles.types(), dtype=int):
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
kr = angle.kr if angle.kr is not None else 0
|
|
495
|
+
theta0 = angle.theta0 if angle.theta0 is not None else 0
|
|
496
|
+
|
|
497
|
+
atype = "{}-{}-{}".format(*angle.symbols)
|
|
498
|
+
|
|
499
|
+
fout.write("{:>5} {:>10.6f} {:>10.6f} # {:<8} | {}\n".format(
|
|
500
|
+
angle.id, kr, theta0, atype, angle.resname))
|
|
501
|
+
|
|
502
|
+
if self.get_sorted_attribute("dihedrals"):
|
|
503
|
+
fout.write("\n")
|
|
504
|
+
fout.write("Dihedral Coeffs\n\n")
|
|
505
|
+
|
|
506
|
+
for dihedral in self.get_sorted_attribute("dihedrals"):
|
|
507
|
+
|
|
508
|
+
if dihedral.id not in np.array(system.dihedrals.types(), dtype=int):
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
dihedral.write(fout)
|
|
512
|
+
# atype = "{}-{}-{}-{}".format(*dihedral.symbols)
|
|
513
|
+
# value = "{:>7.4f} {:>7.4f} {:>7.4f} {:>7.4f} {:>7.4f}".format(*dihedral.values)
|
|
514
|
+
|
|
515
|
+
# fout.write("{:>5} {} # {:<8} | {}\n".format(dihedral.id, value, atype, dihedral.resname))
|
|
516
|
+
|
|
517
|
+
if self.get_sorted_attribute("impropers"):
|
|
518
|
+
fout.write("\n")
|
|
519
|
+
fout.write("Improper Coeffs\n\n")
|
|
520
|
+
|
|
521
|
+
for improper in self.get_sorted_attribute("impropers"):
|
|
522
|
+
|
|
523
|
+
if improper.id not in np.array(system.impropers.types(), dtype=int):
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
atype = "{}".format(*improper.symbols)
|
|
527
|
+
value = "{:>7.4f} {:>2d} {:>2d}".format(*improper.values)
|
|
528
|
+
|
|
529
|
+
fout.write("{:>5} {} # {:<2} | {}\n".format(improper.id, value, atype, improper.resname))
|
|
530
|
+
|
|
531
|
+
fout.write("\n")
|
|
532
|
+
|
|
533
|
+
if remember_to_close:
|
|
534
|
+
fout.close()
|
|
535
|
+
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
# convert to ase.Atoms
|
|
539
|
+
@staticmethod
|
|
540
|
+
def to_ase(system):
|
|
541
|
+
|
|
542
|
+
if system is None:
|
|
543
|
+
return ase.Atoms()
|
|
544
|
+
|
|
545
|
+
positions = system.atoms.positions
|
|
546
|
+
|
|
547
|
+
masses = system.atoms.masses
|
|
548
|
+
labels = system.atoms.types
|
|
549
|
+
|
|
550
|
+
symbols = [label_to_element(lab, mas) for lab, mas in zip(labels, masses)]
|
|
551
|
+
|
|
552
|
+
ase_system = ase.Atoms(symbols=symbols, positions=positions)
|
|
553
|
+
|
|
554
|
+
if system.dimensions is not None:
|
|
555
|
+
ase_system.set_cell(system.dimensions)
|
|
556
|
+
ase_system.set_pbc(True)
|
|
557
|
+
|
|
558
|
+
if system.atoms.charges is not None:
|
|
559
|
+
ase_system.set_initial_charges(system.atoms.charges)
|
|
560
|
+
|
|
561
|
+
return ase_system
|
|
562
|
+
|
|
563
|
+
@property
|
|
564
|
+
def solvent(self):
|
|
565
|
+
if self._solvent is None:
|
|
566
|
+
return None
|
|
567
|
+
return self._solvent.to_universe()
|
|
568
|
+
|
|
569
|
+
@property
|
|
570
|
+
def solute(self):
|
|
571
|
+
if self._solute is None:
|
|
572
|
+
return None
|
|
573
|
+
return [ii.to_universe() for ii in self._solute]
|
|
574
|
+
|
|
575
|
+
@property
|
|
576
|
+
def interface(self):
|
|
577
|
+
if self._interface is None:
|
|
578
|
+
return None
|
|
579
|
+
return self._interface.to_universe()
|
|
580
|
+
|
|
581
|
+
@property
|
|
582
|
+
def enderface(self):
|
|
583
|
+
if self._enderface is None:
|
|
584
|
+
return None
|
|
585
|
+
return self._enderface.to_universe()
|
|
586
|
+
|
|
587
|
+
@property
|
|
588
|
+
def species(self):
|
|
589
|
+
# merge all species in system
|
|
590
|
+
all_species = np.concatenate((as_list(self.solvent), as_list(self.solute),
|
|
591
|
+
as_list(self.interface), as_list(self.enderface)))
|
|
592
|
+
return [ii for ii in all_species if ii is not None]
|
|
593
|
+
|
|
594
|
+
@property
|
|
595
|
+
def _species(self):
|
|
596
|
+
|
|
597
|
+
return np.concatenate((as_list(self._solvent), as_list(self._solute),
|
|
598
|
+
as_list(self._interface), as_list(self._enderface)))
|
|
599
|
+
|
|
600
|
+
def get_sorted_attribute(self, attribute):
|
|
601
|
+
|
|
602
|
+
if attribute.lower() in "bonds":
|
|
603
|
+
attribute = "_btype"
|
|
604
|
+
elif attribute.lower() in "angles":
|
|
605
|
+
attribute = "_atype"
|
|
606
|
+
elif attribute.lower() in "dihedrals":
|
|
607
|
+
attribute = "_dtype"
|
|
608
|
+
elif attribute.lower() in "impropers":
|
|
609
|
+
attribute = "_itype"
|
|
610
|
+
elif attribute.lower() in "atoms":
|
|
611
|
+
attribute = "_stype"
|
|
612
|
+
|
|
613
|
+
indexes = []
|
|
614
|
+
attributes = []
|
|
615
|
+
for specie in self._species:
|
|
616
|
+
for attr in specie.__getattribute__(attribute):
|
|
617
|
+
indexes.append(attr.id)
|
|
618
|
+
attributes.append(attr)
|
|
619
|
+
|
|
620
|
+
return [attributes[ii] for ii in np.argsort(indexes)]
|
|
621
|
+
|