oILAB
Loading...
Searching...
No Matches
lammps_writer.py
Go to the documentation of this file.
1import numpy as np
2
3
4def compute_triclinic_box(ell1, ell2, zlo=-20.0, zhi=20.0):
5 """
6 Convert two 2D Cartesian box vectors into LAMMPS triclinic box parameters.
7
8 Assumes the user has already rotated the system, if desired.
9
10 Parameters
11 ----------
12 ell1, ell2 : array-like
13 2D Cartesian simulation cell vectors.
14 zlo, zhi : float
15 Lower and upper z bounds.
16
17 Returns
18 -------
19 dict
20 Dictionary with xlo, xhi, ylo, yhi, zlo, zhi, xy, xz, yz.
21 """
22 ell1 = np.asarray(ell1, dtype=float).reshape(2)
23 ell2 = np.asarray(ell2, dtype=float).reshape(2)
24
25 # Standard 2D triclinic reduction used by LAMMPS
26 lx = np.linalg.norm(ell1)
27 if lx <= 0.0:
28 raise ValueError("ell1 has zero length.")
29
30 ex = ell1 / lx
31 xy = np.dot(ell2, ex)
32
33 ell2_perp = ell2 - xy * ex
34 ly = np.linalg.norm(ell2_perp)
35 if ly <= 0.0:
36 raise ValueError("ell1 and ell2 are collinear.")
37
38 return {
39 "xlo": 0.0,
40 "xhi": float(lx),
41 "ylo": 0.0,
42 "yhi": float(ly),
43 "zlo": float(zlo),
44 "zhi": float(zhi),
45 "xy": float(xy),
46 "xz": 0.0,
47 "yz": 0.0,
48 }
49
50
51def write_lammps_data(
52 filename,
53 positions,
54 atom_types,
55 charges,
56 ell1,
57 ell2,
58 atom_style="atomic",
59 molecule_ids=None,
60 z_padding=20.0,
61 comment="LAMMPS data file",
62):
63 """
64 Write a LAMMPS data file for atomistic simulations.
65
66 This function generates a triclinic simulation box from the in-plane
67 lattice vectors (ell1, ell2) and writes atomic data in either
68 'atomic' or 'full' atom_style format.
69
70 Parameters
71 ----------
72 filename : str
73 Output file name.
74
75 positions : list[(x, y, z)]
76 Cartesian atomic positions.
77
78 atom_types : list[int]
79 Integer atom types (1-based), consistent with LAMMPS conventions.
80
81 charges : list[float]
82 Atomic charges. Used only when atom_style='full'. Ignored otherwise.
83
84 ell1, ell2 : array-like
85 2D Cartesian lattice vectors defining the in-plane periodic cell.
86 These are converted internally into a triclinic LAMMPS box.
87
88 atom_style : str, optional
89 LAMMPS atom style. Supported options:
90 - 'atomic' : writes (id, type, x, y, z)
91 - 'full' : writes (id, molecule-ID, type, charge, x, y, z)
92
93 molecule_ids : list[int] or None, optional
94 Molecule (or layer) IDs for each atom, required for certain
95 many-body or interlayer potentials (e.g., ilp/tmd).
96 If None and atom_style='full', all atoms are assigned molecule ID = 1.
97
98 z_padding : float, optional
99 Half-width of the simulation box in the z-direction. The box is
100 constructed as [ -z_padding, z_padding ]. This should be large
101 enough to avoid spurious interactions between periodic images
102 in z when using boundary p p p.
103
104 comment : str, optional
105 Header comment written at the top of the data file.
106 """
107 if len(positions) != len(atom_types):
108 raise ValueError("positions and atom_types must have the same length.")
109
110 if atom_style == "full" and len(positions) != len(charges):
111 raise ValueError("positions and charges must have the same length for atom_style='full'.")
112
113 if atom_style == "full" and molecule_ids is not None and len(positions) != len(molecule_ids):
114 raise ValueError("positions and molecule_ids must have the same length for atom_style='full'.")
115
116 box = compute_triclinic_box(ell1, ell2, zlo=-z_padding, zhi=z_padding)
117
118 n_atoms = len(positions)
119 n_atom_types = max(atom_types)
120
121 with open(filename, "w", encoding="utf-8") as f:
122 f.write(f"{comment}\n\n")
123 f.write(f"{n_atoms} atoms\n")
124 f.write(f"{n_atom_types} atom types\n\n")
125
126 f.write(f"{box['xlo']:.16f} {box['xhi']:.16f} xlo xhi\n")
127 f.write(f"{box['ylo']:.16f} {box['yhi']:.16f} ylo yhi\n")
128 f.write(f"{box['zlo']:.16f} {box['zhi']:.16f} zlo zhi\n")
129 f.write(f"{box['xy']:.16f} {box['xz']:.16f} {box['yz']:.16f} xy xz yz\n\n")
130
131 if atom_style == "atomic":
132 f.write("Atoms # atomic\n\n")
133 for i, ((x, y, z), atom_type) in enumerate(zip(positions, atom_types), start=1):
134 f.write(f"{i:d} {atom_type:d} {x:.16f} {y:.16f} {z:.16f}\n")
135
136 elif atom_style == "full":
137 f.write("Atoms # full\n\n")
138 if molecule_ids is None:
139 molecule_ids = [1] * n_atoms
140
141 for i, ((x, y, z), mol_id, atom_type, charge) in enumerate(
142 zip(positions, molecule_ids, atom_types, charges), start=1
143 ):
144 f.write(
145 f"{i:d} {mol_id:d} {atom_type:d} "
146 f"{charge:.16f} {x:.16f} {y:.16f} {z:.16f}\n"
147 )
148 else:
149 raise ValueError("atom_style must be either 'atomic' or 'full'.")
150
151
152def summarize_configuration(positions, atom_types, ell1, ell2):
153 """
154 Print a compact summary of the configuration.
155 """
156 print("\nConfiguration summary")
157 print("---------------------")
158 print(f"Number of atoms : {len(positions)}")
159 print(f"Number of atom types : {max(atom_types)}")
160 print(f"ell1 : {np.asarray(ell1, dtype=float)}")
161 print(f"ell2 : {np.asarray(ell2, dtype=float)}")
compute_triclinic_box(ell1, ell2, zlo=-20.0, zhi=20.0)