File: coordinatetransform.py

package info (click to toggle)
python-ase 3.26.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 15,484 kB
  • sloc: python: 148,112; xml: 2,728; makefile: 110; javascript: 47
file content (304 lines) | stat: -rw-r--r-- 10,462 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
# fmt: off

"""Prism"""
import warnings
from typing import Sequence, Union

import numpy as np

from ase.geometry import wrap_positions


def calc_box_parameters(cell: np.ndarray) -> np.ndarray:
    """Calculate box parameters

    https://docs.lammps.org/Howto_triclinic.html
    """
    ax = np.sqrt(cell[0] @ cell[0])
    bx = cell[0] @ cell[1] / ax
    by = np.sqrt(cell[1] @ cell[1] - bx ** 2)
    cx = cell[0] @ cell[2] / ax
    cy = (cell[1] @ cell[2] - bx * cx) / by
    cz = np.sqrt(cell[2] @ cell[2] - cx ** 2 - cy ** 2)
    return np.array((ax, by, cz, bx, cx, cy))


def calc_rotated_cell(cell: np.ndarray) -> np.ndarray:
    """Calculate rotated cell in LAMMPS coordinates

    Parameters
    ----------
    cell : np.ndarray
        Cell to be rotated.

    Returns
    -------
    rotated_cell : np.ndarray
        Rotated cell represented by a lower triangular matrix.
    """
    ax, by, cz, bx, cx, cy = calc_box_parameters(cell)
    return np.array(((ax, 0.0, 0.0), (bx, by, 0.0), (cx, cy, cz)))


def calc_reduced_cell(
    cell: np.ndarray,
    pbc: Union[np.ndarray, Sequence[bool]],
) -> np.ndarray:
    """Calculate LAMMPS cell with short lattice basis vectors

    The lengths of the second and the third lattice basis vectors, b and c, are
    shortened with keeping the same periodicity of the system. b is modified by
    adding multiple a vectors, and c is modified by adding first multiple b
    vectors and then multiple a vectors.

    Parameters
    ----------
    cell : np.ndarray
        Cell to be reduced. This must be already a lower triangular matrix.
    pbc : Sequence[bool]
        True if the system is periodic along the corresponding direction.

    Returns
    -------
    reduced_cell : np.ndarray
        Reduced cell. `xx`, `yy`, `zz` are the same as the original cell,
        and `abs(xy) <= xx`, `abs(xz) <= xx`, `abs(yz) <= yy`.
    """
    # cell 1 (reduced) <- cell 2 (original)
    # o-----------------------------/==o-----------------------------/--o
    #  \                        /--/    \                        /--/
    #   \                   /--/         \                   /--/
    #    \         1    /--/              \   2          /--/
    #     \         /--/                   \         /--/
    #      \    /--/                        \    /--/
    #       o==/-----------------------------o--/
    reduced_cell = cell.copy()
    # Order in which off-diagonal elements are checked for strong tilt
    # yz is updated before xz so that the latter does not affect the former
    flip_order = ((1, 0), (2, 1), (2, 0))
    for i, j in flip_order:
        if not pbc[j]:
            continue
        ratio = reduced_cell[i, j] / reduced_cell[j, j]
        if abs(ratio) > 0.5:
            reduced_cell[i] -= reduced_cell[j] * np.round(ratio)
    return reduced_cell


class Prism:
    """The representation of the unit cell in LAMMPS

    The main purpose of the prism-object is to create suitable string
    representations of prism limits and atom positions within the prism.

    Parameters
    ----------
    cell : np.ndarray
        Cell in ASE coordinate system.
    pbc : one or three bool
        Periodic boundary conditions flags.
    reduce_cell : bool
        If True, the LAMMPS cell is reduced for short lattice basis vectors.
        The atomic positions are always wraped into the reduced cell,
        regardress of `wrap` in `vector_to_lammps` and `vector_to_ase`.
    tolerance : float
        Precision for skewness test.

    Methods
    -------
    vector_to_lammps
        Rotate vectors from ASE to LAMMPS coordinates.
        Positions can be further wrapped into the LAMMPS cell by `wrap=True`.

    vector_to_ase
        Rotate vectors from LAMMPS to ASE coordinates.
        Positions can be further wrapped into the LAMMPS cell by `wrap=True`.

    Notes
    -----
    LAMMPS prefers triangular matrixes without a strong tilt.
    Therefore the 'Prism'-object contains three coordinate systems:

    - ase_cell (the simulated system in the ASE coordination system)
    - lammps_tilt (ase-cell rotated to be an lower triangular matrix)
    - lammps_cell (same volume as tilted cell, but reduce edge length)

    The translation between 'ase_cell' and 'lammps_tilt' is done with a
    rotation matrix 'rot_mat'. (Mathematically this is a QR decomposition.)

    The transformation between 'lammps_tilt' and 'lammps_cell' is done by
    changing the off-diagonal elements.

    Depending on the option `reduce`, vectors in ASE coordinates are
    transformed either `lammps_tilt` or `lammps_cell`.

    The vector conversion can fail as depending on the simulation run LAMMPS
    might have changed the simulation box significantly. This is for example a
    problem with hexagonal cells. LAMMPS might also wrap atoms across periodic
    boundaries, which can lead to problems for example NEB calculations.
    """

    # !TODO: derive tolerance from cell-dimensions
    def __init__(
        self,
        cell: np.ndarray,
        pbc: Union[bool, np.ndarray] = True,
        reduce_cell: bool = False,
        tolerance: float = 1.0e-8,
    ):
        #    rot_mat * lammps_tilt^T = ase_cell^T
        # => lammps_tilt * rot_mat^T = ase_cell
        # => lammps_tilt             = ase_cell * rot_mat
        # LAMMPS requires positive diagonal elements of the triangular matrix.
        # The diagonals of `lammps_tilt` are always positive by construction.
        self.lammps_tilt = calc_rotated_cell(cell)
        self.rot_mat = np.linalg.solve(self.lammps_tilt, cell).T
        self.ase_cell = cell
        self.tolerance = tolerance
        self.pbc = tuple(np.zeros(3, bool) + pbc)
        self.lammps_cell = calc_reduced_cell(self.lammps_tilt, self.pbc)
        self.is_reduced = reduce_cell

    @property
    def cell(self) -> np.ndarray:
        return self.lammps_cell if self.is_reduced else self.lammps_tilt

    def get_lammps_prism(self) -> np.ndarray:
        """Return box parameters of the rotated cell in LAMMPS coordinates

        Returns
        -------
        np.ndarray
            xhi - xlo, yhi - ylo, zhi - zlo, xy, xz, yz
        """
        return self.cell[(0, 1, 2, 1, 2, 2), (0, 1, 2, 0, 0, 1)]

    def update_cell(self, lammps_cell: np.ndarray) -> np.ndarray:
        """Rotate new LAMMPS cell into ASE coordinate system

        Parameters
        ----------
        lammps_cell : np.ndarray
            New Cell in LAMMPS coordinates received after executing LAMMPS

        Returns
        -------
        np.ndarray
            New cell in ASE coordinates
        """
        # Transformation: integer matrix
        # lammps_cell * transformation = lammps_tilt
        transformation = np.linalg.solve(self.lammps_cell, self.lammps_tilt)

        if self.is_reduced:
            self.lammps_cell = lammps_cell
            self.lammps_tilt = lammps_cell @ transformation
        else:
            self.lammps_tilt = lammps_cell
            self.lammps_cell = calc_reduced_cell(self.lammps_tilt, self.pbc)

        # try to detect potential flips in lammps
        # (lammps minimizes the cell-vector lengths)
        new_ase_cell = self.lammps_tilt @ self.rot_mat.T
        # assuming the cell changes are mostly isotropic
        new_vol = np.linalg.det(new_ase_cell)
        old_vol = np.linalg.det(self.ase_cell)
        test_residual = self.ase_cell.copy()
        test_residual *= (new_vol / old_vol) ** (1.0 / 3.0)
        test_residual -= new_ase_cell
        if any(
                np.linalg.norm(test_residual, axis=1)
                > 0.5 * np.linalg.norm(self.ase_cell, axis=1)
        ):
            warnings.warn(
                "Significant simulation cell changes from LAMMPS detected. "
                "Backtransformation to ASE might fail!"
            )
        return new_ase_cell

    def vector_to_lammps(
        self,
        vec: np.ndarray,
        wrap: bool = False,
    ) -> np.ndarray:
        """Rotate vectors from ASE to LAMMPS coordinates

        Parameters
        ----------
        vec : np.ndarray
            Vectors in ASE coordinates to be rotated into LAMMPS coordinates
        wrap : bool
            If True, the vectors are wrapped into the cell

        Returns
        -------
        np.array
            Vectors in LAMMPS coordinates
        """
        # !TODO: right eps-limit
        # lammps might not like atoms outside the cell
        if wrap or self.is_reduced:
            return wrap_positions(
                vec @ self.rot_mat,
                cell=self.cell,
                pbc=self.pbc,
                eps=1e-18,
            )
        return vec @ self.rot_mat

    def vector_to_ase(
        self,
        vec: np.ndarray,
        wrap: bool = False,
    ) -> np.ndarray:
        """Rotate vectors from LAMMPS to ASE coordinates

        Parameters
        ----------
        vec : np.ndarray
            Vectors in LAMMPS coordinates to be rotated into ASE coordinates
        wrap : bool
            If True, the vectors are wrapped into the cell

        Returns
        -------
        np.ndarray
            Vectors in ASE coordinates
        """
        if wrap or self.is_reduced:
            # fractional in `lammps_tilt` (the same shape as ASE cell)
            fractional = np.linalg.solve(self.lammps_tilt.T, vec.T).T
            # wrap into 0 to 1 for periodic directions
            fractional -= np.floor(fractional) * self.pbc
            # Cartesian coordinates wrapped into `lammps_tilt`
            vec = fractional @ self.lammps_tilt
        # rotate back to the ASE cell
        return vec @ self.rot_mat.T

    def tensor2_to_ase(self, tensor: np.ndarray) -> np.ndarray:
        """Rotate a second order tensor from LAMMPS to ASE coordinates

        Parameters
        ----------
        tensor : np.ndarray
            Tensor in LAMMPS coordinates to be rotated into ASE coordinates

        Returns
        -------
        np.ndarray
            Tensor in ASE coordinates
        """
        return self.rot_mat @ tensor @ self.rot_mat.T

    def is_skewed(self) -> bool:
        """Test if the lammps cell is skewed, i.e., monoclinic or triclinic.

        Returns
        -------
        bool
            True if the lammps cell is skewed.
        """
        cell_sq = self.cell ** 2
        on_diag = np.sum(np.diag(cell_sq))
        off_diag = np.sum(np.tril(cell_sq, -1))
        return off_diag / on_diag > self.tolerance