Coverage for /builds/ase/ase/ase/calculators/lammpslib.py: 73.79%

290 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3"""ASE LAMMPS Calculator Library Version""" 

4 

5 

6import ctypes 

7 

8import numpy as np 

9from numpy.linalg import norm 

10 

11from ase import Atoms 

12from ase.calculators.calculator import Calculator 

13from ase.calculators.lammps import Prism, convert 

14from ase.data import atomic_masses as ase_atomic_masses 

15from ase.data import atomic_numbers as ase_atomic_numbers 

16from ase.data import chemical_symbols as ase_chemical_symbols 

17from ase.utils import deprecated 

18 

19# TODO 

20# 1. should we make a new lammps object each time ? 

21# 4. need a routine to get the model back from lammps 

22# 5. if we send a command to lmps directly then the calculator does 

23# not know about it and the energy could be wrong. 

24# 6. do we need a subroutine generator that converts a lammps string 

25# into a python function that can be called 

26# 8. make matscipy as fallback 

27# 9. keep_alive not needed with no system changes 

28 

29 

30# this one may be moved to some more generic place 

31@deprecated("Please use the technique in https://stackoverflow.com/a/26912166") 

32def is_upper_triangular(arr, atol=1e-8): 

33 """test for upper triangular matrix based on numpy 

34 .. deprecated:: 3.23.0 

35 Please use the technique in https://stackoverflow.com/a/26912166 

36 """ 

37 # must be (n x n) matrix 

38 assert len(arr.shape) == 2 

39 assert arr.shape[0] == arr.shape[1] 

40 return np.allclose(np.tril(arr, k=-1), 0., atol=atol) and \ 

41 np.all(np.diag(arr) >= 0.0) 

42 

43 

44@deprecated( 

45 "Please use " 

46 "`ase.calculators.lammps.coordinatetransform.calc_rotated_cell`. " 

47 "Note that the new function returns the ASE lower trianglar cell and does " 

48 "not return the conversion matrix." 

49) 

50def convert_cell(ase_cell): 

51 """ 

52 Convert a parallelepiped (forming right hand basis) 

53 to lower triangular matrix LAMMPS can accept. This 

54 function transposes cell matrix so the bases are column vectors 

55 

56 .. deprecated:: 3.23.0 

57 Please use 

58 :func:`~ase.calculators.lammps.coordinatetransform.calc_rotated_cell`. 

59 """ 

60 cell = ase_cell.T 

61 

62 if not is_upper_triangular(cell): 

63 # rotate bases into triangular matrix 

64 tri_mat = np.zeros((3, 3)) 

65 A = cell[:, 0] 

66 B = cell[:, 1] 

67 C = cell[:, 2] 

68 tri_mat[0, 0] = norm(A) 

69 Ahat = A / norm(A) 

70 AxBhat = np.cross(A, B) / norm(np.cross(A, B)) 

71 tri_mat[0, 1] = np.dot(B, Ahat) 

72 tri_mat[1, 1] = norm(np.cross(Ahat, B)) 

73 tri_mat[0, 2] = np.dot(C, Ahat) 

74 tri_mat[1, 2] = np.dot(C, np.cross(AxBhat, Ahat)) 

75 tri_mat[2, 2] = norm(np.dot(C, AxBhat)) 

76 

77 # create and save the transformation for coordinates 

78 volume = np.linalg.det(ase_cell) 

79 trans = np.array([np.cross(B, C), np.cross(C, A), np.cross(A, B)]) 

80 trans /= volume 

81 coord_transform = np.dot(tri_mat, trans) 

82 

83 return tri_mat, coord_transform 

84 else: 

85 return cell, None 

86 

87 

88class LAMMPSlib(Calculator): 

89 r""" 

90**Introduction** 

91 

92LAMMPSlib is an interface and calculator for LAMMPS_. LAMMPSlib uses 

93the python interface that comes with LAMMPS to solve an atoms model 

94for energy, atom forces and cell stress. This calculator creates a 

95'.lmp' object which is a running lammps program, so further commands 

96can be sent to this object executed until it is explicitly closed. Any 

97additional variables calculated by lammps can also be extracted. This 

98is still experimental code. 

99 

100**Arguments** 

101 

102======================= ====================================================== 

103Keyword Description 

104======================= ====================================================== 

105``lmpcmds`` list of strings of LAMMPS commands. You need to supply 

106 enough to define the potential to be used e.g. 

107 

108 ["pair_style eam/alloy", 

109 "pair_coeff * * potentials/NiAlH_jea.eam.alloy Ni Al"] 

110 

111``atom_types`` dictionary of ``atomic_symbol :lammps_atom_type`` 

112 pairs, e.g. ``{'Cu':1}`` to bind copper to lammps 

113 atom type 1. If <None>, autocreated by assigning 

114 lammps atom types in order that they appear in the 

115 first used atoms object. 

116 

117``atom_type_masses`` dictionary of ``atomic_symbol :mass`` pairs, e.g. 

118 ``{'Cu':63.546}`` to optionally assign masses that 

119 override default ase.data.atomic_masses. Note that 

120 since unit conversion is done automatically in this 

121 module, these quantities must be given in the 

122 standard ase mass units (g/mol) 

123 

124``log_file`` string 

125 path to the desired LAMMPS log file 

126 

127``lammps_header`` string to use for lammps setup. Default is to use 

128 metal units and simple atom simulation. 

129 

130 lammps_header=['units metal', 

131 'atom_style atomic', 

132 'atom_modify map array sort 0 0']) 

133 

134``amendments`` extra list of strings of LAMMPS commands to be run 

135 post initialization. (Use: Initialization amendments) 

136 e.g. 

137 

138 ["mass 1 58.6934"] 

139 

140``post_changebox_cmds`` extra list of strings of LAMMPS commands to be run 

141 after any LAMMPS 'change_box' command is performed by 

142 the calculator. This is relevant because some 

143 potentials either themselves depend on the geometry 

144 and boundary conditions of the simulation box, or are 

145 frequently coupled with other LAMMPS commands that 

146 do, e.g. the 'buck/coul/long' pair style is often 

147 used with the kspace_* commands, which are sensitive 

148 to the periodicity of the simulation box. 

149 

150``keep_alive`` Boolean 

151 whether to keep the lammps routine alive for more 

152 commands. Default is True. 

153 

154======================= ====================================================== 

155 

156 

157**Requirements** 

158 

159To run this calculator you must have LAMMPS installed and compiled to 

160enable the python interface. See the LAMMPS manual. 

161 

162If the following code runs then lammps is installed correctly. 

163 

164 >>> from lammps import lammps 

165 >>> lmp = lammps() 

166 

167The version of LAMMPS is also important. LAMMPSlib is suitable for 

168versions after approximately 2011. Prior to this the python interface 

169is slightly different from that used by LAMMPSlib. It is not difficult 

170to change to the earlier format. 

171 

172**LAMMPS and LAMMPSlib** 

173 

174The LAMMPS calculator is another calculator that uses LAMMPS (the 

175program) to calculate the energy by generating input files and running 

176a separate LAMMPS job to perform the analysis. The output data is then 

177read back into python. LAMMPSlib makes direct use of the LAMMPS (the 

178program) python interface. As well as directly running any LAMMPS 

179command line it allows the values of any of LAMMPS variables to be 

180extracted and returned to python. 

181 

182**Example** 

183 

184Provided that the respective potential file is in the working directory, one 

185can simply run (note that LAMMPS needs to be compiled to work with EAM 

186potentials) 

187 

188:: 

189 

190 from ase import Atom, Atoms 

191 from ase.build import bulk 

192 from ase.calculators.lammpslib import LAMMPSlib 

193 

194 cmds = ["pair_style eam/alloy", 

195 "pair_coeff * * NiAlH_jea.eam.alloy Ni H"] 

196 

197 Ni = bulk('Ni', cubic=True) 

198 H = Atom('H', position=Ni.cell.diagonal()/2) 

199 NiH = Ni + H 

200 

201 lammps = LAMMPSlib(lmpcmds=cmds, log_file='test.log') 

202 

203 NiH.calc = lammps 

204 print("Energy ", NiH.get_potential_energy()) 

205 

206 

207**Implementation** 

208 

209LAMMPS provides a set of python functions to allow execution of the 

210underlying C++ LAMMPS code. The functions used by the LAMMPSlib 

211interface are:: 

212 

213 from lammps import lammps 

214 

215 lmp = lammps(cmd_args) # initiate LAMMPS object with command line args 

216 

217 lmp.scatter_atoms('x',1,3,positions) # atom coords to LAMMPS C array 

218 lmp.command(cmd) # executes a one line cmd string 

219 lmp.extract_variable(...) # extracts a per atom variable 

220 lmp.extract_global(...) # extracts a global variable 

221 lmp.close() # close the lammps object 

222 

223For a single Ni atom model the following lammps file commands would be run 

224by invoking the get_potential_energy() method:: 

225 

226 units metal 

227 atom_style atomic 

228 atom_modify map array sort 0 0 

229 

230 region cell prism 0 xhi 0 yhi 0 zhi xy xz yz units box 

231 create_box 1 cell 

232 create_atoms 1 single 0 0 0 units box 

233 mass * 1.0 

234 

235 ## user lmpcmds get executed here 

236 pair_style eam/alloy 

237 pair_coeff * * NiAlH_jea.eam.alloy Ni 

238 ## end of user lmmpcmds 

239 

240 run 0 

241 

242where xhi, yhi and zhi are the lattice vector lengths and xy, 

243xz and yz are the tilt of the lattice vectors, all to be edited. 

244 

245 

246**Notes** 

247 

248.. _LAMMPS: http://lammps.sandia.gov/ 

249 

250* Units: The default lammps_header sets the units to Angstrom and eV 

251 and for compatibility with ASE Stress is in GPa. 

252 

253* The global energy is currently extracted from LAMMPS using 

254 extract_variable since lammps.lammps currently extract_global only 

255 accepts the following ['dt', 'boxxlo', 'boxxhi', 'boxylo', 'boxyhi', 

256 'boxzlo', 'boxzhi', 'natoms', 'nlocal']. 

257 

258* If an error occurs while lammps is in control it will crash 

259 Python. Check the output of the log file to find the lammps error. 

260 

261* If the are commands directly sent to the LAMMPS object this may 

262 change the energy value of the model. However the calculator will not 

263 know of it and still return the original energy value. 

264 

265""" 

266 

267 implemented_properties = ['energy', 'free_energy', 'forces', 'stress', 

268 'energies'] 

269 

270 started = False 

271 initialized = False 

272 

273 default_parameters = dict( 

274 atom_types=None, 

275 atom_type_masses=None, 

276 log_file=None, 

277 lammps_name='', 

278 keep_alive=True, 

279 lammps_header=['units metal', 

280 'atom_style atomic', 

281 'atom_modify map array sort 0 0'], 

282 amendments=None, 

283 post_changebox_cmds=None, 

284 boundary=True, 

285 create_box=True, 

286 create_atoms=True, 

287 read_molecular_info=False, 

288 comm=None) 

289 

290 def __init__(self, *args, **kwargs): 

291 Calculator.__init__(self, *args, **kwargs) 

292 self.lmp = None 

293 

294 def __enter__(self): 

295 return self 

296 

297 def __exit__(self, *args): 

298 self.clean() 

299 

300 def clean(self): 

301 if self.started: 

302 self.lmp.close() 

303 self.started = False 

304 self.initialized = False 

305 self.lmp = None 

306 

307 def set_cell(self, atoms: Atoms, change: bool = False): 

308 self.prism = Prism(atoms.cell, atoms.pbc) 

309 _ = self.prism.get_lammps_prism() 

310 xhi, yhi, zhi, xy, xz, yz = convert(_, "distance", "ASE", self.units) 

311 box_hi = [xhi, yhi, zhi] 

312 

313 if change: 

314 cell_cmd = ('change_box all ' 

315 'x final 0 {} y final 0 {} z final 0 {} ' 

316 'xy final {} xz final {} yz final {} units box' 

317 ''.format(xhi, yhi, zhi, xy, xz, yz)) 

318 if self.parameters.post_changebox_cmds is not None: 

319 for cmd in self.parameters.post_changebox_cmds: 

320 self.lmp.command(cmd) 

321 else: 

322 # just in case we'll want to run with a funny shape box, 

323 # and here command will only happen once, and before 

324 # any calculation 

325 if self.parameters.create_box: 

326 self.lmp.command('box tilt large') 

327 

328 # Check if there are any indefinite boundaries. If so, 

329 # shrink-wrapping will end up being used, but we want to 

330 # define the LAMMPS region and box fairly tight around the 

331 # atoms to avoid losing any 

332 lammps_boundary_conditions = self.lammpsbc(atoms).split() 

333 if 's' in lammps_boundary_conditions: 

334 pos = self.prism.vector_to_lammps(atoms.positions) 

335 posmin = np.amin(pos, axis=0) 

336 posmax = np.amax(pos, axis=0) 

337 

338 for i in range(3): 

339 if lammps_boundary_conditions[i] == 's': 

340 box_hi[i] = 1.05 * abs(posmax[i] - posmin[i]) 

341 

342 cell_cmd = ('region cell prism ' 

343 '0 {} 0 {} 0 {} ' 

344 '{} {} {} units box' 

345 ''.format(*box_hi, xy, xz, yz)) 

346 

347 self.lmp.command(cell_cmd) 

348 

349 def set_lammps_pos(self, atoms: Atoms): 

350 # Create local copy of positions that are wrapped along any periodic 

351 # directions 

352 pos = convert(atoms.positions, "distance", "ASE", self.units) 

353 

354 # wrap only after scaling and rotating to reduce chances of 

355 # lammps neighbor list bugs. 

356 pos = self.prism.vector_to_lammps(pos, wrap=True) 

357 

358 # Convert ase position matrix to lammps-style position array 

359 # contiguous in memory 

360 lmp_positions = list(pos.ravel()) 

361 

362 # Convert that lammps-style array into a C object 

363 c_double_array = (ctypes.c_double * len(lmp_positions)) 

364 lmp_c_positions = c_double_array(*lmp_positions) 

365 # self.lmp.put_coosrds(lmp_c_positions) 

366 self.lmp.scatter_atoms('x', 1, 3, lmp_c_positions) 

367 

368 def calculate(self, atoms, properties, system_changes): 

369 self.propagate(atoms, properties, system_changes, 0) 

370 

371 def propagate(self, atoms, properties, system_changes, n_steps, dt=None, 

372 dt_not_real_time=False, velocity_field=None): 

373 """"atoms: Atoms object 

374 Contains positions, unit-cell, ... 

375 properties: list of str 

376 List of what needs to be calculated. Can be any combination 

377 of 'energy', 'forces', 'stress', 'dipole', 'charges', 'magmom' 

378 and 'magmoms'. 

379 system_changes: list of str 

380 List of what has changed since last calculation. Can be 

381 any combination of these five: 'positions', 'numbers', 'cell', 

382 'pbc', 'initial_charges' and 'initial_magmoms'. 

383 """ 

384 if len(system_changes) == 0: 

385 return 

386 

387 if not self.started: 

388 self.start_lammps() 

389 if not self.initialized: 

390 self.initialise_lammps(atoms) 

391 else: # still need to reset cell 

392 # NOTE: The whole point of ``post_changebox_cmds`` is that they're 

393 # executed after any call to LAMMPS' change_box command. Here, we 

394 # rely on the fact that self.set_cell(), where we have currently 

395 # placed the execution of ``post_changebox_cmds``, gets called 

396 # after this initial change_box call. 

397 

398 # Apply only requested boundary condition changes. Note this needs 

399 # to happen before the call to set_cell since 'change_box' will 

400 # apply any shrink-wrapping *after* it's updated the cell 

401 # dimensions 

402 if 'pbc' in system_changes: 

403 change_box_str = 'change_box all boundary {}' 

404 change_box_cmd = change_box_str.format(self.lammpsbc(atoms)) 

405 self.lmp.command(change_box_cmd) 

406 

407 # Reset positions so that if they are crazy from last 

408 # propagation, change_box (in set_cell()) won't hang. 

409 # Could do this only after testing for crazy positions? 

410 # Could also use scatter_atoms() to set values (requires 

411 # MPI comm), or extra_atoms() to get pointers to local 

412 # data structures to zero, but then we would have to be 

413 # careful with parallelism. 

414 self.lmp.command("set atom * x 0.0 y 0.0 z 0.0") 

415 self.set_cell(atoms, change=True) 

416 

417 if self.parameters.atom_types is None: 

418 raise NameError("atom_types are mandatory.") 

419 

420 do_rebuild = ( 

421 not np.array_equal(atoms.numbers, self.previous_atoms_numbers) 

422 or any(_ in system_changes for _ in ('numbers', 'initial_charges')) 

423 ) 

424 if not do_rebuild: 

425 do_redo_atom_types = not np.array_equal( 

426 atoms.numbers, self.previous_atoms_numbers) 

427 else: 

428 do_redo_atom_types = False 

429 

430 self.lmp.command('echo none') # don't echo the atom positions 

431 if do_rebuild: 

432 self.rebuild(atoms) 

433 elif do_redo_atom_types: 

434 self.redo_atom_types(atoms) 

435 self.lmp.command('echo log') # switch back log 

436 

437 self.set_lammps_pos(atoms) 

438 

439 if self.parameters.amendments is not None: 

440 for cmd in self.parameters.amendments: 

441 self.lmp.command(cmd) 

442 

443 if n_steps > 0: 

444 if velocity_field is None: 

445 vel = convert( 

446 atoms.get_velocities(), 

447 "velocity", 

448 "ASE", 

449 self.units) 

450 else: 

451 # FIXME: Do we need to worry about converting to lammps units 

452 # here? 

453 vel = atoms.arrays[velocity_field] 

454 

455 # Transform the velocities to new coordinate system 

456 vel = self.prism.vector_to_lammps(vel) 

457 

458 # Convert ase velocities matrix to lammps-style velocities array 

459 lmp_velocities = list(vel.ravel()) 

460 

461 # Convert that lammps-style array into a C object 

462 c_double_array = (ctypes.c_double * len(lmp_velocities)) 

463 lmp_c_velocities = c_double_array(*lmp_velocities) 

464 self.lmp.scatter_atoms('v', 1, 3, lmp_c_velocities) 

465 

466 # Run for 0 time to calculate 

467 if dt is not None: 

468 if dt_not_real_time: 

469 self.lmp.command('timestep %.30f' % dt) 

470 else: 

471 self.lmp.command('timestep %.30f' % 

472 convert(dt, "time", "ASE", self.units)) 

473 self.lmp.command('run %d' % n_steps) 

474 

475 if n_steps > 0: 

476 # TODO this must be slower than native copy, but why is it broken? 

477 pos = np.array( 

478 [x for x in self.lmp.gather_atoms("x", 1, 3)]).reshape(-1, 3) 

479 pos = self.prism.vector_to_ase(pos) 

480 

481 # Convert from LAMMPS units to ASE units 

482 pos = convert(pos, "distance", self.units, "ASE") 

483 

484 atoms.set_positions(pos) 

485 

486 vel = np.array( 

487 [v for v in self.lmp.gather_atoms("v", 1, 3)]).reshape(-1, 3) 

488 vel = self.prism.vector_to_lammps(vel) 

489 if velocity_field is None: 

490 atoms.set_velocities(convert(vel, 'velocity', self.units, 

491 'ASE')) 

492 

493 # Extract the forces and energy 

494 self.results['energy'] = convert( 

495 self.lmp.extract_variable('pe', None, 0), 

496 "energy", self.units, "ASE" 

497 ) 

498 self.results['free_energy'] = self.results['energy'] 

499 

500 # check for MPI active as per https://matsci.org/t/lammps-ids-in-python/60509/5 

501 world_size = self.lmp.extract_setting('world_size') 

502 if world_size != 1: 

503 raise RuntimeError('Unsupported MPI active as indicated by ' 

504 f'world_size == {world_size} != 1') 

505 # select just n_local which we assume is equal to len(atoms) 

506 ids = self.lmp.numpy.extract_atom("id")[0:len(atoms)] 

507 self.results["energies"] = convert( 

508 self.lmp.numpy.extract_compute('pe_peratom', 

509 self.LMP_STYLE_ATOM, 

510 self.LMP_TYPE_VECTOR), 

511 "energy", self.units, "ASE" 

512 ) 

513 self.results["energies"][ids - 1] = self.results["energies"] 

514 

515 stress = np.empty(6) 

516 stress_vars = ['pxx', 'pyy', 'pzz', 'pyz', 'pxz', 'pxy'] 

517 

518 for i, var in enumerate(stress_vars): 

519 stress[i] = self.lmp.extract_variable(var, None, 0) 

520 

521 stress_mat = np.zeros((3, 3)) 

522 stress_mat[0, 0] = stress[0] 

523 stress_mat[1, 1] = stress[1] 

524 stress_mat[2, 2] = stress[2] 

525 stress_mat[1, 2] = stress[3] 

526 stress_mat[2, 1] = stress[3] 

527 stress_mat[0, 2] = stress[4] 

528 stress_mat[2, 0] = stress[4] 

529 stress_mat[0, 1] = stress[5] 

530 stress_mat[1, 0] = stress[5] 

531 

532 stress_mat = self.prism.tensor2_to_ase(stress_mat) 

533 

534 stress[0] = stress_mat[0, 0] 

535 stress[1] = stress_mat[1, 1] 

536 stress[2] = stress_mat[2, 2] 

537 stress[3] = stress_mat[1, 2] 

538 stress[4] = stress_mat[0, 2] 

539 stress[5] = stress_mat[0, 1] 

540 

541 self.results['stress'] = convert(-stress, "pressure", self.units, "ASE") 

542 

543 # definitely yields atom-id ordered force array 

544 f = convert(np.array(self.lmp.gather_atoms("f", 1, 3)).reshape(-1, 3), 

545 "force", self.units, "ASE") 

546 self.results['forces'] = self.prism.vector_to_ase(f) 

547 

548 # otherwise check_state will always trigger a new calculation 

549 self.atoms = atoms.copy() 

550 

551 if not self.parameters["keep_alive"]: 

552 self.clean() 

553 

554 def lammpsbc(self, atoms): 

555 """Determine LAMMPS boundary types based on ASE pbc settings. For 

556 non-periodic dimensions, if the cell length is finite then 

557 fixed BCs ('f') are used; if the cell length is approximately 

558 zero, shrink-wrapped BCs ('s') are used.""" 

559 

560 retval = '' 

561 pbc = atoms.get_pbc() 

562 if np.all(pbc): 

563 retval = 'p p p' 

564 else: 

565 cell = atoms.get_cell() 

566 for i in range(3): 

567 if pbc[i]: 

568 retval += 'p ' 

569 else: 

570 # See if we're using indefinite ASE boundaries along this 

571 # direction 

572 if np.linalg.norm(cell[i]) < np.finfo(cell[i][0]).tiny: 

573 retval += 's ' 

574 else: 

575 retval += 'f ' 

576 

577 return retval.strip() 

578 

579 def rebuild(self, atoms): 

580 try: 

581 n_diff = len(atoms.numbers) - len(self.previous_atoms_numbers) 

582 except Exception: # XXX Which kind of exception? 

583 n_diff = len(atoms.numbers) 

584 

585 if n_diff > 0: 

586 if any(("reax/c" in cmd) for cmd in self.parameters.lmpcmds): 

587 self.lmp.command("pair_style lj/cut 2.5") 

588 self.lmp.command("pair_coeff * * 1 1") 

589 

590 for cmd in self.parameters.lmpcmds: 

591 if (("pair_style" in cmd) or ("pair_coeff" in cmd) or 

592 ("qeq/reax" in cmd)): 

593 self.lmp.command(cmd) 

594 

595 cmd = f"create_atoms 1 random {n_diff} 1 NULL" 

596 self.lmp.command(cmd) 

597 elif n_diff < 0: 

598 cmd = "group delatoms id {}:{}".format( 

599 len(atoms.numbers) + 1, len(self.previous_atoms_numbers)) 

600 self.lmp.command(cmd) 

601 cmd = "delete_atoms group delatoms" 

602 self.lmp.command(cmd) 

603 

604 self.redo_atom_types(atoms) 

605 

606 def redo_atom_types(self, atoms): 

607 current_types = { 

608 (i + 1, self.parameters.atom_types[sym]) for i, sym 

609 in enumerate(atoms.get_chemical_symbols())} 

610 

611 try: 

612 previous_types = { 

613 (i + 1, self.parameters.atom_types[ase_chemical_symbols[Z]]) 

614 for i, Z in enumerate(self.previous_atoms_numbers)} 

615 except Exception: # XXX which kind of exception? 

616 previous_types = set() 

617 

618 for (i, i_type) in current_types - previous_types: 

619 cmd = f"set atom {i} type {i_type}" 

620 self.lmp.command(cmd) 

621 

622 # set charges only when LAMMPS `atom_style` permits charges 

623 # https://docs.lammps.org/Library_properties.html#extract-atom-flags 

624 if self.lmp.extract_setting('q_flag') == 1: 

625 charges = atoms.get_initial_charges() 

626 if np.any(charges != 0.0): 

627 for i, q in enumerate(charges): 

628 self.lmp.command(f'set atom {i + 1} charge {q}') 

629 

630 self.previous_atoms_numbers = atoms.numbers.copy() 

631 

632 def restart_lammps(self, atoms): 

633 if self.started: 

634 self.lmp.command("clear") 

635 # hope there's no other state to be reset 

636 self.started = False 

637 self.initialized = False 

638 self.previous_atoms_numbers = [] 

639 self.start_lammps() 

640 self.initialise_lammps(atoms) 

641 

642 def start_lammps(self): 

643 # Only import lammps when running a calculation 

644 # so it is not required to use other parts of the 

645 # module 

646 from lammps import LMP_STYLE_ATOM, LMP_TYPE_VECTOR, lammps 

647 

648 self.LMP_STYLE_ATOM = LMP_STYLE_ATOM 

649 self.LMP_TYPE_VECTOR = LMP_TYPE_VECTOR 

650 

651 # start lammps process 

652 if self.parameters.log_file is None: 

653 cmd_args = ['-echo', 'log', '-log', 'none', '-screen', 'none', 

654 '-nocite'] 

655 else: 

656 cmd_args = ['-echo', 'log', '-log', self.parameters.log_file, 

657 '-screen', 'none', '-nocite'] 

658 

659 self.cmd_args = cmd_args 

660 

661 if self.lmp is None: 

662 self.lmp = lammps(self.parameters.lammps_name, self.cmd_args, 

663 comm=self.parameters.comm) 

664 

665 # Run header commands to set up lammps (units, etc.) 

666 for cmd in self.parameters.lammps_header: 

667 self.lmp.command(cmd) 

668 

669 for cmd in self.parameters.lammps_header: 

670 if "units" in cmd: 

671 self.units = cmd.split()[1] 

672 

673 if 'lammps_header_extra' in self.parameters: 

674 if self.parameters.lammps_header_extra is not None: 

675 for cmd in self.parameters.lammps_header_extra: 

676 self.lmp.command(cmd) 

677 

678 self.started = True 

679 

680 def initialise_lammps(self, atoms): 

681 # Initialising commands 

682 if self.parameters.boundary: 

683 # if the boundary command is in the supplied commands use that 

684 # otherwise use atoms pbc 

685 for cmd in self.parameters.lmpcmds: 

686 if 'boundary' in cmd: 

687 break 

688 else: 

689 self.lmp.command('boundary ' + self.lammpsbc(atoms)) 

690 

691 # Initialize cell 

692 self.set_cell(atoms, change=not self.parameters.create_box) 

693 

694 if self.parameters.atom_types is None: 

695 # if None is given, create from atoms object in order of appearance 

696 s = atoms.get_chemical_symbols() 

697 _, idx = np.unique(s, return_index=True) 

698 s_red = np.array(s)[np.sort(idx)].tolist() 

699 self.parameters.atom_types = {j: i + 1 for i, j in enumerate(s_red)} 

700 

701 # Initialize box 

702 if self.parameters.create_box: 

703 # count number of known types 

704 n_types = len(self.parameters.atom_types) 

705 create_box_command = f'create_box {n_types} cell' 

706 self.lmp.command(create_box_command) 

707 

708 # Initialize the atoms with their types 

709 # positions do not matter here 

710 if self.parameters.create_atoms: 

711 self.lmp.command('echo none') # don't echo the atom positions 

712 self.rebuild(atoms) 

713 self.lmp.command('echo log') # turn back on 

714 else: 

715 self.previous_atoms_numbers = atoms.numbers.copy() 

716 

717 # execute the user commands 

718 for cmd in self.parameters.lmpcmds + ["compute pe_peratom all pe/atom"]: 

719 self.lmp.command(cmd) 

720 

721 # Set masses after user commands, e.g. to override 

722 # EAM-provided masses 

723 for sym in self.parameters.atom_types: 

724 if self.parameters.atom_type_masses is None: 

725 mass = ase_atomic_masses[ase_atomic_numbers[sym]] 

726 else: 

727 mass = self.parameters.atom_type_masses[sym] 

728 self.lmp.command('mass %d %.30f' % ( 

729 self.parameters.atom_types[sym], 

730 convert(mass, "mass", "ASE", self.units))) 

731 

732 # Define force & energy variables for extraction 

733 self.lmp.command('variable pxx equal pxx') 

734 self.lmp.command('variable pyy equal pyy') 

735 self.lmp.command('variable pzz equal pzz') 

736 self.lmp.command('variable pxy equal pxy') 

737 self.lmp.command('variable pxz equal pxz') 

738 self.lmp.command('variable pyz equal pyz') 

739 

740 # I am not sure why we need this next line but LAMMPS will 

741 # raise an error if it is not there. Perhaps it is needed to 

742 # ensure the cell stresses are calculated 

743 self.lmp.command('thermo_style custom pe pxx emol ecoul') 

744 

745 self.lmp.command('variable fx atom fx') 

746 self.lmp.command('variable fy atom fy') 

747 self.lmp.command('variable fz atom fz') 

748 

749 # do we need this if we extract from a global ? 

750 self.lmp.command('variable pe equal pe') 

751 

752 self.lmp.command("neigh_modify delay 0 every 1 check yes") 

753 

754 self.initialized = True