mdinterface 1.0.0__tar.gz → 1.1.0__tar.gz

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.
Files changed (31) hide show
  1. {mdinterface-1.0.0/mdinterface.egg-info → mdinterface-1.1.0}/PKG-INFO +15 -4
  2. {mdinterface-1.0.0 → mdinterface-1.1.0}/README.md +13 -3
  3. mdinterface-1.1.0/mdinterface/__init__.py +15 -0
  4. mdinterface-1.1.0/mdinterface/build/__init__.py +3 -0
  5. mdinterface-1.1.0/mdinterface/build/box.py +230 -0
  6. mdinterface-1.1.0/mdinterface/build/continuum2sim.py +45 -0
  7. mdinterface-1.1.0/mdinterface/build/polymer.py +85 -0
  8. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/core/specie.py +18 -23
  9. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/io/packmol.py +1 -1
  10. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/simulationbox.py +8 -222
  11. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/utils/__init__.py +1 -0
  12. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/utils/database.py +37 -1
  13. {mdinterface-1.0.0 → mdinterface-1.1.0/mdinterface.egg-info}/PKG-INFO +15 -4
  14. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface.egg-info/SOURCES.txt +4 -0
  15. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface.egg-info/requires.txt +1 -0
  16. {mdinterface-1.0.0 → mdinterface-1.1.0}/requirements.txt +1 -0
  17. mdinterface-1.0.0/mdinterface/__init__.py +0 -10
  18. {mdinterface-1.0.0 → mdinterface-1.1.0}/LICENSE +0 -0
  19. {mdinterface-1.0.0 → mdinterface-1.1.0}/MANIFEST.in +0 -0
  20. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/core/__init__.py +0 -0
  21. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/core/topology.py +0 -0
  22. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/io/__init__.py +0 -0
  23. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/io/lammpswriter.py +0 -0
  24. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/io/read.py +0 -0
  25. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/utils/auxiliary.py +0 -0
  26. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface/utils/map.py +0 -0
  27. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface.egg-info/dependency_links.txt +0 -0
  28. {mdinterface-1.0.0 → mdinterface-1.1.0}/mdinterface.egg-info/top_level.txt +0 -0
  29. {mdinterface-1.0.0 → mdinterface-1.1.0}/pyproject.toml +0 -0
  30. {mdinterface-1.0.0 → mdinterface-1.1.0}/setup.cfg +0 -0
  31. {mdinterface-1.0.0 → mdinterface-1.1.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mdinterface
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: mdinterface: A package for building interface systems in Molecular Dynamics simulations.
5
5
  Home-page: https://gitlab.com/roncofaber/mdinterface
6
6
  License: Apache-2.0
@@ -16,10 +16,11 @@ Requires-Dist: mdanalysis
16
16
  Requires-Dist: ase
17
17
  Requires-Dist: numpy
18
18
  Requires-Dist: networkx
19
+ Requires-Dist: libarvo
19
20
 
20
- # mdinterface: build interface systems for Molecular Dynamics simulations
21
+ # mdinterface: Build Interface Systems for Molecular Dynamics Simulations
21
22
 
22
- `mdinterface` is a Python package for building systems to use in Molecular Dynamics (MD) simulations. The package was primarly developed to help construct electrolyte/electrode interfaces, but is well suited to generate suitable MD boxes of liquids, electrolyte systems and polymer networks.
23
+ `mdinterface` is a Python package designed to build systems for Molecular Dynamics (MD) simulations. Initially developed to construct electrolyte/electrode interfaces, it is also well-suited for generating MD boxes of liquids, electrolyte systems, and polymer networks.
23
24
 
24
25
  ## Features
25
26
 
@@ -34,6 +35,12 @@ Requires-Dist: networkx
34
35
 
35
36
  Check the file [requirements.txt](requirements.txt) to see which packages are needed. Installing the package using `pip` should already take care of all dependencies.
36
37
 
38
+ Additionaly, you need the `packmol` utility installed to generate MD boxes. You can follow the instructions at [https://m3g.github.io/packmol/](https://m3g.github.io/packmol/) to install it. Alternatively, you can install it using `conda`:
39
+
40
+ ```bash
41
+ conda install -c conda-forge packmol
42
+ ```
43
+
37
44
  ## Installation
38
45
 
39
46
  ### Install using `pip`
@@ -46,7 +53,7 @@ pip install mdinterface
46
53
 
47
54
  ### Install directly the source code
48
55
 
49
- Alternatively you can obtain `mdinterface` directly from the repository by following those steps:
56
+ Alternatively you can obtain `mdinterface` directly from the repository by following these steps:
50
57
 
51
58
  Clone the repository in the desired location:
52
59
 
@@ -74,3 +81,7 @@ If you plan of making changes, clone the package and install the requirements bu
74
81
  ```bash
75
82
  pip install --no-build-isolation --no-deps -e .
76
83
  ```
84
+
85
+ ## Usage
86
+
87
+ Check the files in [examples](mdinterface/examples/) to learn how to use the package.
@@ -1,6 +1,6 @@
1
- # mdinterface: build interface systems for Molecular Dynamics simulations
1
+ # mdinterface: Build Interface Systems for Molecular Dynamics Simulations
2
2
 
3
- `mdinterface` is a Python package for building systems to use in Molecular Dynamics (MD) simulations. The package was primarly developed to help construct electrolyte/electrode interfaces, but is well suited to generate suitable MD boxes of liquids, electrolyte systems and polymer networks.
3
+ `mdinterface` is a Python package designed to build systems for Molecular Dynamics (MD) simulations. Initially developed to construct electrolyte/electrode interfaces, it is also well-suited for generating MD boxes of liquids, electrolyte systems, and polymer networks.
4
4
 
5
5
  ## Features
6
6
 
@@ -15,6 +15,12 @@
15
15
 
16
16
  Check the file [requirements.txt](requirements.txt) to see which packages are needed. Installing the package using `pip` should already take care of all dependencies.
17
17
 
18
+ Additionaly, you need the `packmol` utility installed to generate MD boxes. You can follow the instructions at [https://m3g.github.io/packmol/](https://m3g.github.io/packmol/) to install it. Alternatively, you can install it using `conda`:
19
+
20
+ ```bash
21
+ conda install -c conda-forge packmol
22
+ ```
23
+
18
24
  ## Installation
19
25
 
20
26
  ### Install using `pip`
@@ -27,7 +33,7 @@ pip install mdinterface
27
33
 
28
34
  ### Install directly the source code
29
35
 
30
- Alternatively you can obtain `mdinterface` directly from the repository by following those steps:
36
+ Alternatively you can obtain `mdinterface` directly from the repository by following these steps:
31
37
 
32
38
  Clone the repository in the desired location:
33
39
 
@@ -55,3 +61,7 @@ If you plan of making changes, clone the package and install the requirements bu
55
61
  ```bash
56
62
  pip install --no-build-isolation --no-deps -e .
57
63
  ```
64
+
65
+ ## Usage
66
+
67
+ Check the files in [examples](mdinterface/examples/) to learn how to use the package.
@@ -0,0 +1,15 @@
1
+ """
2
+ mdinterface: Build Interface Systems for Molecular Dynamics Simulations
3
+
4
+ `mdinterface` is a Python package designed to build systems for Molecular Dynamics (MD) simulations.
5
+ Initially developed to construct electrolyte/electrode interfaces, it is also well-suited for generating MD boxes of liquids,
6
+ electrolyte systems, and polymer networks.
7
+
8
+ """
9
+
10
+ __version__ = '1.1.0'
11
+ __date__ = '14 Jan. 2025'
12
+ __author__ = 'Fabrice Roncoroni'
13
+ __all__ = ['SimulationBox']
14
+
15
+ from .simulationbox import SimulationBox
@@ -0,0 +1,3 @@
1
+ from .polymer import *
2
+ from .box import *
3
+ from .continuum2sim import *
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Tue Jan 14 10:23:57 2025
5
+
6
+ @author: roncofaber
7
+ """
8
+
9
+ from mdinterface.io.packmol import header, box_place, fix_place
10
+ from mdinterface.build.continuum2sim import discretize_concentration
11
+
12
+ import MDAnalysis as mda
13
+ from ase import units
14
+
15
+ import numpy as np
16
+
17
+ import subprocess
18
+
19
+ #%%
20
+
21
+ def make_solvent_box(species, solvent, ions, volume, density, nions, concentration,
22
+ conmodel, ion_pos):
23
+
24
+ # make sure info is sound
25
+ assert not( nions is not None and concentration is not None),\
26
+ "'nions' and 'concentration' cannot both be not None"
27
+
28
+ # convert concentration to number of ions
29
+ if concentration is not None:
30
+ nions = int(concentration*np.prod(volume)*units.mol/((units.m/10)**3))
31
+
32
+ # define instructions for packmol
33
+ instructions = []
34
+
35
+ # populate ions
36
+ if (conmodel is not None) or (nions is not None and ions is not None):
37
+ ion_instr = populate_with_ions(ions, nions, volume, ion_pos=ion_pos,
38
+ conmodel=conmodel)
39
+ instructions.extend(ion_instr)
40
+
41
+ # add solvent
42
+ if solvent is not None:
43
+ solvent_volume = 1e-24*np.prod(volume)
44
+ mass = solvent.atoms.masses.sum()
45
+
46
+ # number of solvent molecules
47
+ nummols = int(units.mol*density*(1.0/mass)*solvent_volume)
48
+
49
+ instructions.append([solvent, nummols, "box"])
50
+
51
+ # generate universe file
52
+ universe = populate_box(volume, instructions)
53
+
54
+ if universe is None:
55
+ return None
56
+
57
+ # Create a dictionary for quick lookup of species by residue name
58
+ species_dict = {specie.residues.resnames[0]: specie for specie in species}
59
+
60
+ alist = []
61
+ for res in universe.residues:
62
+ resname = res.resname
63
+ if resname in species_dict:
64
+ nmol = species_dict[resname].copy()
65
+ nmol.atoms.positions = res.atoms.positions
66
+ alist.append(nmol.atoms)
67
+
68
+ solution = mda.Merge(*alist)
69
+ solution.dimensions = volume + [90,90,90]
70
+
71
+ return solution
72
+
73
+ # populate a box with solvent and ions
74
+ def populate_box(volume, instructions, input_file="input_packmol.in",
75
+ output_file="system.pdb"):
76
+
77
+ if not instructions:
78
+ return None
79
+
80
+ # check volume
81
+ assert len(volume) == 3, "Check volume!"
82
+
83
+ # generate box boundaries with 1 AA padding
84
+ box = np.concatenate(([1,1,1], np.asarray(volume)-1)).tolist()
85
+
86
+ tmp_files = ["packmol.log", "input_packmol.in", "system.pdb"]
87
+ with open(input_file, "w") as fout:
88
+
89
+ fout.write(header.format(output_file, np.random.randint(100000)))
90
+
91
+ for cc, instruction in enumerate(instructions):
92
+
93
+ # unpack instructions
94
+ mol = instruction[0]
95
+ rep = instruction[1]
96
+ typ = instruction[2]
97
+
98
+ if isinstance(rep, int):
99
+ if not rep:
100
+ continue
101
+
102
+ if typ == "box": # normal add
103
+ fout.write(box_place.format(cc, rep, " ".join(map(str, box))))
104
+
105
+ elif typ == "fixed": # coordinate -> fixed point
106
+ fout.write(fix_place.format(cc, *rep))
107
+
108
+ elif typ == "zfixed": # small bin to use in CM
109
+ tbox = box.copy()
110
+ tbox[2] = tbox[2] - mol.estimate_specie_radius()
111
+ tbox[5] = tbox[5] + mol.estimate_specie_radius()
112
+ fout.write(box_place.format(cc, 1, " ".join(map(str, tbox))))
113
+ mol = mol.to_universe()
114
+
115
+ else:
116
+ raise "Wrong instructions"
117
+
118
+ # write tmp pdb file and store info
119
+ mol.atoms.write("mol_{}.pdb".format(cc))
120
+ tmp_files.append("mol_{}.pdb".format(cc))
121
+
122
+ # run packmol
123
+ try:
124
+ subprocess.run(['packmol < {} > packmol.log'.format(input_file)],
125
+ shell=True, check=True, text=True)
126
+
127
+ except:
128
+ print("WARNING: packmol might not have worked, check system.")
129
+
130
+ try:
131
+ universe = mda.Universe(output_file)
132
+ except:
133
+ universe = None
134
+
135
+ # remove temp mol files and packmol files
136
+ subprocess.call(['rm'] + tmp_files)
137
+
138
+ return universe
139
+
140
+ # generate a slab from a unit cell
141
+ def make_interface_slab(interface_uc, xsize, ysize, layers=1):
142
+
143
+ if layers == 0 or interface_uc is None:
144
+ return None
145
+
146
+ xrep = int(np.round(xsize/interface_uc.atoms.get_cell()[0][0]))
147
+ yrep = int(np.round(ysize/interface_uc.atoms.get_cell()[1][1]))
148
+
149
+ slab = interface_uc.copy()
150
+
151
+ if not np.isclose(np.dot(slab.atoms.cell[0], [1,0,0]), slab.atoms.cell[0][0]):
152
+ xrep +=1
153
+ print("WARNING: check interface if pattern matches")
154
+
155
+ if not np.isclose(np.dot(slab.atoms.cell[1], [0,1,0]), slab.atoms.cell[1][1]):
156
+ yrep +=1
157
+ print("WARNING: check interface if pattern matches")
158
+
159
+ slab.repeat((xrep, yrep, 1), make_cubic=True)
160
+
161
+ if layers > 1: # helps with indexing
162
+ slab.repeat([1,1,layers])
163
+
164
+ slab.atoms.center()
165
+ # slab.atoms.rattle()
166
+
167
+ return slab
168
+
169
+ #THANKS CHATGPT (but mostly me tbh)
170
+ def populate_with_ions(ions, nions, volume, ion_pos=False, conmodel=None):
171
+ def place_ion(ion, volume, ion_coords, ion_radii, zpos=None, max_attempts=100):
172
+ ion_radius = ion.estimate_specie_radius()
173
+ for _ in range(max_attempts):
174
+ new_coord = ion_radius + 1 + np.random.rand(3) * (volume - 2 * (ion_radius + 1))
175
+ if zpos is not None:
176
+ new_coord[2] = zpos
177
+
178
+ if not ion_coords or np.all(np.linalg.norm(ion_coords - new_coord, axis=1) >= np.array(ion_radii) + ion_radius + 1):
179
+ return new_coord
180
+ print(f"Warning: Failed to place ion {ion} after {max_attempts} attempts")
181
+ return None
182
+
183
+ def place_ions_conmodel(ions, conmodel, volume, max_attempts=100):
184
+ instructions = []
185
+ ion_coords = []
186
+ ion_radii = []
187
+ for cc, ion in enumerate(ions):
188
+ z_coords, conc_profile = conmodel[cc]
189
+ z_positions = discretize_concentration(ion, conc_profile, z_coords, volume)
190
+ for zpos in z_positions:
191
+ new_coord = place_ion(ion, volume, ion_coords, ion_radii, zpos=zpos,
192
+ max_attempts=max_attempts)
193
+ if new_coord is not None:
194
+ ion_coords.append(new_coord)
195
+ ion_radii.append(ion.estimate_specie_radius())
196
+ instructions.append((ion.to_universe(), new_coord, "fixed"))
197
+ return instructions
198
+
199
+ def place_ions_random(ions, nions, volume, to_center, max_attempts=100):
200
+ instructions = []
201
+ ion_coords = []
202
+ ion_radii = []
203
+ for cc, ion in enumerate(ions):
204
+ nrep = nions if isinstance(nions, int) else nions[cc]
205
+ for _ in range(nrep):
206
+ new_coord = volume / 2 if to_center else place_ion(ion, volume,
207
+ ion_coords,
208
+ ion_radii,
209
+ max_attempts=max_attempts)
210
+ if new_coord is not None:
211
+ ion_coords.append(new_coord)
212
+ ion_radii.append(ion.estimate_specie_radius())
213
+ instructions.append((ion.to_universe(), new_coord, "fixed"))
214
+ return instructions
215
+
216
+ max_attempts = 100 # Limit placement attempts to avoid infinite loop
217
+ volume = np.array(volume)
218
+
219
+ if conmodel is not None:
220
+ assert len(conmodel) == len(ions), "Need one profile per specie"
221
+ return place_ions_conmodel(ions, conmodel, volume, max_attempts=max_attempts)
222
+
223
+ if ion_pos == "box":
224
+ return [(ion.to_universe(), nions if isinstance(nions, int) else nions[cc], "box") for cc, ion in enumerate(ions)]
225
+
226
+ to_center = ion_pos == "center"
227
+ if ion_pos == "left":
228
+ volume[2] /= 2
229
+
230
+ return place_ions_random(ions, nions, volume, to_center, max_attempts=max_attempts)
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Tue Jan 14 10:34:26 2025
5
+
6
+ @author: roncofaber
7
+ """
8
+
9
+ from ase import units
10
+ import numpy as np
11
+ from scipy import integrate
12
+
13
+ #%%
14
+ # return a discrete concentration profile given a "continuum" conc. profile
15
+ # for a specie. Return positions of the bins where ion should be.
16
+ def discretize_concentration(specie, conc_profile, z_coords, volume):
17
+
18
+ xsize, ysize, zsize = volume
19
+
20
+ conv = units.mol/1e27
21
+ area = xsize*ysize
22
+
23
+ # integrate conc. prof. over area
24
+ cumsum = integrate.cumulative_trapezoid(conc_profile, z_coords, initial=0, axis=0)
25
+ cumsum_conv = cumsum*conv*area
26
+
27
+ # generate bins (1 atom in each one) for K
28
+ Nions = 0
29
+ bins = []
30
+ for ii, con in enumerate(cumsum_conv):
31
+ if con > np.maximum(cumsum_conv.max()/100, Nions):
32
+ Nions += 1
33
+ bins.append(z_coords[ii])
34
+
35
+ # now find middle point of bins
36
+ z_pos = []
37
+ for ii, pos in enumerate(bins[:-1]):
38
+ z_pos.append((bins[ii] + bins[ii+1])/2)
39
+
40
+ # add one last ion
41
+ if cumsum_conv.max() % 1 > 0.5:
42
+ z_pos.append((z_coords.max()+bins[-1])/2)
43
+ Nions += 1
44
+
45
+ return z_pos
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Tue Oct 22 17:17:30 2024
5
+
6
+ @author: roncofaber
7
+ """
8
+
9
+ import numpy as np
10
+ import ase
11
+ import ase.build
12
+
13
+ #%%
14
+
15
+ def build_polymer(system, substitute, nrep, target_distance=1.600):
16
+ """
17
+ Build a polymer by replicating a monomer structure and replacing 'X' atoms with a substitute.
18
+
19
+ Parameters:
20
+ - system (ase.Atoms): The monomer structure as an ASE Atoms object.
21
+ - substitute (str): Chemical symbol to replace 'X' with after replication.
22
+ - nrep (int): Number of times to replicate the monomer.
23
+ - target_distance (float, optional): Desired bond distance at the joining points (in angstroms).
24
+ If None, the original distance between 'X' atoms is used.
25
+
26
+ Returns:
27
+ - sout (ase.Atoms): The replicated and modified polymer structure.
28
+ """
29
+
30
+ system = system.copy()
31
+
32
+ # Check if nrep is at least 1
33
+ if nrep < 1:
34
+ raise ValueError("nrep must be at least 1")
35
+
36
+ # Find the indices of the 'X' atoms
37
+ x_idxs = np.where(np.array(system.get_chemical_symbols()) == "X")[0]
38
+ if len(x_idxs) != 2:
39
+ raise ValueError("The monomer should contain exactly two 'X' atoms.")
40
+
41
+ # Find atoms connected to X
42
+ ini_idx = ase.build.connected_indices(system, x_idxs[0])[1]
43
+ end_idx = ase.build.connected_indices(system, x_idxs[1])[1]
44
+
45
+ bnd_vec = system.get_distance(end_idx, x_idxs[1], vector=True)
46
+ bnd_vec = target_distance * bnd_vec / np.linalg.norm(bnd_vec)
47
+
48
+ ini_pos = system.get_positions()[ini_idx]
49
+ end_pos = system.get_positions()[end_idx] + bnd_vec
50
+
51
+ # Get the vector between the two 'X' atoms
52
+ X_vec = end_pos - ini_pos
53
+
54
+ # remember connecting points
55
+ is_connected = np.array(len(system)*[False])
56
+ is_connected[ini_idx] = True
57
+ is_connected[end_idx] = True
58
+ system.new_array("is_connected", is_connected)
59
+
60
+ # Start with a copy of the system
61
+ sout = system.copy()
62
+ sout.new_array("mon_id", np.array(len(sout)*[0]))
63
+
64
+ # Replicate the monomer
65
+ if nrep > 1:
66
+ del sout[x_idxs[1]] # Remove the second 'X' atom in the first monomer
67
+
68
+ for ii in range(nrep - 1):
69
+ sadd = system.copy()
70
+
71
+ if ii < nrep - 2:
72
+ del sadd[x_idxs] # Remove both 'X' atoms in the intermediate monomers
73
+ else:
74
+ del sadd[x_idxs[0]] # Remove the first 'X' atom in the last monomer
75
+
76
+ sadd.translate((ii + 1) * X_vec)
77
+ sadd.set_array("mon_id", np.array(len(sadd)*[ii+1]))
78
+ sout += sadd
79
+
80
+ # Replace 'X' with the substitute
81
+ symbols = np.array(sout.get_chemical_symbols())
82
+ symbols[symbols == "X"] = substitute
83
+ sout.set_chemical_symbols(symbols)
84
+
85
+ return sout#ase.build.sort(sout)
@@ -9,15 +9,15 @@ Created on Fri Apr 19 13:58:43 2024
9
9
  import ase
10
10
  import ase.build
11
11
  import ase.visualize
12
+ from ase.data import vdw_radii
12
13
  from ase.data.colors import jmol_colors
13
14
  import MDAnalysis as mda
14
15
 
15
- from mdinterface.utils.auxiliary import as_list, find_smallest_missing, atoms_to_indexes
16
-
17
- from mdinterface.core.topology import Atom
16
+ import mdinterface.utils.map as pmap
18
17
  import mdinterface.utils.auxiliary as aux
18
+ from mdinterface.core.topology import Atom
19
19
  from mdinterface.io.read import read_lammps_data_file
20
- import mdinterface.utils.map as pmap
20
+ from mdinterface.utils.auxiliary import as_list, find_smallest_missing
21
21
 
22
22
  import copy
23
23
  import numpy as np
@@ -387,30 +387,25 @@ class Specie(object):
387
387
 
388
388
  return atom_types
389
389
 
390
- def estimate_sphere_radius(self):
391
- """
392
- Estimate the radius of the sphere containing the given points.
393
-
394
- Parameters:
395
- points (numpy.ndarray): A 2D array of shape (n, 3) where n is the number of points.
396
-
397
- Returns:
398
- float: The estimated radius of the sphere.
399
- """
400
-
401
- points = self.atoms.get_positions()
390
+ # method to estimate the volume of the specie
391
+ def estimate_specie_volume(self, probe_radius=0):
402
392
 
403
- # Calculate the centroid of the points
404
- centroid = np.mean(points, axis=0)
393
+ try:
394
+ from libarvo import molecular_vs
395
+ except:
396
+ print("libarvo NOT found. Install it.")
405
397
 
406
- # Calculate the distances from the centroid to each point
407
- distances = np.linalg.norm(points - centroid, axis=1)
398
+ centers = self.atoms.get_positions()
399
+ radii = [vdw_radii[ii] for ii in self.atoms.get_atomic_numbers()]
408
400
 
409
- # The radius of the sphere is the maximum distance from the centroid to any point
410
- radius = np.max(distances)
401
+ volume, surface = molecular_vs(centers, radii, probe_radius)
411
402
 
412
- return radius
403
+ return volume
413
404
 
405
+ # method to estimate the radius of the specie if it were a sphere
406
+ def estimate_specie_radius(self, probe_radius=0):
407
+ volume = self.estimate_specie_volume(probe_radius=probe_radius)
408
+ return (3 * volume / (4 * np.pi)) ** (1/3)
414
409
 
415
410
  def view(self):
416
411
  ase.visualize.view(self.atoms)
@@ -14,7 +14,7 @@ nloop 100
14
14
  seed {}
15
15
 
16
16
  filetype pdb
17
- # movebadrandom
17
+ movebadrandom
18
18
  """
19
19
 
20
20
  box_place = """
@@ -8,15 +8,13 @@ Created on Tue Oct 24 15:14:41 2023
8
8
 
9
9
  from mdinterface.utils.auxiliary import label_to_element, as_list, find_smallest_missing
10
10
  from mdinterface.io.lammpswriter import DATAWriter
11
- from mdinterface.io.packmol import header, box_place, fix_place
11
+ from mdinterface.build.box import make_interface_slab, make_solvent_box
12
12
 
13
+ import ase
13
14
  import MDAnalysis as mda
14
15
 
15
- import ase
16
- from ase import units
17
16
  import numpy as np
18
17
 
19
- import subprocess
20
18
  import shutil
21
19
 
22
20
  import warnings
@@ -40,7 +38,6 @@ class SimulationBox():
40
38
 
41
39
  return
42
40
 
43
-
44
41
  def _setup_species(self, solvent, solute, interface, enderface):
45
42
 
46
43
  self._solvent = None
@@ -72,7 +69,6 @@ class SimulationBox():
72
69
 
73
70
  return
74
71
 
75
-
76
72
  def _update_topology_indexes(self):
77
73
 
78
74
  nitems = {
@@ -105,214 +101,6 @@ class SimulationBox():
105
101
 
106
102
  return
107
103
 
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
104
  @staticmethod
317
105
  def _get_size_from_slab(slab):
318
106
 
@@ -320,9 +108,9 @@ class SimulationBox():
320
108
  ysize = [0,1,0]@slab.atoms.cell@[0,1,0]
321
109
  slab_depth = [0,0,1]@slab.atoms.cell@[0,0,1]
322
110
 
323
-
324
111
  return xsize, ysize, slab_depth
325
112
 
113
+ # main driver to generate a simulation box given instructions
326
114
  def make_simulation_box(self, solvent_vol, solvent_rho, nions=None,
327
115
  concentration=None, conmodel=None, layers=1,
328
116
  padding=1.5, to_ase=False, mirror=False,
@@ -334,8 +122,8 @@ class SimulationBox():
334
122
  xsize, ysize, zsize = solvent_vol
335
123
 
336
124
  # 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)
125
+ islab = make_interface_slab(self._interface, xsize, ysize, layers=layers)
126
+ eslab = make_interface_slab(self._enderface, xsize, ysize, layers=layers)
339
127
 
340
128
  xi, yi, sdi, xe, ye, sde = 0, 0, 0, 0, 0, 0
341
129
  # update the volume with multiples of UC
@@ -358,9 +146,9 @@ class SimulationBox():
358
146
  ysize = np.maximum(yi, ye)
359
147
 
360
148
  # 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)
149
+ solvent = make_solvent_box(self.species, self.solvent, self._solute,
150
+ [xsize, ysize, zsize], solvent_rho,
151
+ nions, concentration, conmodel, ion_pos)
364
152
 
365
153
  # now build system
366
154
  system = None
@@ -393,7 +181,6 @@ class SimulationBox():
393
181
  system = mda.Merge(system.atoms, eslab.atoms)
394
182
  zdim += sde + padding
395
183
 
396
-
397
184
  system.dimensions = [xsize, ysize, zdim] + [90, 90, 90] #TODO not like this
398
185
 
399
186
  if vacuum is not None:
@@ -593,7 +380,6 @@ class SimulationBox():
593
380
 
594
381
  @property
595
382
  def _species(self):
596
-
597
383
  return np.concatenate((as_list(self._solvent), as_list(self._solute),
598
384
  as_list(self._interface), as_list(self._enderface)))
599
385
 
@@ -6,3 +6,4 @@ utils: Utility functions and helpers.
6
6
 
7
7
  from .auxiliary import *
8
8
  from .database import *
9
+ from .map import *
@@ -13,7 +13,7 @@ from mdinterface.core.specie import Specie
13
13
  from mdinterface.core.topology import Bond, Angle, Atom, Dihedral, Improper
14
14
 
15
15
  import ase.build
16
-
16
+ from ase.build import fcc111
17
17
  #%%
18
18
 
19
19
  # graphene https://onlinelibrary.wiley.com/doi/10.1002/adma.201705791
@@ -109,3 +109,39 @@ class Perchlorate(Specie):
109
109
 
110
110
  super().__init__(atoms=pclo, lj=lj, bonds=[b1, b2], angles=a1, cutoff=1.5, **kwargs)
111
111
  return
112
+
113
+ #metal parameters from https://pubs.acs.org/doi/full/10.1021/jp801931d
114
+ class Metal111(Specie):
115
+
116
+ def __init__(self, metal, **kwargs):
117
+
118
+ slab = fcc111(metal, size=(1,2,3), orthogonal=True, periodic=True)
119
+
120
+ # those are in kcal/mol and r0, so it should be divided by 2**1/6
121
+ metal_params = {
122
+ "Ag" : [4.56, 2.955],
123
+ "Al" : [4.02, 2.925],
124
+ "Au" : [5.29, 2.951],
125
+ "Cu" : [4.72, 2.616],
126
+ "Ni" : [5.65, 2.552],
127
+ "Pb" : [2.93, 3.565],
128
+ "Pd" : [6.15, 2.819],
129
+ "Pt" : [7.80, 2.845],
130
+ "Ac" : [6.51, 3.843],
131
+ "Ca" : [3.36, 4.025],
132
+ "Ce" : [6.38, 3.734],
133
+ "Es" : [2.88, 4.133],
134
+ "Ir" : [9.20, 2.785],
135
+ "Fe" : [6.00, 2.590],
136
+ "Rh" : [7.84, 2.757],
137
+ "Sr" : [3.40, 4.379],
138
+ "Th" : [8.47, 3.683],
139
+ "Yb" : [2.71, 3.942]
140
+ }
141
+
142
+ lj = metal_params[metal]
143
+ lj_copy = [lj[0], round(lj[1] / np.power(2, 1/6), 5)]
144
+
145
+ super().__init__(atoms=slab, lj=lj_copy, **kwargs)
146
+
147
+ return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mdinterface
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: mdinterface: A package for building interface systems in Molecular Dynamics simulations.
5
5
  Home-page: https://gitlab.com/roncofaber/mdinterface
6
6
  License: Apache-2.0
@@ -16,10 +16,11 @@ Requires-Dist: mdanalysis
16
16
  Requires-Dist: ase
17
17
  Requires-Dist: numpy
18
18
  Requires-Dist: networkx
19
+ Requires-Dist: libarvo
19
20
 
20
- # mdinterface: build interface systems for Molecular Dynamics simulations
21
+ # mdinterface: Build Interface Systems for Molecular Dynamics Simulations
21
22
 
22
- `mdinterface` is a Python package for building systems to use in Molecular Dynamics (MD) simulations. The package was primarly developed to help construct electrolyte/electrode interfaces, but is well suited to generate suitable MD boxes of liquids, electrolyte systems and polymer networks.
23
+ `mdinterface` is a Python package designed to build systems for Molecular Dynamics (MD) simulations. Initially developed to construct electrolyte/electrode interfaces, it is also well-suited for generating MD boxes of liquids, electrolyte systems, and polymer networks.
23
24
 
24
25
  ## Features
25
26
 
@@ -34,6 +35,12 @@ Requires-Dist: networkx
34
35
 
35
36
  Check the file [requirements.txt](requirements.txt) to see which packages are needed. Installing the package using `pip` should already take care of all dependencies.
36
37
 
38
+ Additionaly, you need the `packmol` utility installed to generate MD boxes. You can follow the instructions at [https://m3g.github.io/packmol/](https://m3g.github.io/packmol/) to install it. Alternatively, you can install it using `conda`:
39
+
40
+ ```bash
41
+ conda install -c conda-forge packmol
42
+ ```
43
+
37
44
  ## Installation
38
45
 
39
46
  ### Install using `pip`
@@ -46,7 +53,7 @@ pip install mdinterface
46
53
 
47
54
  ### Install directly the source code
48
55
 
49
- Alternatively you can obtain `mdinterface` directly from the repository by following those steps:
56
+ Alternatively you can obtain `mdinterface` directly from the repository by following these steps:
50
57
 
51
58
  Clone the repository in the desired location:
52
59
 
@@ -74,3 +81,7 @@ If you plan of making changes, clone the package and install the requirements bu
74
81
  ```bash
75
82
  pip install --no-build-isolation --no-deps -e .
76
83
  ```
84
+
85
+ ## Usage
86
+
87
+ Check the files in [examples](mdinterface/examples/) to learn how to use the package.
@@ -12,6 +12,10 @@ mdinterface.egg-info/SOURCES.txt
12
12
  mdinterface.egg-info/dependency_links.txt
13
13
  mdinterface.egg-info/requires.txt
14
14
  mdinterface.egg-info/top_level.txt
15
+ mdinterface/build/__init__.py
16
+ mdinterface/build/box.py
17
+ mdinterface/build/continuum2sim.py
18
+ mdinterface/build/polymer.py
15
19
  mdinterface/core/__init__.py
16
20
  mdinterface/core/specie.py
17
21
  mdinterface/core/topology.py
@@ -2,3 +2,4 @@ mdanalysis
2
2
  ase
3
3
  numpy
4
4
  networkx
5
+ libarvo
@@ -2,3 +2,4 @@ mdanalysis
2
2
  ase
3
3
  numpy
4
4
  networkx
5
+ libarvo
@@ -1,10 +0,0 @@
1
- """
2
- mdinterface is a package to setup molecular dynamics systems.
3
- """
4
-
5
- __version__ = '1.0.0'
6
- __date__ = '16 Dec. 2024'
7
- __author__ = 'Fabrice Roncoroni'
8
- __all__ = ['SimulationBox']
9
-
10
- from .simulationbox import SimulationBox
File without changes
File without changes
File without changes
File without changes
File without changes