fastatomstruct 0.8.1__cp311-cp311-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.
@@ -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