mdinterface 1.0.0__tar.gz → 1.2.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 (50) hide show
  1. mdinterface-1.2.0/PKG-INFO +133 -0
  2. mdinterface-1.2.0/README.md +111 -0
  3. mdinterface-1.2.0/mdinterface/__init__.py +20 -0
  4. mdinterface-1.2.0/mdinterface/build/__init__.py +3 -0
  5. mdinterface-1.2.0/mdinterface/build/box.py +254 -0
  6. mdinterface-1.2.0/mdinterface/build/charges.py +8 -0
  7. mdinterface-1.2.0/mdinterface/build/continuum2sim.py +45 -0
  8. mdinterface-1.2.0/mdinterface/build/polymerize.py +95 -0
  9. mdinterface-1.2.0/mdinterface/config.py +40 -0
  10. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface/core/__init__.py +1 -0
  11. mdinterface-1.2.0/mdinterface/core/polymer.py +158 -0
  12. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface/core/specie.py +142 -81
  13. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface/core/topology.py +31 -6
  14. mdinterface-1.2.0/mdinterface/database/__init__.py +12 -0
  15. mdinterface-1.2.0/mdinterface/database/graphene.py +35 -0
  16. mdinterface-1.2.0/mdinterface/database/ions.py +188 -0
  17. mdinterface-1.2.0/mdinterface/database/metals.py +56 -0
  18. mdinterface-1.2.0/mdinterface/database/molecules.py +58 -0
  19. mdinterface-1.2.0/mdinterface/externals/__init__.py +11 -0
  20. mdinterface-1.2.0/mdinterface/externals/ligpargen.py +91 -0
  21. mdinterface-1.2.0/mdinterface/externals/obabel.py +49 -0
  22. mdinterface-1.2.0/mdinterface/externals/pyscf.py +74 -0
  23. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface/io/lammpswriter.py +1 -1
  24. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface/io/packmol.py +1 -1
  25. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface/io/read.py +53 -40
  26. mdinterface-1.2.0/mdinterface/simulationbox.py +438 -0
  27. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface/utils/__init__.py +2 -1
  28. mdinterface-1.2.0/mdinterface/utils/auxiliary.py +193 -0
  29. mdinterface-1.0.0/mdinterface/utils/auxiliary.py → mdinterface-1.2.0/mdinterface/utils/graphs.py +82 -129
  30. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface/utils/map.py +18 -7
  31. mdinterface-1.2.0/mdinterface.egg-info/PKG-INFO +133 -0
  32. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface.egg-info/SOURCES.txt +17 -1
  33. mdinterface-1.2.0/mdinterface.egg-info/requires.txt +7 -0
  34. mdinterface-1.2.0/requirements.txt +7 -0
  35. mdinterface-1.0.0/PKG-INFO +0 -76
  36. mdinterface-1.0.0/README.md +0 -57
  37. mdinterface-1.0.0/mdinterface/__init__.py +0 -10
  38. mdinterface-1.0.0/mdinterface/simulationbox.py +0 -621
  39. mdinterface-1.0.0/mdinterface/utils/database.py +0 -111
  40. mdinterface-1.0.0/mdinterface.egg-info/PKG-INFO +0 -76
  41. mdinterface-1.0.0/mdinterface.egg-info/requires.txt +0 -4
  42. mdinterface-1.0.0/requirements.txt +0 -4
  43. {mdinterface-1.0.0 → mdinterface-1.2.0}/LICENSE +0 -0
  44. {mdinterface-1.0.0 → mdinterface-1.2.0}/MANIFEST.in +0 -0
  45. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface/io/__init__.py +0 -0
  46. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface.egg-info/dependency_links.txt +0 -0
  47. {mdinterface-1.0.0 → mdinterface-1.2.0}/mdinterface.egg-info/top_level.txt +0 -0
  48. {mdinterface-1.0.0 → mdinterface-1.2.0}/pyproject.toml +0 -0
  49. {mdinterface-1.0.0 → mdinterface-1.2.0}/setup.cfg +0 -0
  50. {mdinterface-1.0.0 → mdinterface-1.2.0}/setup.py +0 -0
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.2
2
+ Name: mdinterface
3
+ Version: 1.2.0
4
+ Summary: mdinterface: A package for building interface systems in Molecular Dynamics simulations.
5
+ Home-page: https://gitlab.com/roncofaber/mdinterface
6
+ License: Apache-2.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: Apache Software License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Requires-Python: >=3.6
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: mdanalysis
16
+ Requires-Dist: ase
17
+ Requires-Dist: numpy
18
+ Requires-Dist: networkx
19
+ Requires-Dist: libarvo
20
+ Requires-Dist: platformdirs
21
+ Requires-Dist: configparser
22
+
23
+ <div style="display: flex; align-items: center;">
24
+ <img src="./assets/mdinterface.png" alt="Logo" width="80" style="margin-right: 10px;">
25
+ <div style="display: flex; flex-direction: column;">
26
+ <h1 style="margin: 0;">mdinterface: Build Interface Systems for Molecular Dynamics Simulations</h1>
27
+ </div>
28
+ </div>
29
+
30
+ [![PyPI version](https://badge.fury.io/py/mdinterface.svg?icon=si%3Apython)](https://pypi.org/project/mdinterface/) [![GitHub version](https://badge.fury.io/gh/roncofaber%2Fmdinterface.svg?icon=si%3Agithub)](https://github.com/roncofaber/mdinterface)
31
+
32
+ `mdinterface` is a Python package designed to build systems for Molecular Dynamics (MD) simulations. Initially developed to construct electrolyte/electrode solid-liquid interfaces, it is also well-suited for generating MD boxes of liquids, electrolyte systems, and polymer networks.
33
+
34
+ ## Features
35
+
36
+ Using `mdinterface` you can:
37
+
38
+ - Create layered simulation boxes with solvents, solutes, and interface slabs.
39
+ - Populate your system with ions and solvents using PACKMOL, and provide the starting concentration profile of the species to get the MD where you want it to be, but faster.
40
+ - Import common molecules/metals/polymers with pre-defined classical force fields parameters from the database, or automatically generate new OPLS-AA force field parameters using [https://github.com/Isra3l/ligpargen](LigParGen).
41
+ - Generate polymer chains of any length from a starting monomer.
42
+ - Estimate the RESP charges of molecules using the [PySCF electronic structure code](https://github.com/pyscf/pyscf).
43
+ - Automatically write LAMMPS data files and coefficients, so you can start making them atoms dance!
44
+
45
+ ## Requirements
46
+
47
+ Check the file [requirements.txt](requirements.txt) to see which packages are needed. Installing the package using `pip` should already take care of all mandatory dependencies.
48
+
49
+ 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`:
50
+
51
+ ```bash
52
+ conda install -c conda-forge packmol
53
+ ```
54
+
55
+ ### Optional packages
56
+
57
+ #### Automatic OPLS-AA force field generator with LigParGen
58
+
59
+ If you want to use LigParGen, please follow the instructions on their [GitHub](https://github.com/Isra3l/ligpargen). To make sure that BOSS is recognized by the package, specify the environment variable `BOSSdir`.
60
+
61
+ You can do this manually every time, or just add the path to the `config.ini` file to your config directory:
62
+
63
+ ```bash
64
+ # config.ini
65
+ [settings]
66
+ BOSSdir = /path/to/your/boss/dir
67
+ ```
68
+
69
+ The config directory is found using [`platformdirs`](https://pypi.org/project/platformdirs/) and is OS dependent.
70
+
71
+ ### RESP charge analysis with PySCF
72
+
73
+ To use the RESP charge analysis feature, you need to install [PySCF](https://github.com/pyscf/pyscf) and [PyMBXAS](https://gitlab.com/roncofaber/pymbxas).
74
+
75
+ ## Installation
76
+
77
+ ### Install using `pip`
78
+
79
+ You can simply install the latest release of the package and all dependencies using:
80
+
81
+ ```bash
82
+ pip install mdinterface
83
+ ```
84
+
85
+ ### Install directly the source code
86
+
87
+ Alternatively you can obtain `mdinterface` directly from the repository by following these steps:
88
+
89
+ Clone the repository in the desired location:
90
+
91
+ ```bash
92
+ git clone git@gitlab.com:roncofaber/mdinterface.git
93
+ ```
94
+
95
+ Install the required packages:
96
+
97
+ ```bash
98
+ cd mdinterface
99
+ conda install -c conda-forge --file requirements.txt
100
+ ```
101
+
102
+ Install the package with pip:
103
+
104
+ ```bash
105
+ pip install .
106
+ ```
107
+
108
+ ### Install a development environment
109
+
110
+ If you plan of making changes, clone the package and install the requirements but then add it to your development environment with:
111
+
112
+ ```bash
113
+ pip install --no-build-isolation --no-deps -e .
114
+ ```
115
+
116
+ ## Usage
117
+
118
+ Creating a new Specie (with its topology attributes) is as simple as doing:
119
+
120
+ ```python
121
+ #%% Make a specie, and use LigParGen to estimate FF parameters
122
+ my_specie = Specie("CH3ONO", ligpargen=True) # molecule is in ASE database
123
+ ```
124
+
125
+ Please, check the files in [examples](mdinterface/examples/) to learn how to use the package. Including setting up simulation boxes, creating polymers and much more.
126
+
127
+ ## Roadmap
128
+
129
+ Since the original idea was to make a package to build MD boxes layer by layer, I am strongly debating renaming everything as "Workflow for Easy Molecular DYnamics Simulations", aka WEMDYS.
130
+
131
+ ## Questions & Issues
132
+
133
+ Sir, this is a WEMDY'S. Please contact me or open an issue, glad to talk about ideas and improvements!
@@ -0,0 +1,111 @@
1
+ <div style="display: flex; align-items: center;">
2
+ <img src="./assets/mdinterface.png" alt="Logo" width="80" style="margin-right: 10px;">
3
+ <div style="display: flex; flex-direction: column;">
4
+ <h1 style="margin: 0;">mdinterface: Build Interface Systems for Molecular Dynamics Simulations</h1>
5
+ </div>
6
+ </div>
7
+
8
+ [![PyPI version](https://badge.fury.io/py/mdinterface.svg?icon=si%3Apython)](https://pypi.org/project/mdinterface/) [![GitHub version](https://badge.fury.io/gh/roncofaber%2Fmdinterface.svg?icon=si%3Agithub)](https://github.com/roncofaber/mdinterface)
9
+
10
+ `mdinterface` is a Python package designed to build systems for Molecular Dynamics (MD) simulations. Initially developed to construct electrolyte/electrode solid-liquid interfaces, it is also well-suited for generating MD boxes of liquids, electrolyte systems, and polymer networks.
11
+
12
+ ## Features
13
+
14
+ Using `mdinterface` you can:
15
+
16
+ - Create layered simulation boxes with solvents, solutes, and interface slabs.
17
+ - Populate your system with ions and solvents using PACKMOL, and provide the starting concentration profile of the species to get the MD where you want it to be, but faster.
18
+ - Import common molecules/metals/polymers with pre-defined classical force fields parameters from the database, or automatically generate new OPLS-AA force field parameters using [https://github.com/Isra3l/ligpargen](LigParGen).
19
+ - Generate polymer chains of any length from a starting monomer.
20
+ - Estimate the RESP charges of molecules using the [PySCF electronic structure code](https://github.com/pyscf/pyscf).
21
+ - Automatically write LAMMPS data files and coefficients, so you can start making them atoms dance!
22
+
23
+ ## Requirements
24
+
25
+ Check the file [requirements.txt](requirements.txt) to see which packages are needed. Installing the package using `pip` should already take care of all mandatory dependencies.
26
+
27
+ 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`:
28
+
29
+ ```bash
30
+ conda install -c conda-forge packmol
31
+ ```
32
+
33
+ ### Optional packages
34
+
35
+ #### Automatic OPLS-AA force field generator with LigParGen
36
+
37
+ If you want to use LigParGen, please follow the instructions on their [GitHub](https://github.com/Isra3l/ligpargen). To make sure that BOSS is recognized by the package, specify the environment variable `BOSSdir`.
38
+
39
+ You can do this manually every time, or just add the path to the `config.ini` file to your config directory:
40
+
41
+ ```bash
42
+ # config.ini
43
+ [settings]
44
+ BOSSdir = /path/to/your/boss/dir
45
+ ```
46
+
47
+ The config directory is found using [`platformdirs`](https://pypi.org/project/platformdirs/) and is OS dependent.
48
+
49
+ ### RESP charge analysis with PySCF
50
+
51
+ To use the RESP charge analysis feature, you need to install [PySCF](https://github.com/pyscf/pyscf) and [PyMBXAS](https://gitlab.com/roncofaber/pymbxas).
52
+
53
+ ## Installation
54
+
55
+ ### Install using `pip`
56
+
57
+ You can simply install the latest release of the package and all dependencies using:
58
+
59
+ ```bash
60
+ pip install mdinterface
61
+ ```
62
+
63
+ ### Install directly the source code
64
+
65
+ Alternatively you can obtain `mdinterface` directly from the repository by following these steps:
66
+
67
+ Clone the repository in the desired location:
68
+
69
+ ```bash
70
+ git clone git@gitlab.com:roncofaber/mdinterface.git
71
+ ```
72
+
73
+ Install the required packages:
74
+
75
+ ```bash
76
+ cd mdinterface
77
+ conda install -c conda-forge --file requirements.txt
78
+ ```
79
+
80
+ Install the package with pip:
81
+
82
+ ```bash
83
+ pip install .
84
+ ```
85
+
86
+ ### Install a development environment
87
+
88
+ If you plan of making changes, clone the package and install the requirements but then add it to your development environment with:
89
+
90
+ ```bash
91
+ pip install --no-build-isolation --no-deps -e .
92
+ ```
93
+
94
+ ## Usage
95
+
96
+ Creating a new Specie (with its topology attributes) is as simple as doing:
97
+
98
+ ```python
99
+ #%% Make a specie, and use LigParGen to estimate FF parameters
100
+ my_specie = Specie("CH3ONO", ligpargen=True) # molecule is in ASE database
101
+ ```
102
+
103
+ Please, check the files in [examples](mdinterface/examples/) to learn how to use the package. Including setting up simulation boxes, creating polymers and much more.
104
+
105
+ ## Roadmap
106
+
107
+ Since the original idea was to make a package to build MD boxes layer by layer, I am strongly debating renaming everything as "Workflow for Easy Molecular DYnamics Simulations", aka WEMDYS.
108
+
109
+ ## Questions & Issues
110
+
111
+ Sir, this is a WEMDY'S. Please contact me or open an issue, glad to talk about ideas and improvements!
@@ -0,0 +1,20 @@
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, electrolyte systems, and polymer networks.
6
+
7
+ """
8
+
9
+ __version__ = '1.2.0'
10
+ __date__ = '28 Mar. 2025'
11
+ __author__ = 'Fabrice Roncoroni'
12
+ __all__ = ['SimulationBox', "Specie", "Polymer"]
13
+
14
+ from .simulationbox import SimulationBox
15
+ from .core.specie import Specie
16
+ from .core.polymer import Polymer
17
+
18
+ # load configuration file
19
+ from .config import load_config
20
+ load_config()
@@ -0,0 +1,3 @@
1
+ from .polymerize import *
2
+ from .box import *
3
+ from .continuum2sim import *
@@ -0,0 +1,254 @@
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)
231
+
232
+
233
+ # add a component to the system
234
+ def add_component(system, component, zdim, padding=0):
235
+
236
+ # nothing to add here
237
+ if component is None:
238
+ return system, zdim
239
+
240
+ # ohh, let's lego the shit out of this
241
+ component = component.copy()
242
+
243
+ # component: "look at me, I am the system now."
244
+ if system is None:
245
+ component.atoms.translate([0, 0, zdim])
246
+ system = component
247
+ zdim += component.dimensions[2]
248
+
249
+ # make space and add it to the pile
250
+ else:
251
+ component.atoms.translate([0, 0, zdim + padding])
252
+ system = mda.Merge(system.atoms, component.atoms)
253
+ zdim += component.dimensions[2] + padding
254
+ return system, zdim
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Mon Feb 3 14:50:47 2025
5
+
6
+ @author: roncofaber
7
+ """
8
+
@@ -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,95 @@
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(monomer, substitute, nrep, start_end_idxs=None, 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
+ - monomer (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
+ # make sure we don't mess nothing
31
+ monomer = monomer.copy()
32
+
33
+ # if start end is provided, do substitution here
34
+ if start_end_idxs is not None:
35
+ assert len(start_end_idxs) == 2
36
+ symbols = monomer.get_chemical_symbols()
37
+ symbols[start_end_idxs[0]] = "X"
38
+ symbols[start_end_idxs[1]] = "X"
39
+ monomer.set_chemical_symbols(symbols)
40
+
41
+ # Check if nrep is at least 1
42
+ if nrep < 1:
43
+ raise ValueError("nrep must be at least 1")
44
+
45
+ # Find the indices of the 'X' atoms
46
+ x_idxs = np.where(np.array(monomer.get_chemical_symbols()) == "X")[0]
47
+ if len(x_idxs) != 2:
48
+ raise ValueError("The monomer should contain exactly two 'X' atoms.")
49
+
50
+ # Find atoms connected to X
51
+ ini_idx = ase.build.connected_indices(monomer, x_idxs[0])[1]
52
+ end_idx = ase.build.connected_indices(monomer, x_idxs[1])[1]
53
+
54
+ bnd_vec = monomer.get_distance(end_idx, x_idxs[1], vector=True)
55
+ bnd_vec = target_distance * bnd_vec / np.linalg.norm(bnd_vec)
56
+
57
+ ini_pos = monomer.get_positions()[ini_idx]
58
+ end_pos = monomer.get_positions()[end_idx] + bnd_vec
59
+
60
+ # Get the vector between the two 'X' atoms
61
+ X_vec = end_pos - ini_pos
62
+
63
+ # remember connecting points
64
+ is_connected = np.array(len(monomer)*[False])
65
+ is_connected[ini_idx] = True
66
+ is_connected[end_idx] = True
67
+ monomer.new_array("is_connected", is_connected)
68
+
69
+ # Start with a copy of the monomer
70
+ sout = monomer.copy()
71
+ sout.new_array("mon_id", np.array(len(sout)*[0]))
72
+
73
+ # Replicate the monomer
74
+ if nrep > 1:
75
+ del sout[x_idxs[1]] # Remove the second 'X' atom in the first monomer
76
+
77
+ for ii in range(nrep - 1):
78
+ sadd = monomer.copy()
79
+
80
+ if ii < nrep - 2:
81
+ del sadd[x_idxs] # Remove both 'X' atoms in the intermediate monomers
82
+ else:
83
+ del sadd[x_idxs[0]] # Remove the first 'X' atom in the last monomer
84
+
85
+ sadd.translate((ii + 1) * X_vec)
86
+ sadd.set_array("mon_id", np.array(len(sadd)*[ii+1]))
87
+ sout += sadd
88
+
89
+ # Replace 'X' with the substitute
90
+ symbols = np.array(sout.get_chemical_symbols())
91
+ symbols[symbols == "X"] = substitute
92
+ sout.set_chemical_symbols(symbols)
93
+ sout.center()
94
+
95
+ return sout