Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import numpy as np
2from ase.geometry import wrap_positions
4# Order in which off-diagonal elements are checked for strong tilt
5FLIP_ORDER = [(1, 0, 0), (2, 0, 0), (2, 1, 1)]
8class Prism:
9 """The representation of the unit cell in LAMMPS
11 The main purpose of the prism-object is to create suitable
12 string representations of prism limits and atom positions
13 within the prism.
15 :param cell: cell in ase coordinate system
16 :param pbc: periodic boundaries
17 :param tolerance: precision for skewness test
19 **Implementation**
21 LAMMPS requires triangular matrixes without a strong tilt.
22 Therefore the 'Prism'-object contains three coordinate systems:
24 - ase_cell (the simulated system in the ASE coordination system)
25 - lammps_tilt (ase-cell rotated to be an lower triangular matrix)
26 - lammps_cell (same volume as tilted cell, but reduce edge length)
28 The translation between 'ase_cell' and 'lammps_cell' is down with a
29 rotation matrix 'rot_mat' obtained from a QR decomposition. The
30 transformation between 'lammps_tilt' and 'lammps_cell' is done by
31 changing the off-diagonal elements. 'flip' saves the modified
32 elements for later reversal. All vectors except positions are just
33 rotated with 'rot_mat'. Positions are rotated and wrapped into
34 'lammps_cell'. Translating results back from LAMMPS needs first the
35 unwrapping of positions. Then all vectors and the unit-cell are
36 rotated back into the ASE coordination system. This can fail as
37 depending on the simulation run LAMMPS might have changed the
38 simulation box significantly. This is for example a problem with
39 hexagonal cells. LAMMPS might also wrap atoms across periodic
40 boundaries, which can lead to problems for example NEB
41 calculations.
42 """
44 # !TODO: derive tolerance from cell-dimensions
45 def __init__(self, cell, pbc=(True, True, True), tolerance=1.0e-8):
46 # Use QR decomposition to get the lammps cell
47 # rot_mat * lammps_tilt^T = ase_cell^T
48 # => lammps_tilt * rot_mat^T = ase_cell
49 # => lammps_tilt = ase_cell * rot_mat
50 qtrans, ltrans = np.linalg.qr(cell.T, mode="complete")
51 self.rot_mat = qtrans
52 self.lammps_tilt = ltrans.T
53 self.ase_cell = cell
54 self.tolerance = tolerance
55 self.pbc = pbc
57 # LAMMPS requires positive values on the diagonal of the
58 # triangluar matrix -> mirror if necessary
59 for i in range(3):
60 if self.lammps_tilt[i][i] < 0.0:
61 mirror_mat = np.eye(3)
62 mirror_mat[i][i] = -1.0
63 self.lammps_tilt = np.dot(mirror_mat, self.lammps_tilt.T).T
64 self.rot_mat = np.dot(self.rot_mat, mirror_mat)
66 self.lammps_cell = self.lammps_tilt.copy()
68 # LAMMPS minimizes the edge length of the parallelepiped
69 # What is ment with 'flip': cell 2 is transformed into cell 1
70 # cell 2 = 'lammps_tilt'; cell 1 = 'lammps_cell'
71 # o-----------------------------/==o-----------------------------/--o
72 # \ /--/ \ /--/
73 # \ /--/ \ /--/
74 # \ 1 /--/ \ 2 /--/
75 # \ /--/ \ /--/
76 # \ /--/ \ /--/
77 # o==/-----------------------------o--/
78 # !TODO: handle extreme tilt (= off-diagonal > 1.5)
79 self.flip = np.array(
80 [
81 abs(self.lammps_cell[i][j] / self.lammps_tilt[k][k]) > 0.5
82 and self.pbc[k]
83 for i, j, k in FLIP_ORDER
84 ]
85 )
86 for iteri, (i, j, k) in enumerate(FLIP_ORDER):
87 if self.flip[iteri]:
88 change = self.lammps_cell[k][k]
89 change *= np.sign(self.lammps_cell[i][j])
90 self.lammps_cell[i][j] -= change
92 def get_lammps_prism(self):
93 """Return into lammps coordination system rotated cell
95 :returns: lammps cell
96 :rtype: np.array
98 """
99 return self.lammps_cell[(0, 1, 2, 1, 2, 2), (0, 1, 2, 0, 0, 1)]
101 # !TODO: detect flip in lammps
102 def update_cell(self, lammps_cell):
103 """Rotate new lammps cell into ase coordinate system
105 :param lammps_cell: new lammps cell received after executing lammps
106 :returns: ase cell
107 :rtype: np.array
109 """
110 self.lammps_cell = lammps_cell
111 self.lammps_tilt = self.lammps_cell.copy()
113 # reverse flip
114 for iteri, (i, j, k) in enumerate(FLIP_ORDER):
115 if self.flip[iteri]:
116 change = self.lammps_cell[k][k]
117 change *= np.sign(self.lammps_cell[i][j])
118 self.lammps_tilt[i][j] -= change
120 # try to detect potential flips in lammps
121 # (lammps minimizes the cell-vector lengths)
122 new_ase_cell = np.dot(self.lammps_tilt, self.rot_mat.T)
123 # assuming the cell changes are mostly isotropic
124 new_vol = np.linalg.det(new_ase_cell)
125 old_vol = np.linalg.det(self.ase_cell)
126 test_residual = self.ase_cell.copy()
127 test_residual *= (new_vol / old_vol) ** (1.0 / 3.0)
128 test_residual -= new_ase_cell
129 if any(
130 np.linalg.norm(test_residual, axis=1)
131 > 0.5 * np.linalg.norm(self.ase_cell, axis=1)
132 ):
133 print(
134 "WARNING: Significant simulation cell changes from LAMMPS "
135 + "detected.\n"
136 + " " * 9
137 + "Backtransformation to ASE might fail!"
138 )
139 return new_ase_cell
141 def vector_to_lammps(self, vec, wrap=False):
142 """Rotate vector from ase coordinate system to lammps one
144 :param vec: to be rotated ase-vector
145 :returns: lammps-vector
146 :rtype: np.array
148 """
149 # !TODO: right eps-limit
150 # lammps might not like atoms outside the cell
151 if wrap:
152 return wrap_positions(
153 np.dot(vec, self.rot_mat),
154 self.lammps_cell,
155 pbc=self.pbc,
156 eps=1e-18,
157 )
159 return np.dot(vec, self.rot_mat)
161 def vector_to_ase(self, vec, wrap=False):
162 """Rotate vector from lammps coordinate system to ase one
164 :param vec: to be rotated lammps-vector
165 :param wrap: was vector wrapped into 'lammps_cell'
166 :returns: ase-vector
167 :rtype: np.array
169 """
170 if wrap:
171 # trying to reverse wraping across periodic boundaries in the
172 # lammps coordination-system:
173 # translate: expresses lammps-coordinate system in the rotate, but
174 # without tilt removed system
175 # fractional: vector in tilted system
176 translate = np.linalg.solve(
177 self.lammps_tilt.T, self.lammps_cell.T
178 ).T
179 fractional = np.linalg.solve(self.lammps_tilt.T, vec.T).T
181 # !TODO: make somehow nicer
182 # !TODO: handle extreme tilted cells
183 for ifrac in fractional:
184 for zyx in reversed(range(3)):
185 if ifrac[zyx] >= 1.0 and self.pbc[zyx]:
186 ifrac -= translate[zyx]
187 elif ifrac[zyx] < 0.0 and self.pbc[zyx]:
188 ifrac += translate[zyx]
189 vec = np.dot(fractional, self.lammps_tilt)
191 return np.dot(vec, self.rot_mat.T)
193 def is_skewed(self):
194 """Test if a lammps cell is not tetragonal
196 :returns: bool
197 :rtype: bool
199 """
200 cell_sq = self.lammps_cell ** 2
201 return (
202 np.sum(np.tril(cell_sq, -1)) / np.sum(np.diag(cell_sq))
203 > self.tolerance
204 )