fastatomstruct 0.8.1__cp310-cp310-win_amd64.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.
- fastatomstruct/__init__.py +799 -0
- fastatomstruct/__init__.pyi +267 -0
- fastatomstruct/fastatomstruct.cp310-win_amd64.pyd +0 -0
- fastatomstruct/fastatomstruct.pyi +1595 -0
- fastatomstruct-0.8.1.dist-info/METADATA +95 -0
- fastatomstruct-0.8.1.dist-info/RECORD +8 -0
- fastatomstruct-0.8.1.dist-info/WHEEL +4 -0
- fastatomstruct-0.8.1.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
3
|
+
from time import time
|
|
4
|
+
|
|
5
|
+
import ase
|
|
6
|
+
import numpy as np
|
|
7
|
+
from .fastatomstruct import *
|
|
8
|
+
from scipy.integrate import simpson as simps
|
|
9
|
+
from scipy.fftpack import fft
|
|
10
|
+
from tidynamics import msd
|
|
11
|
+
|
|
12
|
+
rank = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_root() -> bool:
|
|
16
|
+
"""Check if the current process is the root process."""
|
|
17
|
+
if rank is None:
|
|
18
|
+
return True
|
|
19
|
+
return rank == 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def chunks(indices: List[int], n: int):
|
|
23
|
+
"""Yield successive chunks from list of atoms."""
|
|
24
|
+
for i in range(0, len(indices), n):
|
|
25
|
+
yield indices[i : i + n]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ipar(
|
|
29
|
+
func: Callable, atoms: List[ase.Atoms], *args: List[Any], **kwargs: Dict[Any, Any]
|
|
30
|
+
) -> List[Any]:
|
|
31
|
+
"""Image-based parallelization.
|
|
32
|
+
|
|
33
|
+
The calculation of many structural quantities is parallelized over atoms.
|
|
34
|
+
However, image-based parallelization (or a mixture of both) can be more efficient
|
|
35
|
+
in some cases. This function makes using this parallelization layer quite easy.
|
|
36
|
+
You can just use it as a simple wrapper around other functions implemented in
|
|
37
|
+
`fastatomstruct` (see example below). **This function needs a working installation
|
|
38
|
+
of the MPI4Py package!** You can set the `RAYON_NUM_THREADS` enviroment variable
|
|
39
|
+
to control the number of threads.
|
|
40
|
+
|
|
41
|
+
Arguments:
|
|
42
|
+
func (Callable): Some function from the `fastatomstruct` package
|
|
43
|
+
atoms (List[ase.Atoms]): List of ASE configurations
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of results; type depends on the output of func
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
|
|
50
|
+
The exemplary file "Sb-1.00-300-100.traj" `can be found here <https://zivgitlab.uni-muenster.de/ag-salinga/fastatomstruct/-/raw/master/regressiontesting/Structures/Sb-1.00-300-100.traj>`_.
|
|
51
|
+
The code below needs to be run e.g. with `mpirun -n 2 test.py`. In this case,
|
|
52
|
+
two processes will be used.
|
|
53
|
+
|
|
54
|
+
.. plot::
|
|
55
|
+
|
|
56
|
+
>>> import matplotlib.pyplot as plt
|
|
57
|
+
>>> import fastatomstruct as fs
|
|
58
|
+
>>> from ase import io
|
|
59
|
+
>>> if fs.is_root():
|
|
60
|
+
>>> atoms = io.read("Sb-1.00-300-100.traj", index="::200")
|
|
61
|
+
>>> q = fs.ipar(fs.q_tetrahedral, atoms, 3.2, 3)
|
|
62
|
+
>>> if fs.is_root():
|
|
63
|
+
>>> plt.hist(q[0], bins=25, alpha=0.5)
|
|
64
|
+
>>> plt.hist(q[1], bins=25, color="C3", alpha=0.5)
|
|
65
|
+
"""
|
|
66
|
+
from mpi4py import MPI
|
|
67
|
+
|
|
68
|
+
comm = MPI.COMM_WORLD
|
|
69
|
+
global rank
|
|
70
|
+
rank = comm.Get_rank()
|
|
71
|
+
|
|
72
|
+
if rank == 0:
|
|
73
|
+
print("\nImage parallelization report\n" + "----------------------------")
|
|
74
|
+
|
|
75
|
+
if rank == 0:
|
|
76
|
+
res_full, t0_f = [], time()
|
|
77
|
+
|
|
78
|
+
for i, c in enumerate(chunks(list(range(len(atoms))), comm.size)):
|
|
79
|
+
while len(c) < comm.size:
|
|
80
|
+
c.append(None)
|
|
81
|
+
|
|
82
|
+
if rank == 0:
|
|
83
|
+
per = (len(c) - np.count_nonzero([ci is None for ci in c])) / len(c)
|
|
84
|
+
t0 = time()
|
|
85
|
+
if c[rank] is not None:
|
|
86
|
+
a = atoms[c[rank]]
|
|
87
|
+
res = func(a, *args, **kwargs)
|
|
88
|
+
else:
|
|
89
|
+
res = None
|
|
90
|
+
|
|
91
|
+
res = comm.gather(res, root=0)
|
|
92
|
+
if rank == 0:
|
|
93
|
+
t = time() - t0
|
|
94
|
+
print(f"Chunk {i + 1}: {per * 100:03.2f}% | {t:.2f} s")
|
|
95
|
+
res_full.extend(res)
|
|
96
|
+
|
|
97
|
+
if rank == 0:
|
|
98
|
+
for i in range(len(res_full) - 1, -1, -1):
|
|
99
|
+
if res_full[i] is None:
|
|
100
|
+
del res_full[i]
|
|
101
|
+
print("----------------------------")
|
|
102
|
+
print(f"Total: {(time() - t0_f):.2f} s\n")
|
|
103
|
+
else:
|
|
104
|
+
res_full = None
|
|
105
|
+
|
|
106
|
+
return res_full
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def static_structure_factor(
|
|
110
|
+
atoms: Union[ase.Atoms, List[ase.Atoms]],
|
|
111
|
+
q: np.ndarray,
|
|
112
|
+
r_max: float,
|
|
113
|
+
n_bins: int,
|
|
114
|
+
filter: Optional[Filter] = None,
|
|
115
|
+
) -> np.ndarray:
|
|
116
|
+
"""Static structure factor, as calculated from the RDF.
|
|
117
|
+
|
|
118
|
+
For isotropic systems, the static structure factor can be calculated using
|
|
119
|
+
|
|
120
|
+
.. math::
|
|
121
|
+
|
|
122
|
+
S(q) = q + 4 \pi \\rho \int_0^\infty r (g(r) - 1) \\frac{\sin{qr}}{q} dr,
|
|
123
|
+
|
|
124
|
+
with :math:`q` the absolute value of the reciprocal vector and :math:`g(r)`
|
|
125
|
+
the radial distribution function.
|
|
126
|
+
|
|
127
|
+
Arguments:
|
|
128
|
+
atoms (Union[ase.Atoms, List[ase.Atoms]]): Atoms object(s) from ASE
|
|
129
|
+
q (np.ndarray): Array with values of :math:`q`
|
|
130
|
+
r_max (float): Cutoff radius for calculating the radial distribution function
|
|
131
|
+
n_bins (int): Number of bins for calculating the radial distribution function
|
|
132
|
+
filter (Optional[fastatomstruct.Filter]): Filter applied to the atoms
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
np.ndarray of floats with values of :math:`S(q)`
|
|
136
|
+
|
|
137
|
+
Examples
|
|
138
|
+
--------
|
|
139
|
+
|
|
140
|
+
The exemplary file "Sb540.traj" `can be found here <https://zivgitlab.uni-muenster.de/ag-salinga/fastatomstruct/-/raw/master/regressiontesting/Structures/Sb540.traj>`_.
|
|
141
|
+
|
|
142
|
+
.. plot::
|
|
143
|
+
|
|
144
|
+
>>> import matplotlib.pyplot as plt
|
|
145
|
+
>>> import fastatomstruct as fs
|
|
146
|
+
>>> import numpy as np
|
|
147
|
+
>>> from ase import io
|
|
148
|
+
>>> atoms = io.read("Sb540.traj")
|
|
149
|
+
>>> q = np.linspace(0.1, 10, 100)
|
|
150
|
+
>>> s = fs.static_structure_factor(atoms, q, 10, 100)
|
|
151
|
+
>>> plt.plot(q, s)
|
|
152
|
+
>>> plt.xlabel(r"$q$")
|
|
153
|
+
>>> plt.ylabel(r"$S(q)$")
|
|
154
|
+
>>> plt.xlim(0, 10)
|
|
155
|
+
"""
|
|
156
|
+
if isinstance(atoms, list):
|
|
157
|
+
rdf = []
|
|
158
|
+
for a in atoms:
|
|
159
|
+
r, rdf_i = radial_distribution_function(a, r_max, n_bins, filter)
|
|
160
|
+
rdf.append(rdf_i)
|
|
161
|
+
rdf = np.mean(rdf, axis=0)
|
|
162
|
+
rho = len(atoms[0]) / atoms[0].get_volume()
|
|
163
|
+
else:
|
|
164
|
+
r, rdf = radial_distribution_function(atoms, r_max, n_bins)
|
|
165
|
+
rho = len(atoms) / atoms.get_volume()
|
|
166
|
+
|
|
167
|
+
integral = np.zeros(len(q))
|
|
168
|
+
for i, qi in enumerate(q):
|
|
169
|
+
integrand = r[1:] * np.sin(qi * r[1:]) * (rdf[1:] - 1)
|
|
170
|
+
integral[i] = np.trapz(integrand, r[1:])
|
|
171
|
+
return 1 + 4 * np.pi * rho / q * integral
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def __convert_pos(atoms: List[ase.Atoms]) -> List[np.ndarray]:
|
|
175
|
+
pos = []
|
|
176
|
+
for i in range(len(atoms[0])):
|
|
177
|
+
pos_temp = np.empty((len(atoms), 3))
|
|
178
|
+
for j, a in enumerate(atoms):
|
|
179
|
+
pos_temp[j, :] = a.positions[i, :]
|
|
180
|
+
pos.append(pos_temp)
|
|
181
|
+
return pos
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def mean_squared_displacement(atoms: List[ase.Atoms]) -> np.ndarray:
|
|
185
|
+
"""Mean squared displacment of a trajectory.
|
|
186
|
+
|
|
187
|
+
The MSD is calculated using the `tidynamics` package. It is defined as
|
|
188
|
+
|
|
189
|
+
.. math::
|
|
190
|
+
|
|
191
|
+
\\mathrm{MSD} = \\frac{1}{N} \\sum_{i = 1}^N |\\vec{x}_i(t) - \\vec{x}_i(0)|^2,
|
|
192
|
+
|
|
193
|
+
where the :math:`\\vec{x}_i(t)` are the atomic positions at time :math:`t`.
|
|
194
|
+
|
|
195
|
+
Arguments:
|
|
196
|
+
atoms (List[ase.Atoms]): Trajectory, atoms objects from ASE
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
NumPy Array of floats containing the mean squared displacement
|
|
200
|
+
|
|
201
|
+
Examples
|
|
202
|
+
--------
|
|
203
|
+
|
|
204
|
+
The exemplary file "Sb-1.00-300-100.traj" `can be found here <https://zivgitlab.uni-muenster.de/ag-salinga/fastatomstruct/-/raw/master/regressiontesting/Structures/Sb-1.00-300-100.traj>`_.
|
|
205
|
+
|
|
206
|
+
.. plot::
|
|
207
|
+
|
|
208
|
+
>>> import matplotlib.pyplot as plt
|
|
209
|
+
>>> import fastatomstruct as fs
|
|
210
|
+
>>> import numpy as np
|
|
211
|
+
>>> from ase import io
|
|
212
|
+
>>> atoms = io.read("Sb-1.00-300-100.traj", index=":")
|
|
213
|
+
>>> time = np.arange(0, len(atoms)) * 200 / 1000
|
|
214
|
+
>>> msd = fs.mean_squared_displacement(atoms)
|
|
215
|
+
>>> plt.loglog(time, msd)
|
|
216
|
+
>>> plt.xlabel(r"$t$ (ps)")
|
|
217
|
+
>>> plt.ylabel(r"$\langle r^2 \rangle - \langle r \rangle^2$ ($\mathrm{\AA^2}$)")
|
|
218
|
+
"""
|
|
219
|
+
return squared_displacement(atoms).mean(axis=0)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def squared_displacement(atoms: List[ase.Atoms]) -> np.ndarray:
|
|
223
|
+
"""Squared displacment of a trajectory.
|
|
224
|
+
|
|
225
|
+
The SD is calculated using the `tidynamics` package. It is defined as
|
|
226
|
+
|
|
227
|
+
.. math::
|
|
228
|
+
|
|
229
|
+
\\mathrm{SD}_i = |\vec{x}_i(t) - \vec{x}_i(0)|^2,
|
|
230
|
+
|
|
231
|
+
where the :math:`\vec{x}_i(t)` are the atomic positions at time :math:`t`.
|
|
232
|
+
|
|
233
|
+
Arguments:
|
|
234
|
+
atoms (List[ase.Atoms]): Trajectory, atoms objects from ASE
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
NumPy Array of floats containing the mean squared displacement
|
|
238
|
+
|
|
239
|
+
Examples
|
|
240
|
+
--------
|
|
241
|
+
|
|
242
|
+
The exemplary file "Sb-1.00-300-100.traj" `can be found here <https://zivgitlab.uni-muenster.de/ag-salinga/fastatomstruct/-/raw/master/regressiontesting/Structures/Sb-1.00-300-100.traj>`_.
|
|
243
|
+
|
|
244
|
+
>>> import fastatomstruct as fs
|
|
245
|
+
>>> from ase import io
|
|
246
|
+
>>> atoms = io.read("Sb-1.00-300-100.traj", index=":")
|
|
247
|
+
>>> fs.mean_squared_displacement(atoms)
|
|
248
|
+
array([[-2.91038305e-11, 8.38161177e-02, 1.25422533e-01, ...,
|
|
249
|
+
6.15651363e-01, 5.27809637e-01, 6.19426924e-01],
|
|
250
|
+
[ 0.00000000e+00, 9.27498088e-02, 1.52887610e-01, ...,
|
|
251
|
+
1.07754904e+00, 1.10512104e+00, 1.46645886e+00],
|
|
252
|
+
[-1.16415322e-10, 8.61089322e-02, 1.37081905e-01, ...,
|
|
253
|
+
1.27085909e+00, 1.18840340e+00, 9.64727558e-01],
|
|
254
|
+
...,
|
|
255
|
+
[-4.65661287e-10, 9.55223907e-02, 1.56593970e-01, ...,
|
|
256
|
+
4.45991428e-01, 2.80684893e-01, 1.91919705e-01],
|
|
257
|
+
[ 0.00000000e+00, 8.69368854e-02, 1.34933834e-01, ...,
|
|
258
|
+
4.86719506e-01, 3.47152810e-01, 5.66086433e-01],
|
|
259
|
+
[-9.31322575e-10, 8.86508557e-02, 1.46782934e-01, ...,
|
|
260
|
+
6.20435820e-01, 7.15149704e-01, 1.23890466e+00]])
|
|
261
|
+
"""
|
|
262
|
+
pos = __convert_pos(atoms)
|
|
263
|
+
m = [msd(p) for p in pos]
|
|
264
|
+
return np.array(m)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class VDOSMethod(Enum):
|
|
268
|
+
DIRECT = 1
|
|
269
|
+
VACF = 2
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
"""Large parts of the code below are taken from the pwtools package,
|
|
273
|
+
which is licensed under the BSD 3-Clause License.
|
|
274
|
+
|
|
275
|
+
The original code can be found here: https://github.com/elcorto/pwtools
|
|
276
|
+
|
|
277
|
+
The original license is as follows:
|
|
278
|
+
|
|
279
|
+
Copyright (c) Steve Schmerler <git@elcorto.com>
|
|
280
|
+
All rights reserved.
|
|
281
|
+
|
|
282
|
+
Redistribution and use in source and binary forms, with or without
|
|
283
|
+
modification, are permitted provided that the following conditions are met:
|
|
284
|
+
|
|
285
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
286
|
+
list of conditions and the following disclaimer.
|
|
287
|
+
|
|
288
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
289
|
+
this list of conditions and the following disclaimer in the documentation
|
|
290
|
+
and/or other materials provided with the distribution.
|
|
291
|
+
|
|
292
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
293
|
+
may be used to endorse or promote products derived from this software
|
|
294
|
+
without specific prior written permission.
|
|
295
|
+
|
|
296
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
297
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
298
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
299
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
300
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
301
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
302
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
303
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
304
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
305
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def vibrational_dos(
|
|
310
|
+
atoms: List[ase.Atoms],
|
|
311
|
+
timestep: float,
|
|
312
|
+
full_out: bool = False,
|
|
313
|
+
method: VDOSMethod = VDOSMethod.DIRECT,
|
|
314
|
+
area: float = 1.0,
|
|
315
|
+
use_masses: bool = True,
|
|
316
|
+
window: bool = True,
|
|
317
|
+
mirror: bool = False,
|
|
318
|
+
npad: Optional[int] = None,
|
|
319
|
+
to_next: bool = False,
|
|
320
|
+
):
|
|
321
|
+
"""Phonon DOS by direct FFT of velocities, or FFT of the VACF.
|
|
322
|
+
|
|
323
|
+
Integral area is normalized to `area`. It is possible (and recommended) to
|
|
324
|
+
zero-pad the velocities (see `npad`).
|
|
325
|
+
|
|
326
|
+
Arguments:
|
|
327
|
+
atoms (List[ase.Atoms]): Atoms object(s) from ASE
|
|
328
|
+
timestep (float): Time step
|
|
329
|
+
full_out (bool): Whether all output should be given, or just frequency and PDOS
|
|
330
|
+
method (fastatomstruct.VDOSMethod): Method for the DOS calculation (VDOSMethod.DIRECT or VDOSMethod.VACF). PDOSMethod.DIRECT is recommended.
|
|
331
|
+
area (float): Normalize area under frequency-PDOS curve to this value
|
|
332
|
+
use_masses (bool): Whether to use masses in the calculation (default: True)
|
|
333
|
+
window (bool): Use Welch windowing on data before FFT (reduces leaking effect,
|
|
334
|
+
recommended, default is True)
|
|
335
|
+
mirror (bool): Mirror one-sided VACF at t=0 before FFT (default: False)
|
|
336
|
+
to_next (bool): method=PDOSMethod.DIRECT only: Pad `vel` with zeros along `axis` up to the next power of two after the array length determined by `npad`. This gives you speed, but variable (better) frequency resolution. Default: False
|
|
337
|
+
|
|
338
|
+
Returns
|
|
339
|
+
-------
|
|
340
|
+
if full_out = False
|
|
341
|
+
| ``(faxis, pdos)``
|
|
342
|
+
| faxis : 1d array [1/unit(dt)]
|
|
343
|
+
| pdos : 1d array, the phonon DOS, normalized to `area`
|
|
344
|
+
if full_out = True
|
|
345
|
+
| if method == 'direct':
|
|
346
|
+
| ``(faxis, pdos, (full_faxis, full_pdos, split_idx))``
|
|
347
|
+
| if method == 'vavcf':
|
|
348
|
+
| ``(faxis, pdos, (full_faxis, full_pdos, split_idx, vacf, fft_vacf))``
|
|
349
|
+
| fft_vacf : 1d complex array, result of fft(vacf) or fft(mirror(vacf))
|
|
350
|
+
| vacf : 1d array, the VACF
|
|
351
|
+
|
|
352
|
+
Examples
|
|
353
|
+
--------
|
|
354
|
+
|
|
355
|
+
.. plot::
|
|
356
|
+
|
|
357
|
+
>>> import matplotlib.pyplot as plt
|
|
358
|
+
>>> import fastatomstruct as fs
|
|
359
|
+
>>> from ase import io
|
|
360
|
+
>>> atoms = io.read("SbViscosity-1000K.traj", index=":")
|
|
361
|
+
>>> freq, pdos = fs.vibrational_dos(atoms, 50)
|
|
362
|
+
>>> freq *= 1000
|
|
363
|
+
>>> plt.plot(freq, pdos)
|
|
364
|
+
>>> freq, pdos = fs.vibrational_dos(atoms, 50, method=fs.VDOSMethod.VACF)
|
|
365
|
+
>>> freq *= 1000
|
|
366
|
+
>>> plt.plot(freq, pdos, color="C3")
|
|
367
|
+
>>> plt.xlim(freq.min(), freq.max())
|
|
368
|
+
>>> plt.ylim(0, 350)
|
|
369
|
+
>>> plt.xlabel("Frequency (THz)")
|
|
370
|
+
>>> plt.ylabel("Phonon DOS")
|
|
371
|
+
|
|
372
|
+
References
|
|
373
|
+
----------
|
|
374
|
+
[1] Phys Rev B 47(9) 4863, 1993
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
axis = 0
|
|
378
|
+
if window:
|
|
379
|
+
sl = [None] * 3
|
|
380
|
+
sl[axis] = slice(None) # ':'
|
|
381
|
+
w = __welch(len(atoms))[tuple(sl)]
|
|
382
|
+
atoms_old = atoms
|
|
383
|
+
atoms = []
|
|
384
|
+
|
|
385
|
+
for i, a in enumerate(atoms_old):
|
|
386
|
+
vel = a.get_velocities()
|
|
387
|
+
a = a.copy()
|
|
388
|
+
a.set_velocities(vel * w[i])
|
|
389
|
+
atoms.append(a)
|
|
390
|
+
|
|
391
|
+
if method == VDOSMethod.DIRECT:
|
|
392
|
+
vel2 = np.array([a.get_velocities() for a in atoms])
|
|
393
|
+
mass_bc = atoms[0].get_masses()[None, :, None]
|
|
394
|
+
|
|
395
|
+
if npad is not None:
|
|
396
|
+
nadd = (vel2.shape[axis] - 1) * npad
|
|
397
|
+
if to_next:
|
|
398
|
+
vel2 = __pad_zeros(
|
|
399
|
+
vel2, tonext=True, tonext_min=vel2.shape[axis] + nadd, axis=axis
|
|
400
|
+
)
|
|
401
|
+
else:
|
|
402
|
+
vel2 = __pad_zeros(vel2, tonext=False, nadd=nadd, axis=axis)
|
|
403
|
+
|
|
404
|
+
full_fft_vel = np.abs(fft(vel2, axis=axis)) ** 2.0
|
|
405
|
+
full_faxis = np.fft.fftfreq(vel2.shape[axis], timestep)
|
|
406
|
+
split_idx = len(full_faxis) // 2
|
|
407
|
+
faxis = full_faxis[:split_idx]
|
|
408
|
+
# First split the array, then multiply by `mass` and average. If
|
|
409
|
+
# full_out, then we need full_fft_vel below, so copy before slicing.
|
|
410
|
+
arr = full_fft_vel.copy() if full_out else full_fft_vel
|
|
411
|
+
fft_vel = __slicetake(arr, slice(0, split_idx), axis=axis, copy=False)
|
|
412
|
+
if use_masses:
|
|
413
|
+
fft_vel *= mass_bc
|
|
414
|
+
# average remaining axes, summing is enough b/c normalization is done below
|
|
415
|
+
# sums: (nstep, natoms, 3) -> (nstep, natoms) -> (nstep,)
|
|
416
|
+
pdos = __enh_sum(fft_vel, axis=axis, keepdims=True)
|
|
417
|
+
default_out = (faxis, __norm_int(pdos, faxis, area=area))
|
|
418
|
+
if full_out:
|
|
419
|
+
# have to re-calculate this here b/c we never calculate the full_pdos
|
|
420
|
+
# normally
|
|
421
|
+
if use_masses:
|
|
422
|
+
full_fft_vel *= mass_bc
|
|
423
|
+
full_pdos = __enh_sum(full_fft_vel, axis=axis, keepdims=True)
|
|
424
|
+
extra_out = (full_faxis, full_pdos, split_idx)
|
|
425
|
+
return default_out + extra_out
|
|
426
|
+
else:
|
|
427
|
+
return default_out
|
|
428
|
+
|
|
429
|
+
elif method == VDOSMethod.VACF:
|
|
430
|
+
_, v = vacf(atoms, timestep=timestep, use_masses=use_masses)
|
|
431
|
+
if mirror:
|
|
432
|
+
fft_vacf = fft(__mirror(v))
|
|
433
|
+
else:
|
|
434
|
+
fft_vacf = fft(v)
|
|
435
|
+
full_faxis = np.fft.fftfreq(fft_vacf.shape[axis], timestep)
|
|
436
|
+
full_pdos = np.abs(fft_vacf)
|
|
437
|
+
split_idx = len(full_faxis) // 2
|
|
438
|
+
faxis = full_faxis[:split_idx]
|
|
439
|
+
pdos = full_pdos[:split_idx]
|
|
440
|
+
default_out = (faxis, __norm_int(pdos, faxis, area=area))
|
|
441
|
+
extra_out = (full_faxis, full_pdos, split_idx, v, fft_vacf)
|
|
442
|
+
if full_out:
|
|
443
|
+
return default_out + extra_out
|
|
444
|
+
else:
|
|
445
|
+
return default_out
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def __pad_zeros(
|
|
449
|
+
arr, axis=0, where="end", nadd=None, upto=None, tonext=None, tonext_min=None
|
|
450
|
+
):
|
|
451
|
+
"""Pad an nd-array with zeros. Default is to append an array of zeros of
|
|
452
|
+
the same shape as `arr` to arr's end along `axis`.
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
arr : nd array
|
|
457
|
+
axis : the axis along which to pad
|
|
458
|
+
where : string {'end', 'start'}, pad at the end ("append to array") or
|
|
459
|
+
start ("prepend to array") of `axis`
|
|
460
|
+
nadd : number of items to padd (i.e. nadd=3 means padd w/ 3 zeros in case
|
|
461
|
+
of an 1d array)
|
|
462
|
+
upto : pad until arr.shape[axis] == upto
|
|
463
|
+
tonext : bool, pad up to the next power of two (pad so that the padded
|
|
464
|
+
array has a length of power of two)
|
|
465
|
+
tonext_min : int, when using `tonext`, pad the array to the next possible
|
|
466
|
+
power of two for which the resulting array length along `axis` is at
|
|
467
|
+
least `tonext_min`; the default is tonext_min = arr.shape[axis]
|
|
468
|
+
|
|
469
|
+
Use only one of nadd, upto, tonext.
|
|
470
|
+
|
|
471
|
+
Returns
|
|
472
|
+
-------
|
|
473
|
+
padded array
|
|
474
|
+
|
|
475
|
+
Examples
|
|
476
|
+
--------
|
|
477
|
+
>>> # 1d
|
|
478
|
+
>>> pad_zeros(a)
|
|
479
|
+
array([1, 2, 3, 0, 0, 0])
|
|
480
|
+
>>> pad_zeros(a, nadd=3)
|
|
481
|
+
array([1, 2, 3, 0, 0, 0])
|
|
482
|
+
>>> pad_zeros(a, upto=6)
|
|
483
|
+
array([1, 2, 3, 0, 0, 0])
|
|
484
|
+
>>> pad_zeros(a, nadd=1)
|
|
485
|
+
array([1, 2, 3, 0])
|
|
486
|
+
>>> pad_zeros(a, nadd=1, where='start')
|
|
487
|
+
array([0, 1, 2, 3])
|
|
488
|
+
>>> # 2d
|
|
489
|
+
>>> a=arange(9).reshape(3,3)
|
|
490
|
+
>>> pad_zeros(a, nadd=1, axis=0)
|
|
491
|
+
array([[0, 1, 2],
|
|
492
|
+
[3, 4, 5],
|
|
493
|
+
[6, 7, 8],
|
|
494
|
+
[0, 0, 0]])
|
|
495
|
+
>>> pad_zeros(a, nadd=1, axis=1)
|
|
496
|
+
array([[0, 1, 2, 0],
|
|
497
|
+
[3, 4, 5, 0],
|
|
498
|
+
[6, 7, 8, 0]])
|
|
499
|
+
>>> # up to next power of two
|
|
500
|
+
>>> 2**arange(10)
|
|
501
|
+
array([ 1, 2, 4, 8, 16, 32, 64, 128, 256, 512])
|
|
502
|
+
>>> pydos.pad_zeros(arange(9), tonext=True).shape
|
|
503
|
+
(16,)
|
|
504
|
+
"""
|
|
505
|
+
if tonext == False:
|
|
506
|
+
tonext = None
|
|
507
|
+
lst = [nadd, upto, tonext]
|
|
508
|
+
assert lst.count(None) in [2, 3], (
|
|
509
|
+
"`nadd`, `upto` and `tonext` must be " + "all None or only one of them not None"
|
|
510
|
+
)
|
|
511
|
+
if nadd is None:
|
|
512
|
+
if upto is None:
|
|
513
|
+
if (tonext is None) or (not tonext):
|
|
514
|
+
# default
|
|
515
|
+
nadd = arr.shape[axis]
|
|
516
|
+
else:
|
|
517
|
+
tonext_min = arr.shape[axis] if (tonext_min is None) else tonext_min
|
|
518
|
+
# beware of int overflows starting w/ 2**arange(64), but we
|
|
519
|
+
# will never have such long arrays anyway
|
|
520
|
+
two_powers = 2 ** np.arange(30)
|
|
521
|
+
assert tonext_min <= two_powers[-1], (
|
|
522
|
+
"tonext_min exceeds " "max power of 2"
|
|
523
|
+
)
|
|
524
|
+
power = two_powers[np.searchsorted(two_powers, tonext_min)]
|
|
525
|
+
nadd = power - arr.shape[axis]
|
|
526
|
+
else:
|
|
527
|
+
nadd = upto - arr.shape[axis]
|
|
528
|
+
if nadd == 0:
|
|
529
|
+
return arr
|
|
530
|
+
add_shape = list(arr.shape)
|
|
531
|
+
add_shape[axis] = nadd
|
|
532
|
+
add_shape = tuple(add_shape)
|
|
533
|
+
if where == "end":
|
|
534
|
+
return np.concatenate((arr, np.zeros(add_shape, dtype=arr.dtype)), axis=axis)
|
|
535
|
+
elif where == "start":
|
|
536
|
+
return np.concatenate((np.zeros(add_shape, dtype=arr.dtype), arr), axis=axis)
|
|
537
|
+
else:
|
|
538
|
+
raise Exception("illegal `where` arg: %s" % where)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def __slicetake(a, sl, axis=None, copy=False):
|
|
542
|
+
"""The equivalent of numpy.take(a, ..., axis=<axis>), but accepts slice
|
|
543
|
+
objects instead of an index array. Also by default, it returns a *view* and
|
|
544
|
+
no copy.
|
|
545
|
+
|
|
546
|
+
Parameters
|
|
547
|
+
----------
|
|
548
|
+
a : numpy ndarray
|
|
549
|
+
sl : slice object, list or tuple of slice objects
|
|
550
|
+
axis=<int>
|
|
551
|
+
one slice object for *that* axis
|
|
552
|
+
axis=None
|
|
553
|
+
`sl` is a list or tuple of slice objects, one for each axis.
|
|
554
|
+
It must index the whole array, i.e. len(sl) == len(a.shape).
|
|
555
|
+
axis : {None, int}
|
|
556
|
+
copy : bool, return a copy instead of a view
|
|
557
|
+
|
|
558
|
+
Returns
|
|
559
|
+
-------
|
|
560
|
+
A view into `a` or copy of a slice of `a`.
|
|
561
|
+
|
|
562
|
+
Examples
|
|
563
|
+
--------
|
|
564
|
+
>>> from numpy import s_
|
|
565
|
+
>>> a = np.random.rand(20,20,20)
|
|
566
|
+
>>> b1 = a[:,:,10:]
|
|
567
|
+
>>> # single slice for axis 2
|
|
568
|
+
>>> b2 = slicetake(a, s_[10:], axis=2)
|
|
569
|
+
>>> # tuple of slice objects
|
|
570
|
+
>>> b3 = slicetake(a, s_[:,:,10:])
|
|
571
|
+
>>> (b2 == b1).all()
|
|
572
|
+
True
|
|
573
|
+
>>> (b3 == b1).all()
|
|
574
|
+
True
|
|
575
|
+
>>> # simple extraction too, sl = integer
|
|
576
|
+
>>> (a[...,5] == slicetake(a, 5, axis=-1))
|
|
577
|
+
True
|
|
578
|
+
"""
|
|
579
|
+
# The long story
|
|
580
|
+
# --------------
|
|
581
|
+
#
|
|
582
|
+
# 1) Why do we need that:
|
|
583
|
+
#
|
|
584
|
+
# # no problem
|
|
585
|
+
# a[5:10:2]
|
|
586
|
+
#
|
|
587
|
+
# # the same, more general
|
|
588
|
+
# sl = slice(5,10,2)
|
|
589
|
+
# a[sl]
|
|
590
|
+
#
|
|
591
|
+
# But we want to:
|
|
592
|
+
# - Define (type in) a slice object only once.
|
|
593
|
+
# - Take the slice of different arrays along different axes.
|
|
594
|
+
# Since numpy.take() and a.take() don't handle slice objects, one would
|
|
595
|
+
# have to use direct slicing and pay attention to the shape of the array:
|
|
596
|
+
#
|
|
597
|
+
# a[sl], b[:,:,sl,:], etc ...
|
|
598
|
+
#
|
|
599
|
+
# We want to use an 'axis' keyword instead. np.r_() generates index arrays
|
|
600
|
+
# from slice objects (e.g r_[1:5] == r_[s_[1:5] ==r_[slice(1,5,None)]).
|
|
601
|
+
# Since we need index arrays for numpy.take(), maybe we can use that? Like
|
|
602
|
+
# so:
|
|
603
|
+
#
|
|
604
|
+
# a.take(r_[sl], axis=0)
|
|
605
|
+
# b.take(r_[sl], axis=2)
|
|
606
|
+
#
|
|
607
|
+
# Here we have what we want: slice object + axis kwarg.
|
|
608
|
+
# But r_[slice(...)] does not work for all slice types. E.g. not for
|
|
609
|
+
#
|
|
610
|
+
# r_[s_[::5]] == r_[slice(None, None, 5)] == array([], dtype=int32)
|
|
611
|
+
# r_[::5] == array([], dtype=int32)
|
|
612
|
+
# r_[s_[1:]] == r_[slice(1, None, None)] == array([0])
|
|
613
|
+
# r_[1:]
|
|
614
|
+
# ValueError: dimensions too large.
|
|
615
|
+
#
|
|
616
|
+
# The returned index arrays are wrong (or we even get an exception).
|
|
617
|
+
# The reason is given below.
|
|
618
|
+
# Bottom line: We need this function.
|
|
619
|
+
#
|
|
620
|
+
# The reason for r_[slice(...)] gererating sometimes wrong index arrays is
|
|
621
|
+
# that s_ translates a fancy index (1:, ::5, 1:10:2, ...) to a slice
|
|
622
|
+
# object. This *always* works. But since take() accepts only index arrays,
|
|
623
|
+
# we use r_[s_[<fancy_index>]], where r_ translates the slice object
|
|
624
|
+
# prodced by s_ to an index array. THAT works only if start and stop of the
|
|
625
|
+
# slice are known. r_ has no way of knowing the dimensions of the array to
|
|
626
|
+
# be sliced and so it can't transform a slice object into a correct index
|
|
627
|
+
# array in case of slice(<number>, None, None) or slice(None, None,
|
|
628
|
+
# <number>).
|
|
629
|
+
#
|
|
630
|
+
# 2) Slice vs. copy
|
|
631
|
+
#
|
|
632
|
+
# numpy.take(a, array([0,1,2,3])) or a[array([0,1,2,3])] return a copy of
|
|
633
|
+
# `a` b/c that's "fancy indexing". But a[slice(0,4,None)], which is the
|
|
634
|
+
# same as indexing (slicing) a[:4], return *views*.
|
|
635
|
+
|
|
636
|
+
if axis is None:
|
|
637
|
+
slices = sl
|
|
638
|
+
else:
|
|
639
|
+
# Note that these are equivalent:
|
|
640
|
+
# a[:]
|
|
641
|
+
# a[s_[:]]
|
|
642
|
+
# a[slice(None)]
|
|
643
|
+
# a[slice(None, None, None)]
|
|
644
|
+
# a[slice(0, None, None)]
|
|
645
|
+
slices = [slice(None)] * a.ndim
|
|
646
|
+
slices[axis] = sl
|
|
647
|
+
# a[...] can take a tuple or list of slice objects
|
|
648
|
+
# a[x:y:z, i:j:k] is the same as
|
|
649
|
+
# a[(slice(x,y,z), slice(i,j,k))] == a[[slice(x,y,z), slice(i,j,k)]]
|
|
650
|
+
slices = tuple(slices)
|
|
651
|
+
if copy:
|
|
652
|
+
return a[slices].copy()
|
|
653
|
+
else:
|
|
654
|
+
return a[slices]
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def __enh_sum(arr, axis=None, keepdims=False, **kwds):
|
|
658
|
+
"""This numpy.sum() with some features implemented which can be found in
|
|
659
|
+
numpy v1.7 and later: `axis` can be a tuple to select arbitrary axes to sum
|
|
660
|
+
over.
|
|
661
|
+
|
|
662
|
+
We also have a `keepdims` keyword, which however works completely different
|
|
663
|
+
from numpy. Docstrings shamelessly stolen from numpy and adapted here
|
|
664
|
+
and there.
|
|
665
|
+
|
|
666
|
+
Parameters
|
|
667
|
+
----------
|
|
668
|
+
arr : nd array
|
|
669
|
+
axis : None or int or tuple of ints, optional
|
|
670
|
+
Axis or axes along which a sum is performed. The default (`axis` =
|
|
671
|
+
`None`) is to perform a sum over all the dimensions of the input array.
|
|
672
|
+
`axis` may be negative, in which case it counts from the last to the
|
|
673
|
+
first axis.
|
|
674
|
+
If this is a tuple of ints, a sum is performed on multiple
|
|
675
|
+
axes, instead of a single axis or all the axes as before.
|
|
676
|
+
keepdims : bool, optional
|
|
677
|
+
If this is set to True, the axes from `axis` are left in the result
|
|
678
|
+
and the reduction (sum) is performed for all remaining axes. Therefore,
|
|
679
|
+
it reverses the `axis` to be summed over.
|
|
680
|
+
**kwds : passed to np.sum().
|
|
681
|
+
|
|
682
|
+
Examples
|
|
683
|
+
--------
|
|
684
|
+
>>> a=rand(2,3,4)
|
|
685
|
+
>>> num.sum(a)
|
|
686
|
+
12.073636268676152
|
|
687
|
+
>>> a.sum()
|
|
688
|
+
12.073636268676152
|
|
689
|
+
>>> num.sum(a, axis=1).shape
|
|
690
|
+
(2, 4)
|
|
691
|
+
>>> num.sum(a, axis=(1,)).shape
|
|
692
|
+
(2, 4)
|
|
693
|
+
>>> # same as axis=1, i.e. it inverts the axis over which we sum
|
|
694
|
+
>>> num.sum(a, axis=(0,2), keepdims=True).shape
|
|
695
|
+
(2, 4)
|
|
696
|
+
>>> # numpy's keepdims has another meaning: it leave the summed axis (0,2)
|
|
697
|
+
>>> # as dimension of size 1 to allow broadcasting
|
|
698
|
+
>>> numpy.sum(a, axis=(0,2), keepdims=True).shape
|
|
699
|
+
(1, 3, 1)
|
|
700
|
+
>>> num.sum(a, axis=(1,)) - num.sum(a, axis=1)
|
|
701
|
+
array([[ 0., 0., 0., 0.],
|
|
702
|
+
[ 0., 0., 0., 0.]])
|
|
703
|
+
>>> num.sum(a, axis=(0,2)).shape
|
|
704
|
+
(3,)
|
|
705
|
+
>>> num.sum(a, axis=(0,2)) - a.sum(axis=0).sum(axis=1)
|
|
706
|
+
array([ 0., 0., 0.])
|
|
707
|
+
"""
|
|
708
|
+
|
|
709
|
+
# Recursion rocks!
|
|
710
|
+
def _sum(arr, tosum):
|
|
711
|
+
if len(tosum) > 0:
|
|
712
|
+
# Choose axis to sum over, remove from list w/ remaining axes.
|
|
713
|
+
axis = tosum.pop(0)
|
|
714
|
+
_arr = arr.sum(axis=axis)
|
|
715
|
+
# arr has one dim less now. Rename remaining axes accordingly.
|
|
716
|
+
_tosum = [xx - 1 if xx > axis else xx for xx in tosum]
|
|
717
|
+
return _sum(_arr, _tosum)
|
|
718
|
+
else:
|
|
719
|
+
return arr
|
|
720
|
+
|
|
721
|
+
axis_is_int = isinstance(axis, int)
|
|
722
|
+
if axis is None:
|
|
723
|
+
if keepdims:
|
|
724
|
+
raise Exception("axis=None + keepdims=True makes no sense")
|
|
725
|
+
else:
|
|
726
|
+
return np.sum(arr, axis=axis, **kwds)
|
|
727
|
+
elif axis_is_int and not keepdims:
|
|
728
|
+
return np.sum(arr, axis=axis, **kwds)
|
|
729
|
+
else:
|
|
730
|
+
if axis_is_int:
|
|
731
|
+
tosum = [axis]
|
|
732
|
+
elif isinstance(axis, tuple) or isinstance(axis, list):
|
|
733
|
+
tosum = list(axis)
|
|
734
|
+
else:
|
|
735
|
+
raise Exception("illegal type for axis: %s" % str(type(axis)))
|
|
736
|
+
if keepdims:
|
|
737
|
+
alldims = range(arr.ndim)
|
|
738
|
+
tosum = [xx for xx in alldims if xx not in tosum]
|
|
739
|
+
return _sum(arr, tosum)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def __mirror(arr, axis=0):
|
|
743
|
+
"""Mirror array `arr` at index 0 along `axis`.
|
|
744
|
+
The length of the returned array is 2*arr.shape[axis]-1 ."""
|
|
745
|
+
return np.concatenate((arr[::-1], arr[1:]), axis=axis)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def __welch(M, sym=1):
|
|
749
|
+
"""Welch window. Function skeleton shamelessly stolen from
|
|
750
|
+
scipy.signal.windows.bartlett() and others."""
|
|
751
|
+
if M < 1:
|
|
752
|
+
return np.array([])
|
|
753
|
+
if M == 1:
|
|
754
|
+
return np.ones(1, dtype=float)
|
|
755
|
+
odd = M % 2
|
|
756
|
+
if not sym and not odd:
|
|
757
|
+
M = M + 1
|
|
758
|
+
n = np.arange(0, M)
|
|
759
|
+
w = 1.0 - ((n - 0.5 * (M - 1)) / (0.5 * (M - 1))) ** 2.0
|
|
760
|
+
if not sym and not odd:
|
|
761
|
+
w = w[:-1]
|
|
762
|
+
return w
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def __norm_int(y, x, area=1.0, scale=True, func=simps):
|
|
766
|
+
"""Normalize integral area of y(x) to `area`.
|
|
767
|
+
|
|
768
|
+
Parameters
|
|
769
|
+
----------
|
|
770
|
+
x,y : numpy 1d arrays
|
|
771
|
+
area : float
|
|
772
|
+
scale : bool, optional
|
|
773
|
+
Scale x and y to the same order of magnitude before integration.
|
|
774
|
+
This may be necessary to avoid numerical trouble if x and y have very
|
|
775
|
+
different scales.
|
|
776
|
+
func : callable
|
|
777
|
+
Function to do integration (like scipy.integrate.{simps,trapz,...}
|
|
778
|
+
Called as ``func(y,x=x)``. Default: simps
|
|
779
|
+
|
|
780
|
+
Returns
|
|
781
|
+
-------
|
|
782
|
+
scaled y
|
|
783
|
+
|
|
784
|
+
Notes
|
|
785
|
+
-----
|
|
786
|
+
The argument order y,x might be confusing. x,y would be more natural but we
|
|
787
|
+
stick to the order used in the scipy.integrate routines.
|
|
788
|
+
"""
|
|
789
|
+
if scale:
|
|
790
|
+
fx = np.abs(x).max()
|
|
791
|
+
fy = np.abs(y).max()
|
|
792
|
+
sx = x / fx
|
|
793
|
+
sy = y / fy
|
|
794
|
+
else:
|
|
795
|
+
fx = fy = 1.0
|
|
796
|
+
sx, sy = x, y
|
|
797
|
+
# Area under unscaled y(x).
|
|
798
|
+
_area = func(sy, x=sx) * fx * fy
|
|
799
|
+
return y * area / _area
|