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.
@@ -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
+
@@ -0,0 +1,8 @@
1
+ # utils/__init__.py
2
+
3
+ """
4
+ utils: Utility functions and helpers.
5
+ """
6
+
7
+ from .auxiliary import *
8
+ from .database import *