File: cell.py

package info (click to toggle)
python-ase 3.21.1-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 13,936 kB
  • sloc: python: 122,428; xml: 946; makefile: 111; javascript: 47
file content (342 lines) | stat: -rw-r--r-- 11,857 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
import ase
from typing import Mapping, Sequence, Union
import numpy as np
from ase.utils.arraywrapper import arraylike
from ase.utils import pbc2pbc


__all__ = ['Cell']


@arraylike
class Cell:
    """Parallel epipedal unit cell of up to three dimensions.

    This object resembles a 3x3 array whose [i, j]-th element is the jth
    Cartesian coordinate of the ith unit vector.

    Cells of less than three dimensions are represented by placeholder
    unit vectors that are zero."""

    ase_objtype = 'cell'  # For JSON'ing

    def __init__(self, array):
        """Create cell.

        Parameters:

        array: 3x3 arraylike object
          The three cell vectors: cell[0], cell[1], and cell[2].
        """
        array = np.asarray(array, dtype=float)
        assert array.shape == (3, 3)
        self.array = array

    def cellpar(self, radians=False):
        """Get unit cell parameters. Sequence of 6 numbers.

        First three are unit cell vector lengths and second three
        are angles between them::

            [len(a), len(b), len(c), angle(b,c), angle(a,c), angle(a,b)]

        in degrees.

        See also :func:`ase.geometry.cell.cell_to_cellpar`."""
        from ase.geometry.cell import cell_to_cellpar
        return cell_to_cellpar(self.array, radians)

    def todict(self):
        return dict(array=self.array)

    @classmethod
    def ascell(cls, cell):
        """Return argument as a Cell object.  See :meth:`ase.cell.Cell.new`.

        A new Cell object is created if necessary."""
        if isinstance(cell, cls):
            return cell
        return cls.new(cell)

    @classmethod
    def new(cls, cell=None):
        """Create new cell from any parameters.

        If cell is three numbers, assume three lengths with right angles.

        If cell is six numbers, assume three lengths, then three angles.

        If cell is 3x3, assume three cell vectors."""

        if cell is None:
            cell = np.zeros((3, 3))

        cell = np.array(cell, float)

        if cell.shape == (3,):
            cell = np.diag(cell)
        elif cell.shape == (6,):
            from ase.geometry.cell import cellpar_to_cell
            cell = cellpar_to_cell(cell)
        elif cell.shape != (3, 3):
            raise ValueError('Cell must be length 3 sequence, length 6 '
                             'sequence or 3x3 matrix!')

        cellobj = cls(cell)
        return cellobj

    @classmethod
    def fromcellpar(cls, cellpar, ab_normal=(0, 0, 1), a_direction=None):
        """Return new Cell from cell lengths and angles.

        See also :func:`~ase.geometry.cell.cellpar_to_cell()`."""
        from ase.geometry.cell import cellpar_to_cell
        cell = cellpar_to_cell(cellpar, ab_normal, a_direction)
        return cls(cell)

    def get_bravais_lattice(self, eps=2e-4, *, pbc=True):
        """Return :class:`~ase.lattice.BravaisLattice` for this cell:

        >>> cell = Cell.fromcellpar([4, 4, 4, 60, 60, 60])
        >>> print(cell.get_bravais_lattice())
        FCC(a=5.65685)

        .. note:: The Bravais lattice object follows the AFlow
           conventions.  ``cell.get_bravais_lattice().tocell()`` may
           differ from the original cell by a permutation or other
           operation which maps it to the AFlow convention.  For
           example, the orthorhombic lattice enforces a < b < c.

           To build a bandpath for a particular cell, use
           :meth:`ase.cell.Cell.bandpath` instead of this method.
           This maps the kpoints back to the original input cell.

        """
        from ase.lattice import identify_lattice
        pbc = self.any(1) & pbc2pbc(pbc)
        lat, op = identify_lattice(self, eps=eps, pbc=pbc)
        return lat

    def bandpath(
            self,
            path: str = None,
            npoints: int = None,
            *,
            density: float = None,
            special_points: Mapping[str, Sequence[float]] = None,
            eps: float = 2e-4,
            pbc: Union[bool, Sequence[bool]] = True
    ) -> "ase.dft.kpoints.BandPath":
        """Build a :class:`~ase.dft.kpoints.BandPath` for this cell.

        If special points are None, determine the Bravais lattice of
        this cell and return a suitable Brillouin zone path with
        standard special points.

        If special special points are given, interpolate the path
        directly from the available data.

        Parameters:

        path: string
            String of special point names defining the path, e.g. 'GXL'.
        npoints: int
            Number of points in total.  Note that at least one point
            is added for each special point in the path.
        density: float
            density of kpoints along the path in Å⁻¹.
        special_points: dict
            Dictionary mapping special points to scaled kpoint coordinates.
            For example ``{'G': [0, 0, 0], 'X': [1, 0, 0]}``.
        eps: float
            Tolerance for determining Bravais lattice.
        pbc: three bools
            Whether cell is periodic in each direction.  Normally not
            necessary.  If cell has three nonzero cell vectors, use
            e.g. pbc=[1, 1, 0] to request a 2D bandpath nevertheless.

        Example
        -------
        >>> cell = Cell.fromcellpar([4, 4, 4, 60, 60, 60])
        >>> cell.bandpath('GXW', npoints=20)
        BandPath(path='GXW', cell=[3x3], special_points={GKLUWX}, kpts=[20x3])

        """
        # TODO: Combine with the rotation transformation from bandpath()

        cell = self.uncomplete(pbc)

        if special_points is None:
            from ase.lattice import identify_lattice
            lat, op = identify_lattice(cell, eps=eps)
            bandpath = lat.bandpath(path, npoints=npoints, density=density)
            return bandpath.transform(op)
        else:
            from ase.dft.kpoints import BandPath, resolve_custom_points
            path, special_points = resolve_custom_points(
                path, special_points, eps=eps)
            bandpath = BandPath(cell, path=path, special_points=special_points)
            return bandpath.interpolate(npoints=npoints, density=density)

    def uncomplete(self, pbc):
        """Return new cell, zeroing cell vectors where not periodic."""
        _pbc = np.empty(3, bool)
        _pbc[:] = pbc
        cell = self.copy()
        cell[~_pbc] = 0
        return cell

    def complete(self):
        """Convert missing cell vectors into orthogonal unit vectors."""
        from ase.geometry.cell import complete_cell
        cell = Cell(complete_cell(self.array))
        return cell

    def copy(self):
        """Return a copy of this cell."""
        cell = Cell(self.array.copy())
        return cell

    @property
    def rank(self) -> int:
        """"Return the dimension of the cell.

        Equal to the number of nonzero lattice vectors."""
        # The name ndim clashes with ndarray.ndim
        return self.any(1).sum()  # type: ignore

    @property
    def orthorhombic(self) -> bool:
        """Return whether this cell is represented by a diagonal matrix."""
        from ase.geometry.cell import is_orthorhombic
        return is_orthorhombic(self)

    def lengths(self):
        """Return the length of each lattice vector as an array."""
        return np.linalg.norm(self, axis=1)

    def angles(self):
        """Return an array with the three angles alpha, beta, and gamma."""
        return self.cellpar()[3:].copy()

    def __array__(self, dtype=float):
        if dtype != float:
            raise ValueError('Cannot convert cell to array of type {}'
                             .format(dtype))
        return self.array

    def __bool__(self):
        return bool(self.any())  # need to convert from np.bool_

    __nonzero__ = __bool__

    @property
    def volume(self) -> float:
        """Get the volume of this cell.

        If there are less than 3 lattice vectors, return 0."""
        # Fail or 0 for <3D cells?
        # Definitely 0 since this is currently a property.
        # I think normally it is more convenient just to get zero
        return np.abs(np.linalg.det(self))

    @property
    def handedness(self) -> int:
        """Sign of the determinant of the matrix of cell vectors.

        1 for right-handed cells, -1 for left, and 0 for cells that
        do not span three dimensions."""
        return np.sign(np.linalg.det(self))

    def scaled_positions(self, positions) -> np.ndarray:
        """Calculate scaled positions from Cartesian positions.

        The scaled positions are the positions given in the basis
        of the cell vectors.  For the purpose of defining the basis, cell
        vectors that are zero will be replaced by unit vectors as per
        :meth:`~ase.cell.Cell.complete`."""
        return np.linalg.solve(self.complete().T, positions.T).T

    def cartesian_positions(self, scaled_positions) -> np.ndarray:
        """Calculate Cartesian positions from scaled positions."""
        return scaled_positions @ self.complete()

    def reciprocal(self) -> 'Cell':
        """Get reciprocal lattice as a Cell object.

        Does not include factor of 2 pi."""
        return Cell(np.linalg.pinv(self).transpose())

    def __repr__(self):
        if self.orthorhombic:
            numbers = self.lengths().tolist()
        else:
            numbers = self.tolist()

        return 'Cell({})'.format(numbers)

    def niggli_reduce(self, eps=1e-5):
        """Niggli reduce this cell, returning a new cell and mapping.

        See also :func:`ase.build.tools.niggli_reduce_cell`."""
        from ase.build.tools import niggli_reduce_cell
        cell, op = niggli_reduce_cell(self, epsfactor=eps)
        result = Cell(cell)
        return result, op

    def minkowski_reduce(self):
        """Minkowski-reduce this cell, returning new cell and mapping.

        See also :func:`ase.geometry.minkowski_reduction.minkowski_reduce`."""
        from ase.geometry.minkowski_reduction import minkowski_reduce
        cell, op = minkowski_reduce(self, self.any(1))
        result = Cell(cell)
        return result, op

    def permute_axes(self, permutation):
        """Permute axes of cell."""
        assert (np.sort(permutation) == np.arange(3)).all()
        permuted = Cell(self[permutation][:, permutation])
        return permuted

    def standard_form(self):
        """Rotate axes such that unit cell is lower triangular. The cell
        handedness is preserved.

        A lower-triangular cell with positive diagonal entries is a canonical
        (i.e. unique) description. For a left-handed cell the diagonal entries
        are negative.

        Returns:

        rcell: the standardized cell object

        Q: ndarray
            The orthogonal transformation.  Here, rcell @ Q = cell, where cell
            is the input cell and rcell is the lower triangular (output) cell.
        """

        # get cell handedness (right or left)
        sign = np.sign(np.linalg.det(self))
        if sign == 0:
            sign = 1

        # LQ decomposition provides an axis-aligned description of the cell.
        # Q is an orthogonal matrix and L is a lower triangular matrix. The
        # decomposition is a unique description if the diagonal elements are
        # all positive (negative for a left-handed cell).
        Q, L = np.linalg.qr(self.T)
        Q = Q.T
        L = L.T

        # correct the signs of the diagonal elements
        signs = np.sign(np.diag(L))
        indices = np.where(signs == 0)[0]
        signs[indices] = 1
        indices = np.where(signs != sign)[0]
        L[:, indices] *= -1
        Q[indices] *= -1
        return Cell(L), Q

    # XXX We want a reduction function that brings the cell into
    # standard form as defined by Setyawan and Curtarolo.