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.
Files changed (158) hide show
  1. cemd/__init__.py +33 -0
  2. cemd/_constants.py +252 -0
  3. cemd/analysis/__init__.py +32 -0
  4. cemd/analysis/density.py +394 -0
  5. cemd/analysis/diffusion.py +645 -0
  6. cemd/analysis/rdf.py +132 -0
  7. cemd/analysis/silicates.py +236 -0
  8. cemd/analysis/tcf.py +222 -0
  9. cemd/analysis/util.py +254 -0
  10. cemd/analysis/velocities.py +60 -0
  11. cemd/build/__init__.py +20 -0
  12. cemd/build/_packmol.py +719 -0
  13. cemd/build/_structures/__init__.py +30 -0
  14. cemd/build/_structures/aft_moore.cif +60 -0
  15. cemd/build/_structures/co3.pdb +10 -0
  16. cemd/build/_structures/h2o.lt +160 -0
  17. cemd/build/_structures/ho.sdf +111 -0
  18. cemd/build/_structures/so4.pdb +15 -0
  19. cemd/build/_structures/tob11a_hamid.cif +89 -0
  20. cemd/build/_structures/tob11a_merlino.cif +104 -0
  21. cemd/build/base.py +86 -0
  22. cemd/build/cement_hydrates/__init__.py +3 -0
  23. cemd/build/cement_hydrates/_interlayer_helpers.py +185 -0
  24. cemd/build/cement_hydrates/_silicate_helpers.py +443 -0
  25. cemd/build/cement_hydrates/hydrates.py +651 -0
  26. cemd/build/glass.py +532 -0
  27. cemd/build/interface.py +331 -0
  28. cemd/build/solution.py +574 -0
  29. cemd/build/split.py +623 -0
  30. cemd/build/surface.py +366 -0
  31. cemd/core/__init__.py +0 -0
  32. cemd/core/_edit.py +574 -0
  33. cemd/core/_format.py +759 -0
  34. cemd/core/_io/__init__.py +20 -0
  35. cemd/core/_io/formats/__init__.py +0 -0
  36. cemd/core/_io/formats/base.py +39 -0
  37. cemd/core/_io/formats/cif.py +52 -0
  38. cemd/core/_io/formats/lammps.py +1023 -0
  39. cemd/core/_io/formats/lt.py +293 -0
  40. cemd/core/_io/formats/mda.py +630 -0
  41. cemd/core/_io/formats/pdb.py +275 -0
  42. cemd/core/_io/formats/pmg.py +146 -0
  43. cemd/core/_io/formats/sdf.py +139 -0
  44. cemd/core/_io/formats/smiles.py +113 -0
  45. cemd/core/_io/io_mixin.py +349 -0
  46. cemd/core/_io/sources/cod.py +368 -0
  47. cemd/core/_io/sources/pubchem.py +329 -0
  48. cemd/core/_view/__init__.py +3 -0
  49. cemd/core/_view/config.py +119 -0
  50. cemd/core/_view/default_vmd_config.toml +114 -0
  51. cemd/core/_view/view.py +391 -0
  52. cemd/core/_view/view.tcl +126 -0
  53. cemd/core/atomic_system.py +1136 -0
  54. cemd/core/forcefield_classes.py +183 -0
  55. cemd/core/forcefield_mixin.py +924 -0
  56. cemd/core/topology_mixin.py +637 -0
  57. cemd/forcefield/__init__.py +31 -0
  58. cemd/forcefield/_config.py +116 -0
  59. cemd/forcefield/_editor.py +282 -0
  60. cemd/forcefield/_parsers/__init__.py +0 -0
  61. cemd/forcefield/_parsers/_base.py +187 -0
  62. cemd/forcefield/_parsers/_gromos.py +290 -0
  63. cemd/forcefield/_parsers/_iff_charmm.py +707 -0
  64. cemd/forcefield/_parsers/_iff_cvff.py +496 -0
  65. cemd/forcefield/_parsers/_toml.py +189 -0
  66. cemd/forcefield/db/GROMOS_54A7_ATB.lt +4702 -0
  67. cemd/forcefield/db/charmm27_interface_v1_5.prm +4123 -0
  68. cemd/forcefield/db/clayff.toml +289 -0
  69. cemd/forcefield/db/cshff2014.toml +186 -0
  70. cemd/forcefield/db/cvff_interface_v1_5.frc +5124 -0
  71. cemd/forcefield/db/guillot2007.toml +130 -0
  72. cemd/forcefield/db/iff.toml +264 -0
  73. cemd/forcefield/db/raiteri2015.toml +179 -0
  74. cemd/forcefield/db/solcon.toml +73 -0
  75. cemd/forcefield/db/spc.toml +91 -0
  76. cemd/forcefield/db/spc_fw.toml +48 -0
  77. cemd/forcefield/db/spce.toml +64 -0
  78. cemd/forcefield/db/sperinck.toml +146 -0
  79. cemd/forcefield/db/tip3p.toml +60 -0
  80. cemd/forcefield/db/tip4p2005.toml +64 -0
  81. cemd/forcefield/db/tip4p_ew.toml +64 -0
  82. cemd/forcefield/db/wang2008.toml +86 -0
  83. cemd/forcefield/forcefield_database.py +599 -0
  84. cemd/forcefield/models.py +327 -0
  85. cemd/gui/__init__.py +0 -0
  86. cemd/gui/_userdata.py +61 -0
  87. cemd/gui/default_config.json +143 -0
  88. cemd/gui/icons/add_mol.svg +70 -0
  89. cemd/gui/icons/atom.svg +54 -0
  90. cemd/gui/icons/bg-color.svg +1 -0
  91. cemd/gui/icons/camera.svg +1 -0
  92. cemd/gui/icons/cash.svg +116 -0
  93. cemd/gui/icons/channel.svg +90 -0
  94. cemd/gui/icons/cod.svg +163 -0
  95. cemd/gui/icons/connectivity.svg +1 -0
  96. cemd/gui/icons/droplet.svg +77 -0
  97. cemd/gui/icons/file-pdf.svg +1 -0
  98. cemd/gui/icons/glass.svg +110 -0
  99. cemd/gui/icons/interface.svg +81 -0
  100. cemd/gui/icons/interface2.svg +55 -0
  101. cemd/gui/icons/items-grid.svg +1 -0
  102. cemd/gui/icons/minus-circle.svg +1 -0
  103. cemd/gui/icons/minus-square-white.svg +63 -0
  104. cemd/gui/icons/minus-square.svg +61 -0
  105. cemd/gui/icons/minus.svg +60 -0
  106. cemd/gui/icons/mol.svg +60 -0
  107. cemd/gui/icons/move.svg +1 -0
  108. cemd/gui/icons/open.svg +1 -0
  109. cemd/gui/icons/orthogonalize.svg +53 -0
  110. cemd/gui/icons/paste.svg +1 -0
  111. cemd/gui/icons/plus-circle.svg +1 -0
  112. cemd/gui/icons/plus-square-white.svg +61 -0
  113. cemd/gui/icons/plus-square.svg +59 -0
  114. cemd/gui/icons/plus.svg +48 -0
  115. cemd/gui/icons/protonate.svg +87 -0
  116. cemd/gui/icons/pubchem.svg +56 -0
  117. cemd/gui/icons/pycsh.svg +142 -0
  118. cemd/gui/icons/question.svg +1 -0
  119. cemd/gui/icons/rdf.svg +44 -0
  120. cemd/gui/icons/reference.svg +1 -0
  121. cemd/gui/icons/replicate.svg +1 -0
  122. cemd/gui/icons/save-all.svg +1 -0
  123. cemd/gui/icons/save.svg +1 -0
  124. cemd/gui/icons/search.svg +1 -0
  125. cemd/gui/icons/silicate.svg +121 -0
  126. cemd/gui/icons/solution.svg +130 -0
  127. cemd/gui/icons/square.svg +1 -0
  128. cemd/gui/icons/surface.svg +57 -0
  129. cemd/gui/icons/trash.svg +1 -0
  130. cemd/gui/icons/wall.svg +10 -0
  131. cemd/gui/icons/wrap.svg +67 -0
  132. cemd/gui/icons/zap.svg +60 -0
  133. cemd/gui/logic/__init__.py +0 -0
  134. cemd/gui/logic/build.py +369 -0
  135. cemd/gui/logic/file_handler.py +127 -0
  136. cemd/gui/main_window.py +1060 -0
  137. cemd/gui/plotter_widget.py +410 -0
  138. cemd/gui/tabs.py +452 -0
  139. cemd/gui/ui/__init__.py +0 -0
  140. cemd/gui/ui/analysis_view.py +368 -0
  141. cemd/gui/ui/atom_table.py +139 -0
  142. cemd/gui/ui/base_dialog.py +144 -0
  143. cemd/gui/ui/build.py +1540 -0
  144. cemd/gui/ui/cod.py +256 -0
  145. cemd/gui/ui/gui_utils.py +85 -0
  146. cemd/gui/ui/managers.py +1050 -0
  147. cemd/gui/ui/panels.py +539 -0
  148. cemd/gui/ui/pubchem.py +216 -0
  149. cemd/topology/__init__.py +27 -0
  150. cemd/topology/_apply.py +253 -0
  151. cemd/topology/predefined.py +76 -0
  152. cemd/topology/rules.py +176 -0
  153. cemd-0.1.0.dist-info/METADATA +263 -0
  154. cemd-0.1.0.dist-info/RECORD +158 -0
  155. cemd-0.1.0.dist-info/WHEEL +5 -0
  156. cemd-0.1.0.dist-info/entry_points.txt +2 -0
  157. cemd-0.1.0.dist-info/licenses/LICENCE +674 -0
  158. 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
+ ]
@@ -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