Coverage for /builds/debichem-team/python-ase/ase/io/lammpsdata.py: 94.08%

338 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1import re 

2import warnings 

3 

4import numpy as np 

5 

6from ase.atoms import Atoms 

7from ase.calculators.lammps import Prism, convert 

8from ase.data import atomic_masses, atomic_numbers 

9from ase.utils import reader, writer 

10 

11 

12@reader 

13def read_lammps_data( 

14 fileobj, 

15 Z_of_type: dict = None, 

16 sort_by_id: bool = True, 

17 read_image_flags: bool = True, 

18 units: str = 'metal', 

19 atom_style: str = None, 

20 style: str = None, 

21): 

22 """Method which reads a LAMMPS data file. 

23 

24 Parameters 

25 ---------- 

26 fileobj : file | str 

27 File from which data should be read. 

28 Z_of_type : dict[int, int], optional 

29 Mapping from LAMMPS atom types (typically starting from 1) to atomic 

30 numbers. If None, if there is the "Masses" section, atomic numbers are 

31 guessed from the atomic masses. Otherwise, atomic numbers of 1 (H), 2 

32 (He), etc. are assigned to atom types of 1, 2, etc. Default is None. 

33 sort_by_id : bool, optional 

34 Order the particles according to their id. Might be faster to set it 

35 False. Default is True. 

36 read_image_flags: bool, default True 

37 If True, the lattice translation vectors derived from image flags are 

38 added to atomic positions. 

39 units : str, optional 

40 `LAMMPS units <https://docs.lammps.org/units.html>`__. Default is 

41 'metal'. 

42 atom_style : {'atomic', 'charge', 'full'} etc., optional 

43 `LAMMPS atom style <https://docs.lammps.org/atom_style.html>`__. 

44 If None, `atom_style` is guessed in the following priority (1) comment 

45 after `Atoms` (2) length of fields (valid only `atomic` and `full`). 

46 Default is None. 

47 """ 

48 if style is not None: 

49 warnings.warn( 

50 FutureWarning('"style" is deprecated; please use "atom_style".'), 

51 ) 

52 atom_style = style 

53 # begin read_lammps_data 

54 file_comment = next(fileobj).rstrip() 

55 

56 # default values (https://docs.lammps.org/read_data.html) 

57 # in most cases these will be updated below 

58 natoms = 0 

59 # N_types = 0 

60 xlo, xhi = -0.5, 0.5 

61 ylo, yhi = -0.5, 0.5 

62 zlo, zhi = -0.5, 0.5 

63 xy, xz, yz = 0.0, 0.0, 0.0 

64 

65 mass_in = {} 

66 vel_in = {} 

67 bonds_in = [] 

68 angles_in = [] 

69 dihedrals_in = [] 

70 

71 sections = [ 

72 'Atoms', 

73 'Velocities', 

74 'Masses', 

75 'Charges', 

76 'Ellipsoids', 

77 'Lines', 

78 'Triangles', 

79 'Bodies', 

80 'Bonds', 

81 'Angles', 

82 'Dihedrals', 

83 'Impropers', 

84 'Impropers Pair Coeffs', 

85 'PairIJ Coeffs', 

86 'Pair Coeffs', 

87 'Bond Coeffs', 

88 'Angle Coeffs', 

89 'Dihedral Coeffs', 

90 'Improper Coeffs', 

91 'BondBond Coeffs', 

92 'BondAngle Coeffs', 

93 'MiddleBondTorsion Coeffs', 

94 'EndBondTorsion Coeffs', 

95 'AngleTorsion Coeffs', 

96 'AngleAngleTorsion Coeffs', 

97 'BondBond13 Coeffs', 

98 'AngleAngle Coeffs', 

99 ] 

100 header_fields = [ 

101 'atoms', 

102 'bonds', 

103 'angles', 

104 'dihedrals', 

105 'impropers', 

106 'atom types', 

107 'bond types', 

108 'angle types', 

109 'dihedral types', 

110 'improper types', 

111 'extra bond per atom', 

112 'extra angle per atom', 

113 'extra dihedral per atom', 

114 'extra improper per atom', 

115 'extra special per atom', 

116 'ellipsoids', 

117 'lines', 

118 'triangles', 

119 'bodies', 

120 'xlo xhi', 

121 'ylo yhi', 

122 'zlo zhi', 

123 'xy xz yz', 

124 ] 

125 sections_re = '(' + '|'.join(sections).replace(' ', '\\s+') + ')' 

126 header_fields_re = '(' + '|'.join(header_fields).replace(' ', '\\s+') + ')' 

127 

128 section = None 

129 header = True 

130 for line in fileobj: 

131 # get string after #; if # does not exist, return '' 

132 line_comment = re.sub(r'^.*#|^.*$', '', line).strip() 

133 line = re.sub('#.*', '', line).rstrip().lstrip() 

134 if re.match('^\\s*$', line): # skip blank lines 

135 continue 

136 

137 # check for known section names 

138 match = re.match(sections_re, line) 

139 if match is not None: 

140 section = match.group(0).rstrip().lstrip() 

141 header = False 

142 if section == 'Atoms': # id * 

143 # guess `atom_style` from the comment after `Atoms` if exists 

144 if atom_style is None and line_comment != '': 

145 atom_style = line_comment 

146 mol_id_in, type_in, charge_in, pos_in, travel_in = \ 

147 _read_atoms_section(fileobj, natoms, atom_style) 

148 continue 

149 

150 if header: 

151 field = None 

152 val = None 

153 match = re.match('(.*)\\s+' + header_fields_re, line) 

154 if match is not None: 

155 field = match.group(2).lstrip().rstrip() 

156 val = match.group(1).lstrip().rstrip() 

157 if field is not None and val is not None: 

158 if field == 'atoms': 

159 natoms = int(val) 

160 elif field == 'xlo xhi': 

161 (xlo, xhi) = (float(x) for x in val.split()) 

162 elif field == 'ylo yhi': 

163 (ylo, yhi) = (float(x) for x in val.split()) 

164 elif field == 'zlo zhi': 

165 (zlo, zhi) = (float(x) for x in val.split()) 

166 elif field == 'xy xz yz': 

167 (xy, xz, yz) = (float(x) for x in val.split()) 

168 

169 if section is not None: 

170 fields = line.split() 

171 if section == 'Velocities': # id vx vy vz 

172 atom_id = int(fields[0]) 

173 vel_in[atom_id] = [float(fields[_]) for _ in (1, 2, 3)] 

174 elif section == 'Masses': 

175 mass_in[int(fields[0])] = float(fields[1]) 

176 elif section == 'Bonds': # id type atom1 atom2 

177 bonds_in.append([int(fields[_]) for _ in (1, 2, 3)]) 

178 elif section == 'Angles': # id type atom1 atom2 atom3 

179 angles_in.append([int(fields[_]) for _ in (1, 2, 3, 4)]) 

180 elif section == 'Dihedrals': # id type atom1 atom2 atom3 atom4 

181 dihedrals_in.append([int(fields[_]) for _ in (1, 2, 3, 4, 5)]) 

182 

183 # set cell 

184 cell = np.zeros((3, 3)) 

185 cell[0, 0] = xhi - xlo 

186 cell[1, 1] = yhi - ylo 

187 cell[2, 2] = zhi - zlo 

188 cell[1, 0] = xy 

189 cell[2, 0] = xz 

190 cell[2, 1] = yz 

191 

192 # initialize arrays for per-atom quantities 

193 positions = np.zeros((natoms, 3)) 

194 numbers = np.zeros(natoms, int) 

195 ids = np.zeros(natoms, int) 

196 types = np.zeros(natoms, int) 

197 velocities = np.zeros((natoms, 3)) if len(vel_in) > 0 else None 

198 masses = np.zeros(natoms) if len(mass_in) > 0 else None 

199 mol_id = np.zeros(natoms, int) if len(mol_id_in) > 0 else None 

200 charge = np.zeros(natoms, float) if len(charge_in) > 0 else None 

201 travel = np.zeros((natoms, 3), int) if len(travel_in) > 0 else None 

202 bonds = [''] * natoms if len(bonds_in) > 0 else None 

203 angles = [''] * natoms if len(angles_in) > 0 else None 

204 dihedrals = [''] * natoms if len(dihedrals_in) > 0 else None 

205 

206 ind_of_id = {} 

207 # copy per-atom quantities from read-in values 

208 for (i, atom_id) in enumerate(pos_in.keys()): 

209 # by id 

210 if sort_by_id: 

211 ind = atom_id - 1 

212 else: 

213 ind = i 

214 ind_of_id[atom_id] = ind 

215 atom_type = type_in[atom_id] 

216 positions[ind, :] = pos_in[atom_id] 

217 if velocities is not None: 

218 velocities[ind, :] = vel_in[atom_id] 

219 if travel is not None: 

220 travel[ind] = travel_in[atom_id] 

221 if mol_id is not None: 

222 mol_id[ind] = mol_id_in[atom_id] 

223 if charge is not None: 

224 charge[ind] = charge_in[atom_id] 

225 ids[ind] = atom_id 

226 # by type 

227 types[ind] = atom_type 

228 if Z_of_type is None: 

229 numbers[ind] = atom_type 

230 else: 

231 numbers[ind] = Z_of_type[atom_type] 

232 if masses is not None: 

233 masses[ind] = mass_in[atom_type] 

234 # convert units 

235 positions = convert(positions, 'distance', units, 'ASE') 

236 cell = convert(cell, 'distance', units, 'ASE') 

237 if masses is not None: 

238 masses = convert(masses, 'mass', units, 'ASE') 

239 if velocities is not None: 

240 velocities = convert(velocities, 'velocity', units, 'ASE') 

241 

242 # guess atomic numbers from atomic masses 

243 # this must be after the above mass-unit conversion 

244 if Z_of_type is None and masses is not None: 

245 numbers = _masses2numbers(masses) 

246 

247 # create ase.Atoms 

248 atoms = Atoms( 

249 positions=positions, 

250 numbers=numbers, 

251 masses=masses, 

252 cell=cell, 

253 pbc=[True, True, True], 

254 ) 

255 

256 # add lattice translation vectors 

257 if read_image_flags and travel is not None: 

258 scaled_positions = atoms.get_scaled_positions(wrap=False) + travel 

259 atoms.set_scaled_positions(scaled_positions) 

260 

261 # set velocities (can't do it via constructor) 

262 if velocities is not None: 

263 atoms.set_velocities(velocities) 

264 

265 atoms.arrays['id'] = ids 

266 atoms.arrays['type'] = types 

267 if mol_id is not None: 

268 atoms.arrays['mol-id'] = mol_id 

269 if charge is not None: 

270 atoms.arrays['initial_charges'] = charge 

271 atoms.arrays['mmcharges'] = charge.copy() 

272 

273 if bonds is not None: 

274 for (atom_type, at1, at2) in bonds_in: 

275 i_a1 = ind_of_id[at1] 

276 i_a2 = ind_of_id[at2] 

277 if len(bonds[i_a1]) > 0: 

278 bonds[i_a1] += ',' 

279 bonds[i_a1] += f'{i_a2:d}({atom_type:d})' 

280 for i, bond in enumerate(bonds): 

281 if len(bond) == 0: 

282 bonds[i] = '_' 

283 atoms.arrays['bonds'] = np.array(bonds) 

284 

285 if angles is not None: 

286 for (atom_type, at1, at2, at3) in angles_in: 

287 i_a1 = ind_of_id[at1] 

288 i_a2 = ind_of_id[at2] 

289 i_a3 = ind_of_id[at3] 

290 if len(angles[i_a2]) > 0: 

291 angles[i_a2] += ',' 

292 angles[i_a2] += f'{i_a1:d}-{i_a3:d}({atom_type:d})' 

293 for i, angle in enumerate(angles): 

294 if len(angle) == 0: 

295 angles[i] = '_' 

296 atoms.arrays['angles'] = np.array(angles) 

297 

298 if dihedrals is not None: 

299 for (atom_type, at1, at2, at3, at4) in dihedrals_in: 

300 i_a1 = ind_of_id[at1] 

301 i_a2 = ind_of_id[at2] 

302 i_a3 = ind_of_id[at3] 

303 i_a4 = ind_of_id[at4] 

304 if len(dihedrals[i_a1]) > 0: 

305 dihedrals[i_a1] += ',' 

306 dihedrals[i_a1] += f'{i_a2:d}-{i_a3:d}-{i_a4:d}({atom_type:d})' 

307 for i, dihedral in enumerate(dihedrals): 

308 if len(dihedral) == 0: 

309 dihedrals[i] = '_' 

310 atoms.arrays['dihedrals'] = np.array(dihedrals) 

311 

312 atoms.info['comment'] = file_comment 

313 

314 return atoms 

315 

316 

317def _read_atoms_section(fileobj, natoms: int, style: str = None): 

318 type_in = {} 

319 mol_id_in = {} 

320 charge_in = {} 

321 pos_in = {} 

322 travel_in = {} 

323 next(fileobj) # skip blank line just after `Atoms` 

324 for _ in range(natoms): 

325 line = next(fileobj) 

326 line = re.sub('#.*', '', line).rstrip().lstrip() 

327 fields = line.split() 

328 if style is None: 

329 style = _guess_atom_style(fields) 

330 atom_id = int(fields[0]) 

331 if style == 'full' and len(fields) in (7, 10): 

332 # id mol-id type q x y z [tx ty tz] 

333 type_in[atom_id] = int(fields[2]) 

334 pos_in[atom_id] = tuple(float(fields[_]) for _ in (4, 5, 6)) 

335 mol_id_in[atom_id] = int(fields[1]) 

336 charge_in[atom_id] = float(fields[3]) 

337 if len(fields) == 10: 

338 travel_in[atom_id] = tuple(int(fields[_]) for _ in (7, 8, 9)) 

339 elif style == 'atomic' and len(fields) in (5, 8): 

340 # id type x y z [tx ty tz] 

341 type_in[atom_id] = int(fields[1]) 

342 pos_in[atom_id] = tuple(float(fields[_]) for _ in (2, 3, 4)) 

343 if len(fields) == 8: 

344 travel_in[atom_id] = tuple(int(fields[_]) for _ in (5, 6, 7)) 

345 elif style in ('angle', 'bond', 'molecular') and len(fields) in (6, 9): 

346 # id mol-id type x y z [tx ty tz] 

347 type_in[atom_id] = int(fields[2]) 

348 pos_in[atom_id] = tuple(float(fields[_]) for _ in (3, 4, 5)) 

349 mol_id_in[atom_id] = int(fields[1]) 

350 if len(fields) == 9: 

351 travel_in[atom_id] = tuple(int(fields[_]) for _ in (6, 7, 8)) 

352 elif style == 'charge' and len(fields) in (6, 9): 

353 # id type q x y z [tx ty tz] 

354 type_in[atom_id] = int(fields[1]) 

355 pos_in[atom_id] = tuple(float(fields[_]) for _ in (3, 4, 5)) 

356 charge_in[atom_id] = float(fields[2]) 

357 if len(fields) == 9: 

358 travel_in[atom_id] = tuple(int(fields[_]) for _ in (6, 7, 8)) 

359 else: 

360 raise RuntimeError( 

361 f'Style "{style}" not supported or invalid. ' 

362 f'Number of fields: {len(fields)}' 

363 ) 

364 return mol_id_in, type_in, charge_in, pos_in, travel_in 

365 

366 

367def _guess_atom_style(fields): 

368 """Guess `atom_sytle` from the length of fields.""" 

369 if len(fields) in (5, 8): 

370 return 'atomic' 

371 if len(fields) in (7, 10): 

372 return 'full' 

373 raise ValueError('atom_style cannot be guessed from len(fields)') 

374 

375 

376def _masses2numbers(masses): 

377 """Guess atomic numbers from atomic masses.""" 

378 return np.argmin(np.abs(atomic_masses - masses[:, None]), axis=1) 

379 

380 

381@writer 

382def write_lammps_data( 

383 fd, 

384 atoms: Atoms, 

385 *, 

386 specorder: list = None, 

387 reduce_cell: bool = False, 

388 force_skew: bool = False, 

389 prismobj: Prism = None, 

390 write_image_flags: bool = False, 

391 masses: bool = False, 

392 velocities: bool = False, 

393 units: str = 'metal', 

394 bonds: bool = True, 

395 atom_style: str = 'atomic', 

396): 

397 """Write atomic structure data to a LAMMPS data file. 

398 

399 Parameters 

400 ---------- 

401 fd : file|str 

402 File to which the output will be written. 

403 atoms : Atoms 

404 Atoms to be written. 

405 specorder : list[str], optional 

406 Chemical symbols in the order of LAMMPS atom types, by default None 

407 force_skew : bool, optional 

408 Force to write the cell as a 

409 `triclinic <https://docs.lammps.org/Howto_triclinic.html>`__ box, 

410 by default False 

411 reduce_cell : bool, optional 

412 Whether the cell shape is reduced or not, by default False 

413 prismobj : Prism|None, optional 

414 Prism, by default None 

415 write_image_flags : bool, default False 

416 If True, the image flags, i.e., in which images of the periodic 

417 simulation box the atoms are, are written. 

418 masses : bool, optional 

419 Whether the atomic masses are written or not, by default False 

420 velocities : bool, optional 

421 Whether the atomic velocities are written or not, by default False 

422 units : str, optional 

423 `LAMMPS units <https://docs.lammps.org/units.html>`__, 

424 by default 'metal' 

425 bonds : bool, optional 

426 Whether the bonds are written or not. Bonds can only be written 

427 for atom_style='full', by default True 

428 atom_style : {'atomic', 'charge', 'full'}, optional 

429 `LAMMPS atom style <https://docs.lammps.org/atom_style.html>`__, 

430 by default 'atomic' 

431 """ 

432 

433 # FIXME: We should add a check here that the encoding of the file object 

434 # is actually ascii once the 'encoding' attribute of IOFormat objects 

435 # starts functioning in implementation (currently it doesn't do 

436 # anything). 

437 

438 if isinstance(atoms, list): 

439 if len(atoms) > 1: 

440 raise ValueError( 

441 'Can only write one configuration to a lammps data file!' 

442 ) 

443 atoms = atoms[0] 

444 

445 fd.write('(written by ASE)\n\n') 

446 

447 symbols = atoms.get_chemical_symbols() 

448 n_atoms = len(symbols) 

449 fd.write(f'{n_atoms} atoms\n') 

450 

451 if specorder is None: 

452 # This way it is assured that LAMMPS atom types are always 

453 # assigned predictably according to the alphabetic order 

454 species = sorted(set(symbols)) 

455 else: 

456 # To index elements in the LAMMPS data file 

457 # (indices must correspond to order in the potential file) 

458 species = specorder 

459 n_atom_types = len(species) 

460 fd.write(f'{n_atom_types} atom types\n\n') 

461 

462 bonds_in = [] 

463 if (bonds and (atom_style == 'full') and 

464 (atoms.arrays.get('bonds') is not None)): 

465 n_bonds = 0 

466 n_bond_types = 1 

467 for i, bondsi in enumerate(atoms.arrays['bonds']): 

468 if bondsi != '_': 

469 for bond in bondsi.split(','): 

470 dummy1, dummy2 = bond.split('(') 

471 bond_type = int(dummy2.split(')')[0]) 

472 at1 = int(i) + 1 

473 at2 = int(dummy1) + 1 

474 bonds_in.append((bond_type, at1, at2)) 

475 n_bonds = n_bonds + 1 

476 if bond_type > n_bond_types: 

477 n_bond_types = bond_type 

478 fd.write(f'{n_bonds} bonds\n') 

479 fd.write(f'{n_bond_types} bond types\n\n') 

480 

481 if prismobj is None: 

482 prismobj = Prism(atoms.get_cell(), reduce_cell=reduce_cell) 

483 

484 # Get cell parameters and convert from ASE units to LAMMPS units 

485 xhi, yhi, zhi, xy, xz, yz = convert( 

486 prismobj.get_lammps_prism(), 'distance', 'ASE', units) 

487 

488 fd.write(f'0.0 {xhi:23.17g} xlo xhi\n') 

489 fd.write(f'0.0 {yhi:23.17g} ylo yhi\n') 

490 fd.write(f'0.0 {zhi:23.17g} zlo zhi\n') 

491 

492 if force_skew or prismobj.is_skewed(): 

493 fd.write(f'{xy:23.17g} {xz:23.17g} {yz:23.17g} xy xz yz\n') 

494 fd.write('\n') 

495 

496 if masses: 

497 _write_masses(fd, atoms, species, units) 

498 

499 # Write (unwrapped) atomic positions. If wrapping of atoms back into the 

500 # cell along periodic directions is desired, this should be done manually 

501 # on the Atoms object itself beforehand. 

502 fd.write(f'Atoms # {atom_style}\n\n') 

503 

504 if write_image_flags: 

505 scaled_positions = atoms.get_scaled_positions(wrap=False) 

506 image_flags = np.floor(scaled_positions).astype(int) 

507 

508 # when `write_image_flags` is True, the positions are wrapped while the 

509 # unwrapped positions can be recovered from the image flags 

510 pos = prismobj.vector_to_lammps( 

511 atoms.get_positions(), 

512 wrap=write_image_flags, 

513 ) 

514 

515 if atom_style == 'atomic': 

516 # Convert position from ASE units to LAMMPS units 

517 pos = convert(pos, 'distance', 'ASE', units) 

518 for i, r in enumerate(pos): 

519 s = species.index(symbols[i]) + 1 

520 line = ( 

521 f'{i + 1:>6} {s:>3}' 

522 f' {r[0]:23.17g} {r[1]:23.17g} {r[2]:23.17g}' 

523 ) 

524 if write_image_flags: 

525 img = image_flags[i] 

526 line += f' {img[0]:6d} {img[1]:6d} {img[2]:6d}' 

527 line += '\n' 

528 fd.write(line) 

529 elif atom_style == 'charge': 

530 charges = atoms.get_initial_charges() 

531 # Convert position and charge from ASE units to LAMMPS units 

532 pos = convert(pos, 'distance', 'ASE', units) 

533 charges = convert(charges, 'charge', 'ASE', units) 

534 for i, (q, r) in enumerate(zip(charges, pos)): 

535 s = species.index(symbols[i]) + 1 

536 line = ( 

537 f'{i + 1:>6} {s:>3} {q:>5}' 

538 f' {r[0]:23.17g} {r[1]:23.17g} {r[2]:23.17g}' 

539 ) 

540 if write_image_flags: 

541 img = image_flags[i] 

542 line += f' {img[0]:6d} {img[1]:6d} {img[2]:6d}' 

543 line += '\n' 

544 fd.write(line) 

545 elif atom_style == 'full': 

546 charges = atoms.get_initial_charges() 

547 # The label 'mol-id' has apparenlty been introduced in read earlier, 

548 # but so far not implemented here. Wouldn't a 'underscored' label 

549 # be better, i.e. 'mol_id' or 'molecule_id'? 

550 if atoms.has('mol-id'): 

551 molecules = atoms.get_array('mol-id') 

552 if not np.issubdtype(molecules.dtype, np.integer): 

553 raise TypeError( 

554 f'If "atoms" object has "mol-id" array, then ' 

555 f'mol-id dtype must be subtype of np.integer, and ' 

556 f'not {molecules.dtype!s:s}.') 

557 if (len(molecules) != len(atoms)) or (molecules.ndim != 1): 

558 raise TypeError( 

559 'If "atoms" object has "mol-id" array, then ' 

560 'each atom must have exactly one mol-id.') 

561 else: 

562 # Assigning each atom to a distinct molecule id would seem 

563 # preferableabove assigning all atoms to a single molecule 

564 # id per default, as done within ase <= v 3.19.1. I.e., 

565 # molecules = np.arange(start=1, stop=len(atoms)+1, 

566 # step=1, dtype=int) However, according to LAMMPS default 

567 # behavior, 

568 molecules = np.zeros(len(atoms), dtype=int) 

569 # which is what happens if one creates new atoms within LAMMPS 

570 # without explicitly taking care of the molecule id. 

571 # Quote from docs at https://lammps.sandia.gov/doc/read_data.html: 

572 # The molecule ID is a 2nd identifier attached to an atom. 

573 # Normally, it is a number from 1 to N, identifying which 

574 # molecule the atom belongs to. It can be 0 if it is a 

575 # non-bonded atom or if you don't care to keep track of molecule 

576 # assignments. 

577 

578 # Convert position and charge from ASE units to LAMMPS units 

579 pos = convert(pos, 'distance', 'ASE', units) 

580 charges = convert(charges, 'charge', 'ASE', units) 

581 for i, (m, q, r) in enumerate(zip(molecules, charges, pos)): 

582 s = species.index(symbols[i]) + 1 

583 line = ( 

584 f'{i + 1:>6} {m:>3} {s:>3} {q:>5}' 

585 f' {r[0]:23.17g} {r[1]:23.17g} {r[2]:23.17g}' 

586 ) 

587 if write_image_flags: 

588 img = image_flags[i] 

589 line += f' {img[0]:6d} {img[1]:6d} {img[2]:6d}' 

590 line += '\n' 

591 fd.write(line) 

592 if bonds and (atoms.arrays.get('bonds') is not None): 

593 fd.write('\nBonds\n\n') 

594 for i in range(n_bonds): 

595 bond_type = bonds_in[i][0] 

596 at1 = bonds_in[i][1] 

597 at2 = bonds_in[i][2] 

598 fd.write(f'{i + 1:>3} {bond_type:>3} {at1:>3} {at2:>3}\n') 

599 else: 

600 raise ValueError(atom_style) 

601 

602 if velocities and atoms.get_velocities() is not None: 

603 fd.write('\n\nVelocities\n\n') 

604 vel = prismobj.vector_to_lammps(atoms.get_velocities()) 

605 # Convert velocity from ASE units to LAMMPS units 

606 vel = convert(vel, 'velocity', 'ASE', units) 

607 for i, v in enumerate(vel): 

608 fd.write(f'{i + 1:>6} {v[0]:23.17g} {v[1]:23.17g} {v[2]:23.17g}\n') 

609 

610 fd.flush() 

611 

612 

613def _write_masses(fd, atoms: Atoms, species: list, units: str): 

614 symbols_indices = atoms.symbols.indices() 

615 fd.write('Masses\n\n') 

616 for i, s in enumerate(species): 

617 if s in symbols_indices: 

618 # Find the first atom of the element `s` and extract its mass 

619 # Cover by `float` to make a new object for safety 

620 mass = float(atoms[symbols_indices[s][0]].mass) 

621 else: 

622 # Fetch from ASE data if the element `s` is not in the system 

623 mass = atomic_masses[atomic_numbers[s]] 

624 # Convert mass from ASE units to LAMMPS units 

625 mass = convert(mass, 'mass', 'ASE', units) 

626 atom_type = i + 1 

627 fd.write(f'{atom_type:>6} {mass:23.17g} # {s}\n') 

628 fd.write('\n')