cemd 0.1.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.
- cemd/__init__.py +33 -0
- cemd/_constants.py +252 -0
- cemd/analysis/__init__.py +32 -0
- cemd/analysis/density.py +394 -0
- cemd/analysis/diffusion.py +645 -0
- cemd/analysis/rdf.py +132 -0
- cemd/analysis/silicates.py +236 -0
- cemd/analysis/tcf.py +222 -0
- cemd/analysis/util.py +254 -0
- cemd/analysis/velocities.py +60 -0
- cemd/build/__init__.py +20 -0
- cemd/build/_packmol.py +719 -0
- cemd/build/_structures/__init__.py +30 -0
- cemd/build/_structures/aft_moore.cif +60 -0
- cemd/build/_structures/co3.pdb +10 -0
- cemd/build/_structures/h2o.lt +160 -0
- cemd/build/_structures/ho.sdf +111 -0
- cemd/build/_structures/so4.pdb +15 -0
- cemd/build/_structures/tob11a_hamid.cif +89 -0
- cemd/build/_structures/tob11a_merlino.cif +104 -0
- cemd/build/base.py +86 -0
- cemd/build/cement_hydrates/__init__.py +3 -0
- cemd/build/cement_hydrates/_interlayer_helpers.py +185 -0
- cemd/build/cement_hydrates/_silicate_helpers.py +443 -0
- cemd/build/cement_hydrates/hydrates.py +651 -0
- cemd/build/glass.py +532 -0
- cemd/build/interface.py +331 -0
- cemd/build/solution.py +574 -0
- cemd/build/split.py +623 -0
- cemd/build/surface.py +366 -0
- cemd/core/__init__.py +0 -0
- cemd/core/_edit.py +574 -0
- cemd/core/_format.py +759 -0
- cemd/core/_io/__init__.py +20 -0
- cemd/core/_io/formats/__init__.py +0 -0
- cemd/core/_io/formats/base.py +39 -0
- cemd/core/_io/formats/cif.py +52 -0
- cemd/core/_io/formats/lammps.py +1023 -0
- cemd/core/_io/formats/lt.py +293 -0
- cemd/core/_io/formats/mda.py +630 -0
- cemd/core/_io/formats/pdb.py +275 -0
- cemd/core/_io/formats/pmg.py +146 -0
- cemd/core/_io/formats/sdf.py +139 -0
- cemd/core/_io/formats/smiles.py +113 -0
- cemd/core/_io/io_mixin.py +349 -0
- cemd/core/_io/sources/cod.py +368 -0
- cemd/core/_io/sources/pubchem.py +329 -0
- cemd/core/_view/__init__.py +3 -0
- cemd/core/_view/config.py +119 -0
- cemd/core/_view/default_vmd_config.toml +114 -0
- cemd/core/_view/view.py +391 -0
- cemd/core/_view/view.tcl +126 -0
- cemd/core/atomic_system.py +1136 -0
- cemd/core/forcefield_classes.py +183 -0
- cemd/core/forcefield_mixin.py +924 -0
- cemd/core/topology_mixin.py +637 -0
- cemd/forcefield/__init__.py +31 -0
- cemd/forcefield/_config.py +116 -0
- cemd/forcefield/_editor.py +282 -0
- cemd/forcefield/_parsers/__init__.py +0 -0
- cemd/forcefield/_parsers/_base.py +187 -0
- cemd/forcefield/_parsers/_gromos.py +290 -0
- cemd/forcefield/_parsers/_iff_charmm.py +707 -0
- cemd/forcefield/_parsers/_iff_cvff.py +496 -0
- cemd/forcefield/_parsers/_toml.py +189 -0
- cemd/forcefield/db/GROMOS_54A7_ATB.lt +4702 -0
- cemd/forcefield/db/charmm27_interface_v1_5.prm +4123 -0
- cemd/forcefield/db/clayff.toml +289 -0
- cemd/forcefield/db/cshff2014.toml +186 -0
- cemd/forcefield/db/cvff_interface_v1_5.frc +5124 -0
- cemd/forcefield/db/guillot2007.toml +130 -0
- cemd/forcefield/db/iff.toml +264 -0
- cemd/forcefield/db/raiteri2015.toml +179 -0
- cemd/forcefield/db/solcon.toml +73 -0
- cemd/forcefield/db/spc.toml +91 -0
- cemd/forcefield/db/spc_fw.toml +48 -0
- cemd/forcefield/db/spce.toml +64 -0
- cemd/forcefield/db/sperinck.toml +146 -0
- cemd/forcefield/db/tip3p.toml +60 -0
- cemd/forcefield/db/tip4p2005.toml +64 -0
- cemd/forcefield/db/tip4p_ew.toml +64 -0
- cemd/forcefield/db/wang2008.toml +86 -0
- cemd/forcefield/forcefield_database.py +599 -0
- cemd/forcefield/models.py +327 -0
- cemd/gui/__init__.py +0 -0
- cemd/gui/_userdata.py +61 -0
- cemd/gui/default_config.json +143 -0
- cemd/gui/icons/add_mol.svg +70 -0
- cemd/gui/icons/atom.svg +54 -0
- cemd/gui/icons/bg-color.svg +1 -0
- cemd/gui/icons/camera.svg +1 -0
- cemd/gui/icons/cash.svg +116 -0
- cemd/gui/icons/channel.svg +90 -0
- cemd/gui/icons/cod.svg +163 -0
- cemd/gui/icons/connectivity.svg +1 -0
- cemd/gui/icons/droplet.svg +77 -0
- cemd/gui/icons/file-pdf.svg +1 -0
- cemd/gui/icons/glass.svg +110 -0
- cemd/gui/icons/interface.svg +81 -0
- cemd/gui/icons/interface2.svg +55 -0
- cemd/gui/icons/items-grid.svg +1 -0
- cemd/gui/icons/minus-circle.svg +1 -0
- cemd/gui/icons/minus-square-white.svg +63 -0
- cemd/gui/icons/minus-square.svg +61 -0
- cemd/gui/icons/minus.svg +60 -0
- cemd/gui/icons/mol.svg +60 -0
- cemd/gui/icons/move.svg +1 -0
- cemd/gui/icons/open.svg +1 -0
- cemd/gui/icons/orthogonalize.svg +53 -0
- cemd/gui/icons/paste.svg +1 -0
- cemd/gui/icons/plus-circle.svg +1 -0
- cemd/gui/icons/plus-square-white.svg +61 -0
- cemd/gui/icons/plus-square.svg +59 -0
- cemd/gui/icons/plus.svg +48 -0
- cemd/gui/icons/protonate.svg +87 -0
- cemd/gui/icons/pubchem.svg +56 -0
- cemd/gui/icons/pycsh.svg +142 -0
- cemd/gui/icons/question.svg +1 -0
- cemd/gui/icons/rdf.svg +44 -0
- cemd/gui/icons/reference.svg +1 -0
- cemd/gui/icons/replicate.svg +1 -0
- cemd/gui/icons/save-all.svg +1 -0
- cemd/gui/icons/save.svg +1 -0
- cemd/gui/icons/search.svg +1 -0
- cemd/gui/icons/silicate.svg +121 -0
- cemd/gui/icons/solution.svg +130 -0
- cemd/gui/icons/square.svg +1 -0
- cemd/gui/icons/surface.svg +57 -0
- cemd/gui/icons/trash.svg +1 -0
- cemd/gui/icons/wall.svg +10 -0
- cemd/gui/icons/wrap.svg +67 -0
- cemd/gui/icons/zap.svg +60 -0
- cemd/gui/logic/__init__.py +0 -0
- cemd/gui/logic/build.py +369 -0
- cemd/gui/logic/file_handler.py +127 -0
- cemd/gui/main_window.py +1060 -0
- cemd/gui/plotter_widget.py +410 -0
- cemd/gui/tabs.py +452 -0
- cemd/gui/ui/__init__.py +0 -0
- cemd/gui/ui/analysis_view.py +368 -0
- cemd/gui/ui/atom_table.py +139 -0
- cemd/gui/ui/base_dialog.py +144 -0
- cemd/gui/ui/build.py +1540 -0
- cemd/gui/ui/cod.py +256 -0
- cemd/gui/ui/gui_utils.py +85 -0
- cemd/gui/ui/managers.py +1050 -0
- cemd/gui/ui/panels.py +539 -0
- cemd/gui/ui/pubchem.py +216 -0
- cemd/topology/__init__.py +27 -0
- cemd/topology/_apply.py +253 -0
- cemd/topology/predefined.py +76 -0
- cemd/topology/rules.py +176 -0
- cemd-0.1.0.dist-info/METADATA +263 -0
- cemd-0.1.0.dist-info/RECORD +158 -0
- cemd-0.1.0.dist-info/WHEEL +5 -0
- cemd-0.1.0.dist-info/entry_points.txt +2 -0
- cemd-0.1.0.dist-info/licenses/LICENCE +674 -0
- cemd-0.1.0.dist-info/top_level.txt +1 -0
cemd/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the CEMD distribution
|
|
3
|
+
# Copyright (c) 2022-2026 Jérôme Claverie.
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, version 3.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful, but
|
|
10
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12
|
+
# General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
19
|
+
|
|
20
|
+
from .core.atomic_system import AtomicSystem
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
__version__ = version("cemd")
|
|
24
|
+
except PackageNotFoundError: # running from a source tree, never installed
|
|
25
|
+
__version__ = "unknown"
|
|
26
|
+
|
|
27
|
+
__author__ = "Jérôme Claverie"
|
|
28
|
+
|
|
29
|
+
__copyright__ = "Copyright (c) 2022-2026 Jérôme Claverie"
|
|
30
|
+
|
|
31
|
+
__license__ = "GPL-3.0"
|
|
32
|
+
|
|
33
|
+
__all__ = ["AtomicSystem", "__version__"]
|
cemd/_constants.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the CEMD distribution
|
|
3
|
+
# Copyright (c) 2022-2026 Jérôme Claverie.
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, version 3.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful, but
|
|
10
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12
|
+
# General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
AVOGADRO = 6.02214076e23
|
|
21
|
+
|
|
22
|
+
MASSES_DICT = {
|
|
23
|
+
"H": 1.007947,
|
|
24
|
+
"He": 4.0026022,
|
|
25
|
+
"Li": 6.9412,
|
|
26
|
+
"Be": 9.0121823,
|
|
27
|
+
"B": 10.8117,
|
|
28
|
+
"C": 12.01078,
|
|
29
|
+
"N": 14.00672,
|
|
30
|
+
"O": 15.99943,
|
|
31
|
+
"F": 18.99840325,
|
|
32
|
+
"Ne": 20.17976,
|
|
33
|
+
"Na": 22.989769282,
|
|
34
|
+
"Mg": 24.30506,
|
|
35
|
+
"Al": 26.981538613,
|
|
36
|
+
"Si": 28.08553,
|
|
37
|
+
"P": 30.9737622,
|
|
38
|
+
"S": 32.0655,
|
|
39
|
+
"Cl": 35.4532,
|
|
40
|
+
"Ar": 39.9481,
|
|
41
|
+
"K": 39.09831,
|
|
42
|
+
"Ca": 40.0784,
|
|
43
|
+
"Sc": 44.9559126,
|
|
44
|
+
"Ti": 47.8671,
|
|
45
|
+
"V": 50.94151,
|
|
46
|
+
"Cr": 51.99616,
|
|
47
|
+
"Mn": 54.9380451,
|
|
48
|
+
"Fe": 55.8452,
|
|
49
|
+
"Co": 58.9331955,
|
|
50
|
+
"Ni": 58.69344,
|
|
51
|
+
"Cu": 63.5463,
|
|
52
|
+
"Zn": 65.382,
|
|
53
|
+
"Ga": 69.7231,
|
|
54
|
+
"Ge": 72.6308,
|
|
55
|
+
"As": 74.921602,
|
|
56
|
+
"Se": 78.963,
|
|
57
|
+
"Br": 79.9041,
|
|
58
|
+
"Kr": 83.7982,
|
|
59
|
+
"Rb": 85.46783,
|
|
60
|
+
"Sr": 87.621,
|
|
61
|
+
"Y": 88.905852,
|
|
62
|
+
"Zr": 91.2242,
|
|
63
|
+
"Nb": 92.906382,
|
|
64
|
+
"Mo": 95.962,
|
|
65
|
+
"Tc": 98.0,
|
|
66
|
+
"Ru": 101.072,
|
|
67
|
+
"Rh": 102.905502,
|
|
68
|
+
"Pd": 106.421,
|
|
69
|
+
"Ag": 107.86822,
|
|
70
|
+
"Cd": 112.4118,
|
|
71
|
+
"In": 114.8181,
|
|
72
|
+
"Sn": 118.7107,
|
|
73
|
+
"Sb": 121.7601,
|
|
74
|
+
"Te": 127.603,
|
|
75
|
+
"I": 126.904473,
|
|
76
|
+
"Xe": 131.2936,
|
|
77
|
+
"Cs": 132.90545196,
|
|
78
|
+
"Ba": 137.3277,
|
|
79
|
+
"D": 2.01410178,
|
|
80
|
+
"T": 3.01604928,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
VDW_RADII = {
|
|
84
|
+
"H": 1.10,
|
|
85
|
+
"He": 1.40,
|
|
86
|
+
"Li": 1.82,
|
|
87
|
+
"Be": 1.53,
|
|
88
|
+
"B": 1.92,
|
|
89
|
+
"C": 1.70,
|
|
90
|
+
"N": 1.55,
|
|
91
|
+
"O": 1.52,
|
|
92
|
+
"F": 1.47,
|
|
93
|
+
"Ne": 1.54,
|
|
94
|
+
"Na": 2.27,
|
|
95
|
+
"Mg": 1.73,
|
|
96
|
+
"Al": 1.84,
|
|
97
|
+
"Si": 2.10,
|
|
98
|
+
"P": 1.80,
|
|
99
|
+
"S": 1.80,
|
|
100
|
+
"Cl": 1.75,
|
|
101
|
+
"Ar": 1.88,
|
|
102
|
+
"K": 2.75,
|
|
103
|
+
"Ca": 2.31,
|
|
104
|
+
"Sc": 2.11,
|
|
105
|
+
"Ti": 1.95,
|
|
106
|
+
"V": 1.91,
|
|
107
|
+
"Cr": 1.85,
|
|
108
|
+
"Mn": 1.79,
|
|
109
|
+
"Fe": 1.63,
|
|
110
|
+
"Co": 1.63,
|
|
111
|
+
"Ni": 1.63,
|
|
112
|
+
"Cu": 1.40,
|
|
113
|
+
"Zn": 1.39,
|
|
114
|
+
"Ga": 1.87,
|
|
115
|
+
"Ge": 2.11,
|
|
116
|
+
"As": 1.85,
|
|
117
|
+
"Se": 1.90,
|
|
118
|
+
"Br": 1.85,
|
|
119
|
+
"Kr": 2.02,
|
|
120
|
+
"Rb": 3.03,
|
|
121
|
+
"Sr": 2.49,
|
|
122
|
+
"Y": 2.32,
|
|
123
|
+
"Zr": 2.23,
|
|
124
|
+
"Nb": 2.18,
|
|
125
|
+
"Mo": 2.17,
|
|
126
|
+
"Tc": 2.16,
|
|
127
|
+
"Ru": 2.13,
|
|
128
|
+
"Rh": 2.10,
|
|
129
|
+
"Pd": 1.63,
|
|
130
|
+
"Ag": 1.72,
|
|
131
|
+
"Cd": 1.58,
|
|
132
|
+
"In": 1.93,
|
|
133
|
+
"Sn": 2.17,
|
|
134
|
+
"Sb": 2.06,
|
|
135
|
+
"Te": 2.06,
|
|
136
|
+
"I": 1.98,
|
|
137
|
+
"Xe": 2.16,
|
|
138
|
+
"Cs": 3.43,
|
|
139
|
+
"Ba": 2.68,
|
|
140
|
+
"La": 2.43,
|
|
141
|
+
"Ce": 2.42,
|
|
142
|
+
"Pr": 2.40,
|
|
143
|
+
"Nd": 2.39,
|
|
144
|
+
"Pm": 2.38,
|
|
145
|
+
"Sm": 2.36,
|
|
146
|
+
"Eu": 2.35,
|
|
147
|
+
"Gd": 2.34,
|
|
148
|
+
"Tb": 2.33,
|
|
149
|
+
"Dy": 2.31,
|
|
150
|
+
"Ho": 2.30,
|
|
151
|
+
"Er": 2.29,
|
|
152
|
+
"Tm": 2.27,
|
|
153
|
+
"Yb": 2.26,
|
|
154
|
+
"Lu": 2.24,
|
|
155
|
+
"Hf": 2.23,
|
|
156
|
+
"Ta": 2.22,
|
|
157
|
+
"W": 2.18,
|
|
158
|
+
"Re": 2.16,
|
|
159
|
+
"Os": 2.16,
|
|
160
|
+
"Ir": 2.13,
|
|
161
|
+
"Pt": 1.75,
|
|
162
|
+
"Au": 1.66,
|
|
163
|
+
"Hg": 1.55,
|
|
164
|
+
"Tl": 1.96,
|
|
165
|
+
"Pb": 2.02,
|
|
166
|
+
"Bi": 2.07,
|
|
167
|
+
"Po": 1.97,
|
|
168
|
+
"At": 2.02,
|
|
169
|
+
"Rn": 2.20,
|
|
170
|
+
"Fr": 3.48,
|
|
171
|
+
"Ra": 2.83,
|
|
172
|
+
"Ac": 2.47,
|
|
173
|
+
"Th": 2.45,
|
|
174
|
+
"Pa": 2.43,
|
|
175
|
+
"U": 1.86,
|
|
176
|
+
"Np": 2.41,
|
|
177
|
+
"Pu": 2.39,
|
|
178
|
+
"Am": 2.38,
|
|
179
|
+
"Cm": 2.37,
|
|
180
|
+
"Bk": 2.35,
|
|
181
|
+
"Cf": 2.34,
|
|
182
|
+
"Es": 2.33,
|
|
183
|
+
"Fm": 2.31,
|
|
184
|
+
"Md": 2.30,
|
|
185
|
+
"No": 2.29,
|
|
186
|
+
"Lr": 2.27,
|
|
187
|
+
"Rr": 3.03,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
CHARGES_DICT = {
|
|
191
|
+
"H": 1,
|
|
192
|
+
"Li": 1,
|
|
193
|
+
"Na": 1,
|
|
194
|
+
"K": 1,
|
|
195
|
+
"Rb": 1,
|
|
196
|
+
"Cs": 1,
|
|
197
|
+
"Be": 2,
|
|
198
|
+
"Mg": 2,
|
|
199
|
+
"Ca": 2,
|
|
200
|
+
"Sr": 2,
|
|
201
|
+
"Ba": 2,
|
|
202
|
+
"B": 3,
|
|
203
|
+
"Al": 3,
|
|
204
|
+
"Ga": 3,
|
|
205
|
+
"In": 3,
|
|
206
|
+
"C": 4,
|
|
207
|
+
"Si": 4,
|
|
208
|
+
"Ge": 4,
|
|
209
|
+
"Sn": 2,
|
|
210
|
+
"Pb": 2,
|
|
211
|
+
"N": -3,
|
|
212
|
+
"P": -3,
|
|
213
|
+
"As": -3,
|
|
214
|
+
"Sb": 3,
|
|
215
|
+
"Bi": 3,
|
|
216
|
+
"O": -2,
|
|
217
|
+
"S": -2,
|
|
218
|
+
"Se": -2,
|
|
219
|
+
"Te": -2,
|
|
220
|
+
"F": -1,
|
|
221
|
+
"Cl": -1,
|
|
222
|
+
"Br": -1,
|
|
223
|
+
"I": -1,
|
|
224
|
+
"Sc": 3,
|
|
225
|
+
"Ti": 4,
|
|
226
|
+
"V": 5,
|
|
227
|
+
"Cr": 3,
|
|
228
|
+
"Mn": 2,
|
|
229
|
+
"Fe": 3,
|
|
230
|
+
"Co": 2,
|
|
231
|
+
"Ni": 2,
|
|
232
|
+
"Cu": 2,
|
|
233
|
+
"Zn": 2,
|
|
234
|
+
"Y": 3,
|
|
235
|
+
"Zr": 4,
|
|
236
|
+
"Nb": 5,
|
|
237
|
+
"Mo": 6,
|
|
238
|
+
"Ag": 1,
|
|
239
|
+
"Cd": 2,
|
|
240
|
+
"W": 6,
|
|
241
|
+
"Pt": 4,
|
|
242
|
+
"Au": 3,
|
|
243
|
+
"Hg": 2,
|
|
244
|
+
"La": 3,
|
|
245
|
+
"Ce": 4,
|
|
246
|
+
"Nd": 3,
|
|
247
|
+
"Gd": 3,
|
|
248
|
+
"Yb": 3,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
INV_MASSES = {float(v): k for k, v in MASSES_DICT.items()}
|
|
252
|
+
MASS_KEYS = np.array(list(INV_MASSES.keys()))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the CEMD distribution
|
|
3
|
+
# Copyright (c) 2022-2026 Jérôme Claverie.
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, version 3.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful, but
|
|
10
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12
|
+
# General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
from .density import density_map, density_profile, electrostatic_potential
|
|
19
|
+
from .diffusion import diffusion_coefficient, msd, msd_profile
|
|
20
|
+
from .rdf import compute_rdf
|
|
21
|
+
from .silicates import analyze_silicates
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"compute_rdf",
|
|
25
|
+
"msd",
|
|
26
|
+
"msd_profile",
|
|
27
|
+
"diffusion_coefficient",
|
|
28
|
+
"density_profile",
|
|
29
|
+
"density_map",
|
|
30
|
+
"electrostatic_potential",
|
|
31
|
+
"analyze_silicates",
|
|
32
|
+
]
|
cemd/analysis/density.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the CEMD distribution
|
|
3
|
+
# Copyright (c) 2022-2026 Jérôme Claverie.
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, version 3.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful, but
|
|
10
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
12
|
+
# General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import dask
|
|
21
|
+
import MDAnalysis as mda
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pandas as pd
|
|
24
|
+
from scipy import integrate
|
|
25
|
+
from tqdm import tqdm
|
|
26
|
+
|
|
27
|
+
_AXIS_MAP = {
|
|
28
|
+
"x": {"axid": 0, "axida": 1, "axidb": 2},
|
|
29
|
+
"y": {"axid": 1, "axida": 0, "axidb": 2},
|
|
30
|
+
"z": {"axid": 2, "axida": 0, "axidb": 1},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_axis_ids(axis: str) -> dict:
|
|
35
|
+
if axis not in _AXIS_MAP:
|
|
36
|
+
raise ValueError(f"axis must be 'x', 'y' or 'z', got '{axis}'")
|
|
37
|
+
return _AXIS_MAP[axis]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def density_profile(
|
|
41
|
+
universe: mda.Universe,
|
|
42
|
+
atom_types: list[str | int],
|
|
43
|
+
axis: str = "z",
|
|
44
|
+
start: int = 0,
|
|
45
|
+
end: int = -1,
|
|
46
|
+
bin_size: float = 0.1,
|
|
47
|
+
) -> pd.DataFrame:
|
|
48
|
+
"""Create a DataFrame with the average density of atoms of given types along a given axis.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
universe : mda.Universe
|
|
53
|
+
The input MDAnalysis Universe to analyze.
|
|
54
|
+
atom_types : list
|
|
55
|
+
Atom types to compute the density for.
|
|
56
|
+
axis : str
|
|
57
|
+
Axis along which to calculate the density ('x', 'y', or 'z').
|
|
58
|
+
start : int
|
|
59
|
+
Starting frame index.
|
|
60
|
+
end : int
|
|
61
|
+
Ending frame index.
|
|
62
|
+
bin_size : float
|
|
63
|
+
Size of bins for the histogram (in Angstroms).
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
pd.DataFrame
|
|
68
|
+
Average density profile for the specified atom types.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
box = universe.dimensions
|
|
72
|
+
|
|
73
|
+
ids = _get_axis_ids(axis)
|
|
74
|
+
axid = ids["axid"]
|
|
75
|
+
axida, axidb = ids["axida"], ids["axidb"]
|
|
76
|
+
slice_vol = bin_size * box[axida] * box[axidb]
|
|
77
|
+
|
|
78
|
+
bins = np.arange(0, box[axid], bin_size)
|
|
79
|
+
pos = (bins[1:] + bins[:-1]) / 2
|
|
80
|
+
density_total = []
|
|
81
|
+
columns = []
|
|
82
|
+
|
|
83
|
+
def count_pframe(frame_index, sel):
|
|
84
|
+
|
|
85
|
+
sel.universe.trajectory[frame_index]
|
|
86
|
+
|
|
87
|
+
posi = sel.positions[:, axid]
|
|
88
|
+
|
|
89
|
+
posi = posi % box[axid]
|
|
90
|
+
|
|
91
|
+
count = np.histogram(posi, bins=bins, range=[0, box[axid]])[0]
|
|
92
|
+
|
|
93
|
+
return count
|
|
94
|
+
|
|
95
|
+
if atom_types == "all":
|
|
96
|
+
atom_types = np.unique(universe.atoms.types)
|
|
97
|
+
|
|
98
|
+
for t in atom_types:
|
|
99
|
+
print(f"Compute 1D atomic density of {t} atoms...")
|
|
100
|
+
|
|
101
|
+
sel = universe.select_atoms(f"type {t}")
|
|
102
|
+
|
|
103
|
+
# nframes = len(universe.trajectory[start:end])
|
|
104
|
+
|
|
105
|
+
# job_list = []
|
|
106
|
+
# for frame_index in tqdm( range(nframes) ):
|
|
107
|
+
# job_list.append(dask.delayed(count_pframe)(frame_index, sel))
|
|
108
|
+
|
|
109
|
+
frames = (
|
|
110
|
+
range(len(universe.trajectory))[start:end]
|
|
111
|
+
if end != -1
|
|
112
|
+
else range(len(universe.trajectory))[start:]
|
|
113
|
+
)
|
|
114
|
+
nframes = len(frames)
|
|
115
|
+
|
|
116
|
+
if nframes == 0:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
"Le slice de la trajectoire [start:end] ne contient aucune frame."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
job_list = []
|
|
122
|
+
for frame_index in tqdm(frames):
|
|
123
|
+
job_list.append(dask.delayed(count_pframe)(frame_index, sel))
|
|
124
|
+
|
|
125
|
+
result = dask.compute(job_list)
|
|
126
|
+
atom_count = np.sum(result[0], axis=0)
|
|
127
|
+
|
|
128
|
+
density = atom_count / slice_vol / nframes * 1000
|
|
129
|
+
|
|
130
|
+
density_total.append(density)
|
|
131
|
+
|
|
132
|
+
columns.append(f"{t}")
|
|
133
|
+
|
|
134
|
+
return pd.DataFrame(np.array(density_total).T, columns=columns, index=pos)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def density_map(
|
|
138
|
+
univ: mda.Universe,
|
|
139
|
+
atom_types: str | list[str | int],
|
|
140
|
+
interface_coordinate: float,
|
|
141
|
+
axis: str = "z",
|
|
142
|
+
eps: float = 3.0,
|
|
143
|
+
start: int = 0,
|
|
144
|
+
end: int = -1,
|
|
145
|
+
bin_size: float = 0.1,
|
|
146
|
+
) -> pd.DataFrame:
|
|
147
|
+
"""Create a 2D density map of atoms within a specified distance of an interface.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
univ : mda.Universe
|
|
152
|
+
The input MDAnalysis Universe to analyze.
|
|
153
|
+
atom_types : str or list
|
|
154
|
+
Atom types to consider.
|
|
155
|
+
interface_coordinate : float
|
|
156
|
+
Interface coordinate along the given axis.
|
|
157
|
+
axis : str
|
|
158
|
+
Axis parallel to which to calculate the density.
|
|
159
|
+
eps : float
|
|
160
|
+
Half-thickness of the slab: atoms between ``interface_coordinate -
|
|
161
|
+
eps`` and ``interface_coordinate + eps`` are counted.
|
|
162
|
+
start : int
|
|
163
|
+
Starting trajectory frame.
|
|
164
|
+
end : int
|
|
165
|
+
Ending trajectory frame.
|
|
166
|
+
bin_size : float
|
|
167
|
+
Size of bins for the histogram in Angstroms.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
pd.DataFrame
|
|
172
|
+
2D average density map.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
box = univ.dimensions
|
|
176
|
+
|
|
177
|
+
type_str = " ".join(atom_types) if isinstance(atom_types, list) else atom_types
|
|
178
|
+
|
|
179
|
+
# A slab of half-thickness `eps` centred on the interface, which is what
|
|
180
|
+
# "within a distance of an interface" means and what this function is
|
|
181
|
+
# for -- the adsorbed layer, not everything on one side of the plane.
|
|
182
|
+
# The selection used to be `prop axis < interface + eps`, i.e. the whole
|
|
183
|
+
# half-cell below: on a solid-liquid system with the solid underneath,
|
|
184
|
+
# that swept the entire solid into a map meant to show the liquid.
|
|
185
|
+
lower = interface_coordinate - eps
|
|
186
|
+
upper = interface_coordinate + eps
|
|
187
|
+
sel = univ.select_atoms(
|
|
188
|
+
f"type {type_str} and prop {axis} > {lower} and prop {axis} < {upper}",
|
|
189
|
+
updating=True,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
ids = _get_axis_ids(axis)
|
|
193
|
+
axid = ids["axid"]
|
|
194
|
+
axida, axidb = ids["axida"], ids["axidb"]
|
|
195
|
+
bins_a = np.arange(0, box[axida], bin_size)
|
|
196
|
+
bins_b = np.arange(0, box[axidb], bin_size)
|
|
197
|
+
|
|
198
|
+
# A bin of the map is a column through the selected slab: bin_size by
|
|
199
|
+
# bin_size in the plane, and as deep as the selection reaches along the
|
|
200
|
+
# perpendicular axis. The 1D slab volume used before
|
|
201
|
+
# (bin_size * box[a] * box[b]) is the volume of a whole slice, so the
|
|
202
|
+
# reported densities were low by box[a] * box[b] / (bin_size * depth) --
|
|
203
|
+
# a factor of 40 on a 20 A box at bin_size 0.5, and one that changes
|
|
204
|
+
# with the box and the binning rather than being a constant offset.
|
|
205
|
+
# It went unnoticed because a density map is read for its contrast.
|
|
206
|
+
slab_depth = min(upper, float(box[axid])) - max(lower, 0.0)
|
|
207
|
+
column_vol = bin_size * bin_size * slab_depth
|
|
208
|
+
|
|
209
|
+
nframes = len(univ.trajectory[start:end])
|
|
210
|
+
|
|
211
|
+
print(f"Compute 2D atomic density of {type_str} atoms...")
|
|
212
|
+
|
|
213
|
+
pos_a_list, pos_b_list = [], []
|
|
214
|
+
for ts in tqdm(univ.trajectory[start:end]):
|
|
215
|
+
posi, posj = sel.positions[:, axida], sel.positions[:, axidb]
|
|
216
|
+
|
|
217
|
+
posi = sel.positions[:, axida] % box[axida]
|
|
218
|
+
posj = sel.positions[:, axidb] % box[axidb]
|
|
219
|
+
|
|
220
|
+
pos_a_list.append(posi)
|
|
221
|
+
pos_b_list.append(posj)
|
|
222
|
+
|
|
223
|
+
pos_a = np.concatenate(pos_a_list)
|
|
224
|
+
pos_b = np.concatenate(pos_b_list)
|
|
225
|
+
|
|
226
|
+
hist, edges_a, edges_b = np.histogram2d(pos_a, pos_b, bins=(bins_a, bins_b))
|
|
227
|
+
|
|
228
|
+
ra = (edges_a[1:] + edges_a[:-1]) / 2
|
|
229
|
+
rb = (edges_b[1:] + edges_b[:-1]) / 2
|
|
230
|
+
|
|
231
|
+
density = hist / column_vol / nframes * 1000
|
|
232
|
+
|
|
233
|
+
return pd.DataFrame(density, index=ra, columns=rb)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def find_interfaces_coordinates(
|
|
237
|
+
input_df: pd.DataFrame, solid_types: list[str | int], liquid_types: list[str | int]
|
|
238
|
+
) -> tuple[float, float, float, float]:
|
|
239
|
+
"""Calculate the solid/liquid interface coordinates for an interfacial system.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
input_df : pd.DataFrame
|
|
244
|
+
Input DataFrame of 1D atomic density profiles.
|
|
245
|
+
solid_types : list
|
|
246
|
+
List of atom types composing the solid phase.
|
|
247
|
+
liquid_types : list
|
|
248
|
+
List of atom types composing the liquid phase.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
tuple
|
|
253
|
+
Left liquid interface, left solid interface, right solid interface,
|
|
254
|
+
and right liquid interface coordinates.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
solid_series = pd.Series(input_df[solid_types].max(axis=1), index=input_df.index)
|
|
258
|
+
liquid_series = pd.Series(input_df[liquid_types].max(axis=1), index=input_df.index)
|
|
259
|
+
|
|
260
|
+
# On the left
|
|
261
|
+
solid_left = (solid_series == 0).idxmax()
|
|
262
|
+
solution_left = (liquid_series != 0).idxmax()
|
|
263
|
+
|
|
264
|
+
# On the right
|
|
265
|
+
df_solide_r = solid_series[::-1]
|
|
266
|
+
df_solution = liquid_series.loc[solution_left:]
|
|
267
|
+
solid_right = (df_solide_r == 0).idxmax()
|
|
268
|
+
solution_right = (df_solution == 0).idxmax()
|
|
269
|
+
|
|
270
|
+
return solution_left, solid_left, solid_right, solution_right
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def shift_profile(
|
|
274
|
+
input_df: pd.DataFrame, shift: float, csv_output: str = "new_density_profile.csv"
|
|
275
|
+
):
|
|
276
|
+
"""Shift the coordinates of a 1D density profile.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
input_df : pd.DataFrame
|
|
281
|
+
Input DataFrame of 1D atomic density profiles.
|
|
282
|
+
shift : float
|
|
283
|
+
Positive or negative shift in Angstroms.
|
|
284
|
+
csv_output : str
|
|
285
|
+
Output CSV file path.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
pd.DataFrame
|
|
290
|
+
Shifted density profile data.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
# to avoid pylint warning
|
|
294
|
+
# pylint: disable=E1101
|
|
295
|
+
columns = input_df.columns
|
|
296
|
+
|
|
297
|
+
r = input_df.index.values
|
|
298
|
+
dr = r[1] - r[0]
|
|
299
|
+
densities = input_df.values
|
|
300
|
+
|
|
301
|
+
idx = int(shift / dr)
|
|
302
|
+
new_densities = np.roll(densities, idx, axis=0)
|
|
303
|
+
|
|
304
|
+
new_data = np.hstack((r.reshape(r.size, 1), new_densities))
|
|
305
|
+
|
|
306
|
+
output_df = pd.DataFrame(new_data, columns=columns)
|
|
307
|
+
|
|
308
|
+
if csv_output is not None:
|
|
309
|
+
output_df.to_csv(csv_output)
|
|
310
|
+
|
|
311
|
+
return output_df
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def electrostatic_potential(
|
|
315
|
+
input_df: pd.DataFrame, list_charges: list[float]
|
|
316
|
+
) -> tuple[pd.Series, pd.Series, pd.Series]:
|
|
317
|
+
"""Calculate charge distribution, electric field, and electrostatic potential.
|
|
318
|
+
|
|
319
|
+
Parameters
|
|
320
|
+
----------
|
|
321
|
+
input_df : pd.DataFrame
|
|
322
|
+
DataFrame of atomic densities.
|
|
323
|
+
list_charges : list
|
|
324
|
+
List of charges for each atom type in the CSV file.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
charge_density : pd.Series
|
|
329
|
+
Calculated charge density profile.
|
|
330
|
+
efield : pd.Series
|
|
331
|
+
Calculated electric field profile.
|
|
332
|
+
potential : pd.Series
|
|
333
|
+
Calculated electrostatic potential profile.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
def shift_constant(array: np.ndarray, threshold: float = 0.1) -> np.ndarray:
|
|
337
|
+
"""Shifts the array by subtracting the mean of its constant (flat) regions.
|
|
338
|
+
|
|
339
|
+
This function automatically detects stationary or flat regions within a 1D signal by analyzing relative consecutive differences. It computes the average intensity of the signal across these stable baselines and subtracts it from the entire array, effectively centering the baseline/background around zero.
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
array
|
|
344
|
+
The 1D input array containing the signal to be corrected.
|
|
345
|
+
threshold
|
|
346
|
+
The maximum relative difference between consecutive elements to consider a region as 'constant' or flat. Evaluated after scaling the array by its maximum value.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
np.ndarray: The baseline-corrected array where the flat regions
|
|
350
|
+
are centered around zero.
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
>>> signal = np.array([10.0, 10.1, 10.0, 25.0, 50.0, 10.2, 10.0])
|
|
354
|
+
>>> shift_constant(signal, threshold=0.02)
|
|
355
|
+
array([-0.025, 0.075, -0.025, 14.975, 39.975, 0.175, -0.025])
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
narr = array / array.max()
|
|
359
|
+
# Calculate the absolute differences between consecutive elements
|
|
360
|
+
differences = np.abs(np.diff(narr))
|
|
361
|
+
|
|
362
|
+
# Identify indices where the difference is within the threshold
|
|
363
|
+
constant_indices = np.where(differences <= threshold)[0]
|
|
364
|
+
|
|
365
|
+
# Adjust indices to include the next element in the array
|
|
366
|
+
constant_indices = np.append(constant_indices, constant_indices + 1)
|
|
367
|
+
|
|
368
|
+
# Get unique indices and sort them
|
|
369
|
+
constant_indices = np.unique(constant_indices)
|
|
370
|
+
|
|
371
|
+
result_array = array - np.mean(array[constant_indices])
|
|
372
|
+
|
|
373
|
+
return result_array
|
|
374
|
+
|
|
375
|
+
charge = np.array(list_charges)
|
|
376
|
+
|
|
377
|
+
r = input_df.index
|
|
378
|
+
dr = (r[1] - r[0]) * 1e-10
|
|
379
|
+
|
|
380
|
+
charge_density = np.sum(input_df.values * charge.T, axis=1)
|
|
381
|
+
charge_density_series = pd.Series(charge_density, index=r)
|
|
382
|
+
|
|
383
|
+
efield = (
|
|
384
|
+
integrate.cumulative_trapezoid(charge_density * 1.602e-19 * 1e27, dx=dr)
|
|
385
|
+
/ 8.854e-12
|
|
386
|
+
)
|
|
387
|
+
r = (r[1:] + r[:-1]) / 2
|
|
388
|
+
efield_series = pd.Series(shift_constant(efield), index=r)
|
|
389
|
+
|
|
390
|
+
potential = -integrate.cumulative_trapezoid(efield, dx=dr)
|
|
391
|
+
r = (r[1:] + r[:-1]) / 2
|
|
392
|
+
potential_series = pd.Series(shift_constant(potential), index=r)
|
|
393
|
+
|
|
394
|
+
return charge_density_series, efield_series, potential_series
|