Skip to content

Commit df4784a

Browse files
lammps: rotate cell to lower_triangle before writing to lammps (#842)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Cell matrices and atomic coordinates are now automatically transformed to a lower triangular form, ensuring consistent output formatting. * **Improvements** * All cell parameter and atomic position outputs now reflect the rotated, standardized cell representation. * **Tests** * Added comprehensive tests verifying the lower triangular transformation and preservation of atomic distances across various cell configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: root <pxlxingliang> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 1a1bb57 commit df4784a

File tree

2 files changed

+111
-15
lines changed

2 files changed

+111
-15
lines changed

dpdata/lammps/lmp.py

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -193,31 +193,64 @@ def to_system_data(lines, type_map=None, type_idx_zero=True):
193193
return system_data(lines, type_map=type_map, type_idx_zero=type_idx_zero)
194194

195195

196+
def rotate_to_lower_triangle(
197+
cell: np.ndarray, coord: np.ndarray
198+
) -> tuple[np.ndarray, np.ndarray]:
199+
"""Rotate the cell to lower triangular and ensure the diagonal elements are non-negative.
200+
201+
Args:
202+
cell (np.ndarray): The original cell matrix.
203+
coord (np.ndarray): The coordinates of the atoms.
204+
205+
Returns
206+
-------
207+
tuple[np.ndarray, np.ndarray]: The rotated cell and adjusted coordinates.
208+
"""
209+
q, _ = np.linalg.qr(cell.T)
210+
cell = np.matmul(cell, q)
211+
coord = np.matmul(coord, q)
212+
213+
# Ensure the diagonal elements of the cell are non-negative
214+
rot = np.eye(3)
215+
if cell[0][0] < 0:
216+
rot[0][0] = -1
217+
if cell[1][1] < 0:
218+
rot[1][1] = -1
219+
if cell[2][2] < 0:
220+
rot[2][2] = -1
221+
cell = np.matmul(cell, rot)
222+
coord = np.matmul(coord, rot)
223+
return cell, coord
224+
225+
196226
def from_system_data(system, f_idx=0):
197227
ret = ""
198228
ret += "\n"
199229
natoms = sum(system["atom_numbs"])
200230
ntypes = len(system["atom_numbs"])
231+
cell, coord = rotate_to_lower_triangle(
232+
system["cells"][f_idx], system["coords"][f_idx]
233+
)
201234
ret += "%d atoms\n" % natoms # noqa: UP031
202235
ret += "%d atom types\n" % ntypes # noqa: UP031
203236
ret += (ptr_float_fmt + " " + ptr_float_fmt + " xlo xhi\n") % (
204237
0,
205-
system["cells"][f_idx][0][0],
238+
cell[0][0],
206239
) # noqa: UP031
207240
ret += (ptr_float_fmt + " " + ptr_float_fmt + " ylo yhi\n") % (
208241
0,
209-
system["cells"][f_idx][1][1],
242+
cell[1][1],
210243
) # noqa: UP031
211244
ret += (ptr_float_fmt + " " + ptr_float_fmt + " zlo zhi\n") % (
212245
0,
213-
system["cells"][f_idx][2][2],
246+
cell[2][2],
214247
) # noqa: UP031
215248
ret += (
216249
ptr_float_fmt + " " + ptr_float_fmt + " " + ptr_float_fmt + " xy xz yz\n"
217250
) % (
218-
system["cells"][f_idx][1][0],
219-
system["cells"][f_idx][2][0],
220-
system["cells"][f_idx][2][1],
251+
cell[1][0],
252+
cell[2][0],
253+
cell[2][1],
221254
) # noqa: UP031
222255
ret += "\n"
223256
ret += "Atoms # atomic\n"
@@ -255,9 +288,9 @@ def from_system_data(system, f_idx=0):
255288
ret += coord_fmt % (
256289
ii + 1,
257290
system["atom_types"][ii] + 1,
258-
system["coords"][f_idx][ii][0] - system["orig"][0],
259-
system["coords"][f_idx][ii][1] - system["orig"][1],
260-
system["coords"][f_idx][ii][2] - system["orig"][2],
291+
coord[ii][0] - system["orig"][0],
292+
coord[ii][1] - system["orig"][1],
293+
coord[ii][2] - system["orig"][2],
261294
system["spins"][f_idx][ii][0] / spins_norm[ii],
262295
system["spins"][f_idx][ii][1] / spins_norm[ii],
263296
system["spins"][f_idx][ii][2] / spins_norm[ii],
@@ -267,9 +300,9 @@ def from_system_data(system, f_idx=0):
267300
ret += coord_fmt % (
268301
ii + 1,
269302
system["atom_types"][ii] + 1,
270-
system["coords"][f_idx][ii][0] - system["orig"][0],
271-
system["coords"][f_idx][ii][1] - system["orig"][1],
272-
system["coords"][f_idx][ii][2] - system["orig"][2],
303+
coord[ii][0] - system["orig"][0],
304+
coord[ii][1] - system["orig"][1],
305+
coord[ii][2] - system["orig"][2],
273306
system["spins"][f_idx][ii][0],
274307
system["spins"][f_idx][ii][1],
275308
system["spins"][f_idx][ii][2] + 1,
@@ -279,9 +312,9 @@ def from_system_data(system, f_idx=0):
279312
ret += coord_fmt % (
280313
ii + 1,
281314
system["atom_types"][ii] + 1,
282-
system["coords"][f_idx][ii][0] - system["orig"][0],
283-
system["coords"][f_idx][ii][1] - system["orig"][1],
284-
system["coords"][f_idx][ii][2] - system["orig"][2],
315+
coord[ii][0] - system["orig"][0],
316+
coord[ii][1] - system["orig"][1],
317+
coord[ii][2] - system["orig"][2],
285318
) # noqa: UP031
286319
return ret
287320

tests/test_lammps_lmp_dump.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import os
44
import unittest
55

6+
import numpy as np
67
from context import dpdata
78
from poscars.poscar_ref_oh import TestPOSCARoh
89

10+
from dpdata.lammps.lmp import rotate_to_lower_triangle
11+
912

1013
class TestLmpDump(unittest.TestCase, TestPOSCARoh):
1114
def setUp(self):
@@ -27,5 +30,65 @@ def setUp(self):
2730
self.system.from_fmt("tmp.lmp", fmt="lammps/lmp", type_map=["O", "H"])
2831

2932

33+
class TestLmpRotateTriAngle(unittest.TestCase):
34+
def test_simple_cubic(self):
35+
cubic_cell = np.diag([5, 5, 5])
36+
cubic_coords = np.array([[0, 0, 0], [2.5, 2.5, 2.5]])
37+
rotated_cell, rotated_coords = rotate_to_lower_triangle(
38+
cubic_cell, cubic_coords
39+
)
40+
41+
self.assertTrue(np.allclose(rotated_cell, np.diag([5, 5, 5])))
42+
self.assertTrue(np.allclose(rotated_coords, cubic_coords))
43+
44+
def test_triclinic_cell(self):
45+
triclinic_cell = np.array([[4.0, 0.0, 0.0], [1.0, 3.0, 0.0], [0.5, 0.5, 2.5]])
46+
47+
triclinic_coords = np.array([[0.5, 0.5, 0.5], [3.0, 2.0, 1.0]])
48+
rotated_cell, rotated_coords = rotate_to_lower_triangle(
49+
triclinic_cell.copy(), triclinic_coords.copy()
50+
)
51+
52+
self.assertTrue(np.isclose(rotated_cell[0, 1], 0, atol=1e-10))
53+
self.assertTrue(np.isclose(rotated_cell[0, 2], 0, atol=1e-10))
54+
self.assertTrue(np.isclose(rotated_cell[1, 2], 0, atol=1e-10))
55+
56+
self.assertTrue(rotated_cell[0, 0] > 0)
57+
self.assertTrue(rotated_cell[1, 1] > 0)
58+
self.assertTrue(rotated_cell[2, 2] > 0)
59+
60+
# check the distance between two atoms
61+
self.assertTrue(
62+
np.isclose(
63+
np.linalg.norm(rotated_coords[0] - rotated_coords[1]),
64+
np.linalg.norm(triclinic_coords[0] - triclinic_coords[1]),
65+
atol=1e-10,
66+
)
67+
)
68+
69+
def test_negative_diagonal(self):
70+
negative_cell = np.array([[-3.0, 1.0, 0.5], [0.0, -2.0, 0.3], [0.0, 0.0, -4.0]])
71+
negative_coords = np.array([[1.0, 0.5, -1.0], [0.5, 1.0, -2.0]])
72+
rotated_cell, rotated_coords = rotate_to_lower_triangle(
73+
negative_cell.copy(), negative_coords.copy()
74+
)
75+
76+
self.assertTrue(np.isclose(rotated_cell[0, 1], 0, atol=1e-10))
77+
self.assertTrue(np.isclose(rotated_cell[0, 2], 0, atol=1e-10))
78+
self.assertTrue(np.isclose(rotated_cell[1, 2], 0, atol=1e-10))
79+
80+
self.assertTrue(rotated_cell[0, 0] > 0)
81+
self.assertTrue(rotated_cell[1, 1] > 0)
82+
self.assertTrue(rotated_cell[2, 2] > 0)
83+
84+
self.assertTrue(
85+
np.isclose(
86+
np.linalg.norm(negative_coords[0] - negative_coords[1]),
87+
np.linalg.norm(rotated_coords[0] - rotated_coords[1]),
88+
atol=1e-6,
89+
)
90+
)
91+
92+
3093
if __name__ == "__main__":
3194
unittest.main()

0 commit comments

Comments
 (0)