Skip to content

Commit e0d98fa

Browse files
committed
improve generate_structure
Using ` np.linalg.qr` we can remove a lot of extraneous code, regarding the transformation of the cell and atomic positions, to LAMMPS compatible format. We also store the transformation matrix, so it will be possible to transform back, during the parsing stage.
1 parent c958668 commit e0d98fa

File tree

5 files changed

+61
-226
lines changed

5 files changed

+61
-226
lines changed

aiida_lammps/calculations/lammps/__init__.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class BaseLammpsCalculation(CalcJob):
110110
_DEFAULT_OUTPUT_INFO_FILE_NAME = "system_info.dump"
111111
_DEFAULT_OUTPUT_RESTART_FILE_NAME = 'lammps.restart'
112112

113-
_retrieve_list = ['log.lammps']
113+
_retrieve_list = []
114114
_retrieve_temporary_list = []
115115
_cmdline_params = ['-in', _INPUT_FILE_NAME]
116116
_stdout_name = None
@@ -123,6 +123,8 @@ def define(cls, spec):
123123
help='lammps potential')
124124
spec.input('parameters', valid_type=Dict,
125125
help='the parameters', required=False)
126+
spec.input('metadata.options.cell_transform_filename',
127+
valid_type=six.string_types, default="cell_transform.npy")
126128
spec.input('metadata.options.output_filename',
127129
valid_type=six.string_types, default=cls._DEFAULT_OUTPUT_FILE_NAME)
128130
spec.input('metadata.options.trajectory_name',
@@ -199,8 +201,11 @@ def prepare_for_submission(self, tempfolder):
199201
potential_txt = self.inputs.potential.get_potential_file()
200202

201203
# Setup structure
202-
structure_txt = generate_lammps_structure(self.inputs.structure,
203-
self.inputs.potential.atom_style)
204+
structure_txt, struct_transform = generate_lammps_structure(
205+
self.inputs.structure, self.inputs.potential.atom_style)
206+
207+
with open(tempfolder.get_abs_path(self.options.cell_transform_filename), 'w+b') as handle:
208+
np.save(handle, struct_transform)
204209

205210
if "parameters" in self.inputs:
206211
parameters = self.inputs.parameters
@@ -255,7 +260,9 @@ def prepare_for_submission(self, tempfolder):
255260

256261
calcinfo = CalcInfo()
257262
calcinfo.uuid = self.uuid
258-
calcinfo.retrieve_list = self._retrieve_list
263+
calcinfo.retrieve_list = self._retrieve_list + [
264+
self.options.output_filename,
265+
self.options.cell_transform_filename]
259266
calcinfo.retrieve_temporary_list = self._retrieve_temporary_list
260267
calcinfo.codes_info = [codeinfo]
261268

Lines changed: 44 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,35 @@
11
""" creation of the structure file content
2-
3-
The code here is largely adapted from https://github.com/andeplane/cif2cell-lammps
4-
ESPInterfaces.LAMMPSFile
52
"""
6-
from math import cos, sin, atan2
7-
import ase
3+
# import ase
84
import numpy as np
95

106

11-
def get_vector(value):
12-
return [float(v) for v in value]
13-
14-
15-
def get_matrix(value):
16-
return [[float(v) for v in vec] for vec in value]
17-
18-
19-
def mvmult3(mat, vec):
20-
""" matrix-vector multiplication """
21-
w = [0., 0., 0.]
22-
for i in range(3):
23-
t = 0
24-
for j in range(3):
25-
t = t + mat[j][i] * vec[j]
26-
w[i] = t
27-
return w
28-
29-
30-
def cartesian_to_frac(lattice, ccoords):
31-
"""convert cartesian coordinate to fractional
7+
def transform_cell(cell):
8+
"""transform the cell to an orientation, compatible with LAMMPS
329
33-
Parameters
34-
----------
35-
lattice: list
36-
3x3 array of lattice vectors
37-
ccoord: list
38-
Nx3 cartesian coordinates
10+
LAMMPS requires the simulation cell to be in the format of a
11+
lower triangular matrix (right-handed basis).
12+
Therefore the cell and positions may require rotation and inversion.
13+
See https://lammps.sandia.gov/doc/Howto_triclinic.html
3914
40-
Returns
41-
-------
42-
list:
43-
Nx3 array of fractional coordinate
15+
:param cell: (3x3) lattice
16+
:returns: (new_cell, transform)
4417
4518
"""
46-
det3 = np.linalg.det
47-
latt_tr = np.transpose(lattice)
48-
det_latt_tr = np.linalg.det(latt_tr)
49-
50-
fcoords = []
51-
for ccoord in ccoords:
52-
a = (det3([[ccoord[0], latt_tr[0][1], latt_tr[0][2]],
53-
[ccoord[1], latt_tr[1][1], latt_tr[1][2]],
54-
[ccoord[2], latt_tr[2][1], latt_tr[2][2]]])) / det_latt_tr
55-
b = (det3([[latt_tr[0][0], ccoord[0], latt_tr[0][2]],
56-
[latt_tr[1][0], ccoord[1], latt_tr[1][2]],
57-
[latt_tr[2][0], ccoord[2], latt_tr[2][2]]])) / det_latt_tr
58-
c = (det3([[latt_tr[0][0], latt_tr[0][1], ccoord[0]],
59-
[latt_tr[1][0], latt_tr[1][1], ccoord[1]],
60-
[latt_tr[2][0], latt_tr[2][1], ccoord[2]]])) / det_latt_tr
61-
62-
fcoords.append([a, b, c])
63-
64-
return fcoords
65-
66-
67-
def is_not_zero(value):
68-
return not np.isclose(value, 0)
69-
70-
71-
def round_by(value, round_dp):
72-
if round_dp is None:
73-
return value
74-
return round(value, round_dp)
19+
cell = np.array(cell)
20+
transform, upper_tri = np.linalg.qr(cell.T, mode="complete")
21+
new_cell = np.transpose(upper_tri)
7522

23+
# LAMMPS also requires positive values on the diagonal of the,
24+
# so invert cell if necessary
25+
inversion = np.eye(3)
26+
for i in range(3):
27+
if new_cell[i][i] < 0.0:
28+
inversion[i][i] = -1.0
29+
new_cell = np.dot(inversion, new_cell.T).T
30+
transform = np.dot(transform, inversion.T).T
7631

77-
class AtomSite(object):
78-
def __init__(self, kind_name, cartesian, fractional=None):
79-
self.kind_name = kind_name
80-
self.cartesian = cartesian
81-
self.fractional = fractional
32+
return new_cell, transform
8233

8334

8435
def generate_lammps_structure(structure,
@@ -100,98 +51,45 @@ def generate_lammps_structure(structure,
10051
docstring : str
10152
docstring to put at top of file
10253
54+
Returns
55+
-------
56+
str: content
57+
the structure file content
58+
numpy.array: transform
59+
the transformation matrix applied to the structure cell and coordinates
60+
10361
"""
10462
if atom_style not in ['atomic', 'charge']:
10563
raise ValueError("atom_style must be in ['atomic', 'charge']")
10664
if charge_dict is None:
10765
charge_dict = {}
10866

109-
atom_sites = [AtomSite(site.kind_name, site.position)
110-
for site in structure.sites]
11167
# mapping of atom kind_name to id number
11268
kind_name_id_map = {}
113-
for site in atom_sites:
69+
for site in structure.sites:
11470
if site.kind_name not in kind_name_id_map:
11571
kind_name_id_map[site.kind_name] = len(kind_name_id_map) + 1
11672
# mapping of atom kind_name to mass
11773
kind_mass_dict = {kind.name: kind.mass for kind in structure.kinds}
11874

11975
filestring = ""
12076
filestring += "# {}\n\n".format(docstring)
121-
filestring += "{0} atoms\n".format(len(atom_sites))
77+
filestring += "{0} atoms\n".format(len(structure.sites))
12278
filestring += "{0} atom types\n\n".format(len(kind_name_id_map))
12379

124-
lattice = get_matrix(structure.cell)
125-
126-
# As per https://lammps.sandia.gov/doc/Howto_triclinic.html,
127-
# if the lattice does not conform to a regular parallelpiped
128-
# then it must first be rotated
129-
130-
if is_not_zero(lattice[0][1]) or is_not_zero(lattice[0][2]) or is_not_zero(lattice[1][2]):
131-
rotated_cell = True
132-
for site in atom_sites:
133-
site.fractional = cartesian_to_frac(lattice, [site.cartesian])[0]
134-
# creating the cell from its lengths and angles,
135-
# generally ensures that it is in a compatible orientation
136-
atoms = ase.Atoms(cell=structure.cell_lengths + structure.cell_angles)
137-
lattice = get_matrix(atoms.cell)
138-
else:
139-
rotated_cell = False
140-
141-
if is_not_zero(lattice[0][1]):
142-
theta = atan2(-lattice[0][1], lattice[0][0])
143-
rot_matrix = get_matrix([
144-
[cos(theta), sin(theta), 0],
145-
[-sin(theta), cos(theta), 0],
146-
[0, 0, 1]
147-
])
148-
lattice[0] = get_vector(mvmult3(rot_matrix, lattice[0]))
149-
lattice[1] = get_vector(mvmult3(rot_matrix, lattice[1]))
150-
lattice[2] = get_vector(mvmult3(rot_matrix, lattice[2]))
151-
152-
if is_not_zero(lattice[0][2]):
153-
theta = atan2(-lattice[0][2], lattice[0][0])
154-
rot_matrix = get_matrix([
155-
[cos(theta), sin(theta), 0],
156-
[0, 1, 0],
157-
[-sin(theta), cos(theta), 0]
158-
])
159-
lattice[0] = get_vector(mvmult3(rot_matrix, lattice[0]))
160-
lattice[1] = get_vector(mvmult3(rot_matrix, lattice[1]))
161-
lattice[2] = get_vector(mvmult3(rot_matrix, lattice[2]))
80+
atoms = structure.get_ase()
81+
cell, coord_transform = transform_cell(atoms.cell)
82+
positions = np.transpose(np.dot(coord_transform, np.transpose(atoms.positions)))
16283

163-
if is_not_zero(lattice[1][2]):
164-
theta = atan2(-lattice[1][2], lattice[1][1])
165-
rot_matrix = get_matrix([
166-
[1, 0, 0],
167-
[0, cos(theta), sin(theta)],
168-
[0, -sin(theta), cos(theta)]
169-
])
170-
lattice[0] = get_vector(mvmult3(rot_matrix, lattice[0]))
171-
lattice[1] = get_vector(mvmult3(rot_matrix, lattice[1]))
172-
lattice[2] = get_vector(mvmult3(rot_matrix, lattice[2]))
84+
if round_dp:
85+
cell = np.round(cell, round_dp) + 0.
86+
positions = np.round(positions, round_dp) + 0.
17387

174-
if is_not_zero(lattice[0][1]) or is_not_zero(lattice[0][2]) or is_not_zero(lattice[1][2]) or lattice[0][0] < 1e-9 or lattice[1][1] < 1e-9 or lattice[2][2] < 1e-9:
175-
raise ValueError(
176-
"Error in triclinic box: {}\n"
177-
"Vectors should follow these rules: "
178-
"https://lammps.sandia.gov/doc/Howto_triclinic.html".format(lattice))
179-
180-
a = round_by(lattice[0][0], round_dp)
181-
b = round_by(lattice[1][1], round_dp)
182-
c = round_by(lattice[2][2], round_dp)
183-
184-
filestring += "0.0 {0:20.10f} xlo xhi\n".format(a)
185-
filestring += "0.0 {0:20.10f} ylo yhi\n".format(b)
186-
filestring += "0.0 {0:20.10f} zlo zhi\n".format(c)
187-
188-
xy = round_by(lattice[1][0], round_dp)
189-
xz = round_by(lattice[2][0], round_dp)
190-
yz = round_by(lattice[2][1], round_dp)
191-
192-
if is_not_zero(xy) or is_not_zero(xz) or is_not_zero(yz):
193-
filestring += "{0:20.10f} {1:20.10f} {2:20.10f} xy xz yz\n\n".format(
194-
xy, xz, yz)
88+
filestring += "0.0 {0:20.10f} xlo xhi\n".format(cell[0][0])
89+
filestring += "0.0 {0:20.10f} ylo yhi\n".format(cell[1][1])
90+
filestring += "0.0 {0:20.10f} zlo zhi\n".format(cell[2][2])
91+
filestring += "{0:20.10f} {1:20.10f} {2:20.10f} xy xz yz\n\n".format(
92+
cell[1][0], cell[2][0], cell[2][1])
19593

19694
filestring += 'Masses\n\n'
19795
for kind_name in sorted(list(kind_name_id_map.keys())):
@@ -201,12 +99,7 @@ def generate_lammps_structure(structure,
20199

202100
filestring += "Atoms\n\n"
203101

204-
for site_index, site in enumerate(atom_sites):
205-
if rotated_cell:
206-
pos = get_vector(mvmult3(lattice, site.fractional))
207-
else:
208-
pos = site.cartesian
209-
pos = [round_by(v, round_dp) for v in pos]
102+
for site_index, (pos, site) in enumerate(zip(positions, structure.sites)):
210103

211104
kind_id = kind_name_id_map[site.kind_name]
212105

@@ -218,75 +111,6 @@ def generate_lammps_structure(structure,
218111
filestring += "{0} {1} {2} {3:20.10f} {4:20.10f} {5:20.10f}\n".format(
219112
site_index + 1, kind_id, charge, pos[0], pos[1], pos[2])
220113
else:
221-
raise ValueError('atom_style')
222-
223-
return filestring
224-
225-
226-
def old_generate_lammps_structure(structure, atom_style):
227-
""" this is the deprecated method, used before 0.3.0b3,
228-
stored here for prosperity.
229-
230-
This method can create erroneous structures for triclinic cells
231-
"""
232-
import numpy as np
233-
234-
types = [site.kind_name for site in structure.sites]
235-
236-
type_index_unique = np.unique(types, return_index=True)[1]
237-
count_index_unique = np.diff(np.append(type_index_unique, [len(types)]))
238-
239-
atom_index = []
240-
for i, index in enumerate(count_index_unique):
241-
atom_index += [i for j in range(index)]
242-
243-
masses = [site.mass for site in structure.kinds]
244-
positions = [site.position for site in structure.sites]
245-
246-
number_of_atoms = len(positions)
247-
248-
lammps_data_file = 'Generated using dynaphopy\n\n'
249-
lammps_data_file += '{0} atoms\n\n'.format(number_of_atoms)
250-
lammps_data_file += '{0} atom types\n\n'.format(len(masses))
251-
252-
cell = np.array(structure.cell)
253-
254-
a = np.linalg.norm(cell[0])
255-
b = np.linalg.norm(cell[1])
256-
c = np.linalg.norm(cell[2])
257-
258-
alpha = np.arccos(np.dot(cell[1], cell[2]) / (c * b))
259-
gamma = np.arccos(np.dot(cell[1], cell[0]) / (a * b))
260-
beta = np.arccos(np.dot(cell[2], cell[0]) / (a * c))
261-
262-
xhi = a
263-
xy = b * np.cos(gamma)
264-
xz = c * np.cos(beta)
265-
yhi = np.sqrt(pow(b, 2) - pow(xy, 2))
266-
yz = (b * c * np.cos(alpha) - xy * xz) / yhi
267-
zhi = np.sqrt(pow(c, 2) - pow(xz, 2) - pow(yz, 2))
268-
269-
xhi = xhi + max(0, 0, xy, xz, xy + xz)
270-
yhi = yhi + max(0, 0, yz)
271-
272-
lammps_data_file += '\n{0:20.10f} {1:20.10f} xlo xhi\n'.format(0, xhi)
273-
lammps_data_file += '{0:20.10f} {1:20.10f} ylo yhi\n'.format(0, yhi)
274-
lammps_data_file += '{0:20.10f} {1:20.10f} zlo zhi\n'.format(0, zhi)
275-
lammps_data_file += '{0:20.10f} {1:20.10f} {2:20.10f} xy xz yz\n\n'.format(
276-
xy, xz, yz)
277-
278-
lammps_data_file += 'Masses\n\n'
279-
280-
for i, mass in enumerate(masses):
281-
lammps_data_file += '{0} {1:20.10f} \n'.format(i + 1, mass)
282-
283-
lammps_data_file += '\nAtoms\n\n'
284-
for i, row in enumerate(positions):
285-
if atom_style == 'charge':
286-
lammps_data_file += '{0} {1} 0.0 {2:20.10f} {3:20.10f} {4:20.10f}\n'.format(
287-
i + 1, atom_index[i] + 1, row[0], row[1], row[2])
288-
else:
289-
lammps_data_file += '{0} {1} {2:20.10f} {3:20.10f} {4:20.10f}\n'.format(
290-
i + 1, atom_index[i] + 1, row[0], row[1], row[2])
114+
raise ValueError('atom_style unknown: {}'.format(atom_style))
291115

292-
return lammps_data_file
116+
return filestring, coord_transform

aiida_lammps/tests/test_generate_structure.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@
1111
])
1212
def test_generate(db_test_app, get_structure_data, structure, file_regression):
1313
structure = get_structure_data(structure)
14-
file_regression.check(six.ensure_text(
15-
generate_lammps_structure(structure, round_dp=8)))
14+
text, transform = generate_lammps_structure(structure, round_dp=8)
15+
file_regression.check(six.ensure_text(text))

aiida_lammps/tests/test_generate_structure/test_generate_Fe_.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
0.0 2.8481160000 xlo xhi
77
0.0 2.8481160000 ylo yhi
88
0.0 2.8481160000 zlo zhi
9+
0.0000000000 0.0000000000 0.0000000000 xy xz yz
10+
911
Masses
1012

1113
1 55.8450000000

aiida_lammps/tests/test_generate_structure/test_generate_pyrite_.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
0.0 5.3800000000 xlo xhi
77
0.0 5.3800000000 ylo yhi
88
0.0 5.3800000000 zlo zhi
9+
0.0000000000 0.0000000000 0.0000000000 xy xz yz
10+
911
Masses
1012

1113
1 55.8450000000

0 commit comments

Comments
 (0)