1import hashlib
2
3import numpy as np
4
5from ase.units import Hartree, Bohr
6
7
8def L_to_lm(L):
9    """Convert L index to (l, m) index."""
10    l = int(np.sqrt(L))
11    m = L - l**2 - l
12    return l, m
13
14
15def lm_to_L(l, m):
16    """Convert (l, m) index to L index."""
17    return l**2 + l + m
18
19
20def split_formula(formula):
21    """Count elements in a chemical formula.
22
23    E.g. split_formula('C2H3Mg') -> ['C', 'C', 'H', 'H', 'H', 'Mg']
24    """
25    res = []
26    for c in formula:
27        if c.isupper():
28            res.append(c)
29        elif c.islower():
30            res[-1] += c
31        else:
32            res.extend([res[-1]] * (eval(c) - 1))
33    return res
34
35
36def construct_reciprocal(gd, q_c=None):
37    """Construct the reciprocal lattice from ``GridDescriptor`` instance.
38
39    The generated reciprocal lattice has lattice vectors corresponding to the
40    real-space lattice defined in the input grid. Note that it is the squared
41    length of the reciprocal lattice vectors that are returned.
42
43    The ordering of the reciprocal lattice agrees with the one typically used
44    in fft algorithms, i.e. positive k-values followed by negative.
45
46    Note that the G=(0,0,0) entry is set to one instead of zero. This
47    bit should probably be moved somewhere else ...
48
49    Parameters
50    ----------
51    q_c: ndarray
52        Offset for the reciprocal lattice vectors (in scaled coordinates of the
53        reciprocal lattice vectors, i.e. array with index ``c``). When
54        specified, the returned array contains the values of (q+G)^2 where G
55        denotes the reciprocal lattice vectors.
56
57    """
58
59    assert gd.pbc_c.all(), 'Works only with periodic boundary conditions!'
60
61    q_c = np.zeros((3, 1), dtype=float) if q_c is None else q_c.reshape((3, 1))
62
63    # Calculate reciprocal lattice vectors
64    N_c1 = gd.N_c[:, None]
65    i_cq = np.indices(gd.n_c, dtype=float).reshape((3, -1))  # offsets....
66    i_cq += gd.beg_c[:, None]
67    i_cq += N_c1 // 2
68    i_cq %= N_c1
69    i_cq -= N_c1 // 2
70
71    i_cq += q_c
72
73    # Convert from scaled to absolute coordinates
74    B_vc = 2.0 * np.pi * gd.icell_cv.T
75    k_vq = np.dot(B_vc, i_cq)
76
77    k_vq *= k_vq
78    k2_Q = k_vq.sum(axis=0).reshape(gd.n_c)
79
80    # Avoid future divide-by-zero by setting k2_Q[G=(0,0,0)] = 1.0 if needed
81    if k2_Q[0, 0, 0] < 1e-10:
82        k2_Q[0, 0, 0] = 1.0           # Only make sense iff
83        assert gd.comm.rank == 0      # * on rank 0 (G=(0,0,0) is only there)
84        assert abs(q_c).sum() < 1e-8  # * q_c is (almost) zero
85
86    assert k2_Q.min() > 0.0       # Now there should be no zero left
87
88    # Determine N^3
89    #
90    # Why do we need to calculate and return this?  The caller already
91    # has access to gd and thus knows how many points there are.
92    N3 = gd.n_c[0] * gd.n_c[1] * gd.n_c[2]
93    return k2_Q, N3
94
95
96def coordinates(gd, origin=None, tiny=1e-12):
97    """Constructs and returns matrices containing cartesian coordinates,
98       and the square of the distance from the origin.
99
100       The origin can be given explicitely (in Bohr units, not Anstroms).
101       Otherwise the origin is placed in the center of the box described
102       by the given grid-descriptor 'gd'.
103    """
104
105    if origin is None:
106        origin = 0.5 * gd.cell_cv.sum(0)
107    r0_v = np.array(origin)
108
109    r_vG = gd.get_grid_point_distance_vectors(r0_v)
110    r2_G = np.sum(r_vG**2, axis=0)
111    # Remove singularity at origin and replace with small number
112    r2_G = np.where(r2_G < tiny, tiny, r2_G)
113
114    # Return r^2 matrix
115    return r_vG, r2_G
116
117
118def pick(a_ix, i):
119    """Take integer index of a, or a linear combination of the elements of a"""
120    if isinstance(i, int):
121        return a_ix[i]
122    shape = a_ix.shape
123    a_x = np.dot(i, a_ix[:].reshape(shape[0], -1))
124    return a_x.reshape(shape[1:])
125
126
127def dagger(a, copy=True):
128    """Return Hermitian conjugate of input
129
130    If copy is False, the original array might be overwritten. This is faster,
131    but use with care.
132    """
133    if copy:
134        return np.conj(a.T)
135    else:
136        a = a.T
137        if a.dtype == complex:
138            a.imag *= -1
139        return a
140
141
142def project(a, b):
143    """Return the projection of b onto a."""
144    return a * (np.dot(a.conj(), b) / np.linalg.norm(a))
145
146
147def normalize(U):
148    """Normalize columns of U."""
149    for col in U.T:
150        col /= np.linalg.norm(col)
151
152
153def get_matrix_index(ind1, ind2=None):
154    if ind2 is None:
155        dim1 = len(ind1)
156        return np.resize(ind1, (dim1, dim1))
157    else:
158        dim1 = len(ind1)
159        dim2 = len(ind2)
160    return np.resize(ind1, (dim2, dim1)).T, np.resize(ind2, (dim1, dim2))
161
162
163def gram_schmidt(U):
164    """Orthonormalize columns of U according to the Gram-Schmidt procedure."""
165    for i, col in enumerate(U.T):
166        for col2 in U.T[:i]:
167            col -= col2 * np.dot(col2.conj(), col)
168        col /= np.linalg.norm(col)
169
170
171def lowdin(U, S=None):
172    """Orthonormalize columns of U according to the Lowdin procedure.
173
174    If the overlap matrix is know, it can be specified in S.
175    """
176    if S is None:
177        S = np.dot(dagger(U), U)
178    eig, rot = np.linalg.eigh(S)
179    rot = np.dot(rot / np.sqrt(eig), dagger(rot))
180    U[:] = np.dot(U, rot)
181
182
183def lowdin_svd(U):
184    """Orthogonalize according to the Lowdin procedure
185       using singular value decomposition.
186
187       U is an N x M matrix containing M vectors as its columns.
188    """
189    Z, D, V = np.linalg.svd(U, full_matrices=0)
190    return np.dot(Z, V)
191
192
193def symmetrize(matrix):
194    """Symmetrize input matrix."""
195    np.add(dagger(matrix), matrix, matrix)
196    np.multiply(.5, matrix, matrix)
197    return matrix
198
199
200def tri2full(H_nn, UL='L', map=np.conj):
201    """Fill in values of hermitian or symmetric matrix.
202
203    Fill values in lower or upper triangle of H_nn based on the opposite
204    triangle, such that the resulting matrix is symmetric/hermitian.
205
206    UL='U' will copy (conjugated) values from upper triangle into the
207    lower triangle.
208
209    UL='L' will copy (conjugated) values from lower triangle into the
210    upper triangle.
211
212    The map parameter can be used to specify a different operation than
213    conjugation, which should work on 1D arrays.  Example::
214
215      def antihermitian(src, dst):
216            np.conj(-src, dst)
217
218      tri2full(H_nn, map=antihermitian)
219
220    """
221    N, tmp = H_nn.shape
222    assert N == tmp, 'Matrix must be square'
223    if UL != 'L':
224        H_nn = H_nn.T
225
226    for n in range(N - 1):
227        map(H_nn[n + 1:, n], H_nn[n, n + 1:])
228
229
230def apply_subspace_mask(H_nn, f_n):
231    """Uncouple occupied and unoccupied subspaces.
232
233    This method forces the H_nn matrix into a block-diagonal form
234    in the occupied and unoccupied states respectively.
235    """
236    occ = 0
237    nbands = len(f_n)
238    while occ < nbands and f_n[occ] > 1e-3:
239        occ += 1
240    H_nn[occ:, :occ] = H_nn[:occ, occ:] = 0
241
242
243def cutoff2gridspacing(E):
244    """Convert planewave energy cutoff to a real-space gridspacing."""
245    return np.pi / np.sqrt(2 * E / Hartree) * Bohr
246
247
248def gridspacing2cutoff(h):
249    """Convert real-space gridspacing to planewave energy cutoff."""
250    # In Hartree units, E = k^2 / 2, where k_max is approx. given by pi / h
251    # See PRB, Vol 54, 14362 (1996)
252    return 0.5 * (np.pi * Bohr / h)**2 * Hartree
253
254
255def tridiag(a, b, c, r, u):
256    """Solve linear system with tridiagonal coefficient matrix.
257
258    a is the lower band, b is the diagonal, c is the upper band, and
259    r is the right hand side.
260    The solution is returned in u.
261
262
263    [b1 c1  0  ...            ] [u1]   [r1]
264    [a1 b2 c2 0 ...           ] [ :]   [ :]
265    [ 0 a2 b3 c3 0 ...        ] [  ] = [  ]
266    [                         ] [  ]   [  ]
267    [     ... 0 an-2 bn-1 cn-1] [ :]   [ :]
268    [          ... 0 an-1 bn  ] [un]   [rn]
269    """
270    n = len(b)
271    tmp = np.zeros(n - 1)  # necessary temporary array
272    if b[0] == 0:
273        raise RuntimeError('System is effectively order N-1')
274
275    beta = b[0]
276    u[0] = r[0] / beta
277    for i in range(1, n):
278        # Decompose and forward substitution
279        tmp[i - 1] = c[i - 1] / beta
280        beta = b[i] - a[i - 1] * tmp[i - 1]
281        if beta == 0:
282            raise RuntimeError('Method failure')
283        u[i] = (r[i] - a[i - 1] * u[i - 1]) / beta
284
285    for i in range(n - 1, 0, -1):
286        # Backward substitution
287        u[i - 1] -= tmp[i - 1] * u[i]
288
289
290def signtrim(data, decimals=None):
291    """Trim off the sign of potential zeros, usually occurring after round.
292
293    data is the ndarray holding NumPy data to round and trim.
294    decimals is an integer specifying how many decimals to round to.
295    """
296    if decimals is not None:
297        data = data.round(decimals)  # np.round is buggy because -0 != 0
298
299    shape = data.shape
300    data = data.reshape(-1)
301
302    if data.dtype == complex:
303        i = np.argwhere(np.sign(data.real) == 0).ravel()
304        j = np.argwhere(np.sign(data.imag) == 0).ravel()
305        data.real[i] = 0
306        data.imag[j] = 0
307    else:
308        i = np.argwhere(np.sign(data) == 0).ravel()
309        data[i] = 0
310
311    return data.reshape(shape)
312
313
314def md5_array(data, numeric=False):
315    """Create MD5 hex digest from NumPy array.
316
317    Optionally, will cast the 128 bit hexadecimal hash to a 64 bit integer.
318
319    Warning: For floating point types, only bitwise identical data will
320    produce matching MD5 fingerprints, so do not attempt to match sets
321    of nearly identical data by rounding off beforehand.
322
323    Example:
324
325     >>> data = np.linspace(0, np.pi, 1000000)
326     >>> eps = 1e-6
327     >>> a = md5_array(data.round(3))
328     >>> b = md5_array((data + eps).round(3))
329     >>> a == b
330     False
331
332    This is due to the inexact nature of the floating point representation.
333    """
334
335    if not isinstance(data, np.ndarray):
336        data = np.asarray(data)
337
338    # Only accepts float,complex,int,bool,...
339    if (not np.issubdtype(data.dtype, np.number) and
340        data.dtype not in [bool, np.bool, np.bool_]):
341        raise TypeError('MD5 hex digest only accepts numeric/boolean arrays.')
342
343    datahash = hashlib.md5(data.tobytes())
344
345    if numeric:
346        def xor(a, b):
347            return chr(ord(a) ^ ord(b))  # bitwise xor on 2 bytes -> 1 byte
348
349        sbuf128 = datahash.digest()
350        sbuf64 = ''.join([xor(a, b)
351                          for a, b in zip(sbuf128[::2], sbuf128[1::2])])
352        return np.fromstring(sbuf64, np.int64).item()
353    else:
354        return datahash.hexdigest()
355
356
357def split_nodes(length, parrank, parsize):
358    """Split length over nodes.
359
360    Divide length into parsize even sized chunks, and return the start/end
361    indices of the parrank'th chunk.
362    """
363    if parsize == 1:
364        return 0, length
365    pernode = int(round(length / float(parsize)))
366    if parrank == parsize - 1:
367        return parrank * pernode, length
368    return parrank * pernode, (parrank + 1) * pernode
369
370
371class Spline:
372    def __init__(self, xi, yi, leftderiv=None, rightderiv=None):
373        """Cubic spline approximation class.
374
375        xi, yi specifies the known data points.
376
377        leftderiv and rightderiv specifies the first derivative on the
378        boundaries. If set to None, the second derivative is set to zero.
379
380        Example usage::
381
382          >>> xi = np.arange(.1, 5, .5)    # known data points
383          >>> yi = np.cos(xi)              # known data points
384          >>> sp = Spline(xi, yi)       # make spline
385          >>> x = np.arange(-.5, 5.5, .05) # points to interpolate to
386          >>> y = sp(x)  # get spline value on an entire list
387          >>> y2 = sp(4) # get spline value at a single point
388
389        Based on 'Numerical recipes in c'
390        """
391        self.xy = (xi, yi)
392        N = len(xi)
393        self.ypp = u = np.zeros(N)  # the second derivatives y''
394        tmp = np.zeros(N - 1)
395
396        # Set left boundary condition
397        if leftderiv is None:  # natural spline - second derivative is zero
398            tmp[0] = u[0] = 0.0
399        else:  # clamped spline - first derivative is fixed
400            tmp[0] = 3 / (xi[1] - xi[0]) * (
401                (yi[1] - yi[0]) / (xi[1] - xi[0]) - leftderiv)
402            u[0] = -.5
403
404        for i in range(1, N - 1):
405            sig = (xi[i] - xi[i - 1]) / (xi[i + 1] - xi[i - 1])
406            p = sig * u[i - 1] + 2
407            u[i] = (sig - 1) / p
408            tmp[i] = (yi[i + 1] - yi[i]) / (xi[i + 1] - xi[i]) - \
409                     (yi[i] - yi[i - 1]) / (xi[i] - xi[i - 1])
410            tmp[i] = (6 * tmp[i] /
411                      (xi[i + 1] - xi[i - 1]) - sig * tmp[i - 1]) / p
412
413        # Set right boundary condition
414        if rightderiv is None:  # natural spline - second derivative is zero
415            qn = tmpn = 0.0
416        else:  # clamped spline - first derivative is fixed
417            qn = .5
418            tmpn = 3 / (xi[N - 1] - xi[N - 2]) * (
419                rightderiv - (yi[N - 1] - yi[N - 2]) / (xi[N - 1] - xi[N - 2]))
420
421        u[N - 1] = (tmpn - qn * tmp[N - 2]) / (qn * u[N - 1] + 1)
422        for k in range(N - 2, -1, -1):  # backsubstitution step
423            u[k] = u[k] * u[k + 1] + tmp[k]
424
425    def __call__(self, x):
426        """Evaluate spline for each point in input argument.
427
428        The value in point x[i-1] < x <= x[i] is determined by::
429
430                                    ''       ''
431          y(x) = a y    + b y  + c y    + d y
432                    i-1      i      i-1      i
433
434        """
435        x = np.array(x, float)
436        if x.ndim == 0:
437            x.shape = (1,)
438        y = np.zeros_like(x)
439        xi, yi = self.xy
440
441        i = None
442        for j, xval in enumerate(x):
443            i = self.locate(xval, i)
444            h = xi[i] - xi[i - 1]
445            a = (xi[i] - xval) / h
446            b = 1. - a
447            c = (a**3 - a) * h**2 / 6.
448            d = (b**3 - b) * h**2 / 6.
449            y[j] = (a * yi[i - 1] + b * yi[i] +
450                    c * self.ypp[i - 1] + d * self.ypp[i])
451        return y
452
453    def locate(self, x, guess=None):
454        """return i such that x[i-1] < x <= xi[i]
455
456        1 or len(xi) - 1 is returned if x is outside list range.
457        """
458        xi = self.xy[0]
459        if x <= xi[0]:
460            return 1
461        elif x > xi[-1]:
462            return len(xi) - 1
463        elif guess and xi[guess - 1] < x <= xi[guess]:
464            return guess
465        else:
466            return np.searchsorted(xi, x)
467