Coverage for ase / calculators / lammpslib.py: 79.85%

263 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-04 10:20 +0000

1# fmt: off 

2 

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

4 

5 

6import ctypes 

7 

8import numpy as np 

9 

10from ase import Atoms 

11from ase.calculators.calculator import Calculator 

12from ase.calculators.lammps import Prism, convert 

13from ase.data import atomic_masses as ase_atomic_masses 

14from ase.data import atomic_numbers as ase_atomic_numbers 

15from ase.data import chemical_symbols as ase_chemical_symbols 

16 

17# TODO 

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

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

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

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

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

23# into a python function that can be called 

24# 8. make matscipy as fallback 

25# 9. keep_alive not needed with no system changes 

26 

27 

28class LAMMPSlib(Calculator): 

29 r""" 

30**Introduction** 

31 

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

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

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

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

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

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

38is still experimental code. 

39 

40**Arguments** 

41 

42======================= ====================================================== 

43Keyword Description 

44======================= ====================================================== 

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

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

47 

48 ["pair_style eam/alloy", 

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

50 

51``atom_types`` dictionary of ``atomic_symbol :lammps_atom_type`` 

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

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

54 lammps atom types in order that they appear in the 

55 first used atoms object. 

56 

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

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

59 override default ase.data.atomic_masses. Note that 

60 since unit conversion is done automatically in this 

61 module, these quantities must be given in the 

62 standard ase mass units (g/mol) 

63 

64``log_file`` string 

65 path to the desired LAMMPS log file 

66 

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

68 metal units and simple atom simulation. 

69 

70 lammps_header=['units metal', 

71 'atom_style atomic', 

72 'atom_modify map array sort 0 0']) 

73 

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

75 post initialization. (Use: Initialization amendments) 

76 e.g. 

77 

78 ["mass 1 58.6934"] 

79 

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

81 after any LAMMPS 'change_box' command is performed by 

82 the calculator. This is relevant because some 

83 potentials either themselves depend on the geometry 

84 and boundary conditions of the simulation box, or are 

85 frequently coupled with other LAMMPS commands that 

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

87 used with the kspace_* commands, which are sensitive 

88 to the periodicity of the simulation box. 

89 

90``extra_cmd_args`` list of extra arguments for 

91 `lammps.lammps(cmd_args=...)`, e.g. for kokkos mliap on 

92 a gpu 

93 ``` 

94 ("-k on g 1 -sf kk " 

95 "-pk kokkos neigh half newton on").split() 

96 ``` 

97 

98``intializer`` callback function that does arbitrary LAMMPS python 

99 API initializtion tasks (e.g. calling 

100 `lammps.mliap.activate_mliapy`) and accepts a single 

101 positional argument, `self.lmp`. 

102 

103``keep_alive`` Boolean 

104 whether to keep the lammps routine alive for more 

105 commands. Default is True. 

106 

107======================= ====================================================== 

108 

109 

110**Requirements** 

111 

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

113enable the python interface. See the LAMMPS manual. 

114 

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

116 

117 >>> from lammps import lammps 

118 >>> lmp = lammps() 

119 

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

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

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

123to change to the earlier format. 

124 

125**LAMMPS and LAMMPSlib** 

126 

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

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

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

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

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

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

133extracted and returned to python. 

134 

135**Example** 

136 

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

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

139potentials) 

140 

141:: 

142 

143 from ase import Atom, Atoms 

144 from ase.build import bulk 

145 from ase.calculators.lammpslib import LAMMPSlib 

146 

147 cmds = ["pair_style eam/alloy", 

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

149 

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

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

152 NiH = Ni + H 

153 

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

155 

156 NiH.calc = lammps 

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

158 

159 

160**Implementation** 

161 

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

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

164interface are:: 

165 

166 from lammps import lammps 

167 

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

169 

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

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

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

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

174 lmp.close() # close the lammps object 

175 

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

177by invoking the get_potential_energy() method:: 

178 

179 units metal 

180 atom_style atomic 

181 atom_modify map array sort 0 0 

182 

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

184 create_box 1 cell 

185 create_atoms 1 single 0 0 0 units box 

186 mass * 1.0 

187 

188 ## user lmpcmds get executed here 

189 pair_style eam/alloy 

190 pair_coeff * * NiAlH_jea.eam.alloy Ni 

191 ## end of user lmmpcmds 

192 

193 run 0 

194 

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

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

197 

198 

199**Notes** 

200 

201.. _LAMMPS: https://lammps.org/ 

202 

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

204 and for compatibility with ASE Stress is in GPa. 

205 

206* The global energy is currently extracted from LAMMPS using 

207 extract_variable since lammps.lammps currently extract_global only 

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

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

210 

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

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

213 

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

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

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

217 

218""" 

219 

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

221 'energies'] 

222 

223 started = False 

224 initialized = False 

225 

226 default_parameters = dict( 

227 atom_types=None, 

228 atom_type_masses=None, 

229 log_file=None, 

230 lammps_name='', 

231 keep_alive=True, 

232 lammps_header=['units metal', 

233 'atom_style atomic', 

234 'atom_modify map array sort 0 0'], 

235 amendments=None, 

236 post_changebox_cmds=None, 

237 extra_cmd_args=(), 

238 initializer=None, 

239 boundary=True, 

240 create_box=True, 

241 create_atoms=True, 

242 read_molecular_info=False, 

243 comm=None) 

244 

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

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

247 self.lmp = None 

248 

249 def __enter__(self): 

250 return self 

251 

252 def __exit__(self, *args): 

253 self.clean() 

254 

255 def clean(self): 

256 if self.started: 

257 self.lmp.close() 

258 self.started = False 

259 self.initialized = False 

260 self.lmp = None 

261 

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

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

264 _ = self.prism.get_lammps_prism() 

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

266 box_hi = [xhi, yhi, zhi] 

267 

268 if change: 

269 cell_cmd = ('change_box all ' 

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

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

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

273 if self.parameters.post_changebox_cmds is not None: 

274 for cmd in self.parameters.post_changebox_cmds: 

275 self.lmp.command(cmd) 

276 else: 

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

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

279 # any calculation 

280 if self.parameters.create_box: 

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

282 

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

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

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

286 # atoms to avoid losing any 

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

288 if 's' in lammps_boundary_conditions: 

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

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

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

292 

293 for i in range(3): 

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

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

296 

297 cell_cmd = ('region cell prism ' 

298 '0 {} 0 {} 0 {} ' 

299 '{} {} {} units box' 

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

301 

302 self.lmp.command(cell_cmd) 

303 

304 def set_lammps_pos(self, atoms: Atoms): 

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

306 # directions 

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

308 

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

310 # lammps neighbor list bugs. 

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

312 

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

314 # contiguous in memory 

315 lmp_positions = list(pos.ravel()) 

316 

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

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

319 lmp_c_positions = c_double_array(*lmp_positions) 

320 # self.lmp.put_coosrds(lmp_c_positions) 

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

322 

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

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

325 

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

327 dt_not_real_time=False, velocity_field=None): 

328 """"atoms: Atoms object 

329 Contains positions, unit-cell, ... 

330 properties: list of str 

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

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

333 and 'magmoms'. 

334 system_changes: list of str 

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

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

337 'pbc', 'initial_charges' and 'initial_magmoms'. 

338 """ 

339 if len(system_changes) == 0: 

340 return 

341 

342 if not self.started: 

343 self.start_lammps() 

344 if not self.initialized: 

345 self.initialise_lammps(atoms) 

346 else: # still need to reset cell 

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

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

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

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

351 # after this initial change_box call. 

352 

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

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

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

356 # dimensions 

357 if 'pbc' in system_changes: 

358 change_box_str = 'change_box all boundary {}' 

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

360 self.lmp.command(change_box_cmd) 

361 

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

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

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

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

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

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

368 # careful with parallelism. 

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

370 self.set_cell(atoms, change=True) 

371 

372 if self.parameters.atom_types is None: 

373 raise NameError("atom_types are mandatory.") 

374 

375 do_rebuild = ( 

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

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

378 ) 

379 if not do_rebuild: 

380 do_redo_atom_types = not np.array_equal( 

381 atoms.numbers, self.previous_atoms_numbers) 

382 else: 

383 do_redo_atom_types = False 

384 

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

386 if do_rebuild: 

387 self.rebuild(atoms) 

388 elif do_redo_atom_types: 

389 self.redo_atom_types(atoms) 

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

391 

392 self.set_lammps_pos(atoms) 

393 

394 if self.parameters.amendments is not None: 

395 for cmd in self.parameters.amendments: 

396 self.lmp.command(cmd) 

397 

398 if n_steps > 0: 

399 if velocity_field is None: 

400 vel = convert( 

401 atoms.get_velocities(), 

402 "velocity", 

403 "ASE", 

404 self.units) 

405 else: 

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

407 # here? 

408 vel = atoms.arrays[velocity_field] 

409 

410 # Transform the velocities to new coordinate system 

411 vel = self.prism.vector_to_lammps(vel) 

412 

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

414 lmp_velocities = list(vel.ravel()) 

415 

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

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

418 lmp_c_velocities = c_double_array(*lmp_velocities) 

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

420 

421 # Run for 0 time to calculate 

422 if dt is not None: 

423 if dt_not_real_time: 

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

425 else: 

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

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

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

429 

430 if n_steps > 0: 

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

432 pos = np.array( 

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

434 pos = self.prism.vector_to_ase(pos) 

435 

436 # Convert from LAMMPS units to ASE units 

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

438 

439 atoms.set_positions(pos) 

440 

441 vel = np.array( 

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

443 vel = self.prism.vector_to_lammps(vel) 

444 if velocity_field is None: 

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

446 'ASE')) 

447 

448 # Extract the forces and energy 

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

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

451 "energy", self.units, "ASE" 

452 ) 

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

454 

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

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

457 if world_size != 1: 

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

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

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

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

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

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

464 self.LMP_STYLE_ATOM, 

465 self.LMP_TYPE_VECTOR), 

466 "energy", self.units, "ASE" 

467 ) 

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

469 

470 stress = np.empty(6) 

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

472 

473 for i, var in enumerate(stress_vars): 

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

475 

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

477 stress_mat[0, 0] = stress[0] 

478 stress_mat[1, 1] = stress[1] 

479 stress_mat[2, 2] = stress[2] 

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

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

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

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

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

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

486 

487 stress_mat = self.prism.tensor2_to_ase(stress_mat) 

488 

489 stress[0] = stress_mat[0, 0] 

490 stress[1] = stress_mat[1, 1] 

491 stress[2] = stress_mat[2, 2] 

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

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

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

495 

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

497 

498 # definitely yields atom-id ordered force array 

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

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

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

502 

503 # otherwise check_state will always trigger a new calculation 

504 self.atoms = atoms.copy() 

505 

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

507 self.clean() 

508 

509 def lammpsbc(self, atoms): 

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

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

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

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

514 

515 retval = '' 

516 pbc = atoms.get_pbc() 

517 if np.all(pbc): 

518 retval = 'p p p' 

519 else: 

520 cell = atoms.get_cell() 

521 for i in range(3): 

522 if pbc[i]: 

523 retval += 'p ' 

524 else: 

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

526 # direction 

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

528 retval += 's ' 

529 else: 

530 retval += 'f ' 

531 

532 return retval.strip() 

533 

534 def rebuild(self, atoms): 

535 try: 

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

537 except Exception: # XXX Which kind of exception? 

538 n_diff = len(atoms.numbers) 

539 

540 if n_diff > 0: 

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

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

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

544 

545 for cmd in self.parameters.lmpcmds: 

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

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

548 self.lmp.command(cmd) 

549 

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

551 self.lmp.command(cmd) 

552 elif n_diff < 0: 

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

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

555 self.lmp.command(cmd) 

556 cmd = "delete_atoms group delatoms" 

557 self.lmp.command(cmd) 

558 

559 self.redo_atom_types(atoms) 

560 

561 def redo_atom_types(self, atoms): 

562 current_types = { 

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

564 in enumerate(atoms.get_chemical_symbols())} 

565 

566 try: 

567 previous_types = { 

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

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

570 except Exception: # XXX which kind of exception? 

571 previous_types = set() 

572 

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

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

575 self.lmp.command(cmd) 

576 

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

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

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

580 charges = atoms.get_initial_charges() 

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

582 for i, q in enumerate(charges): 

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

584 

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

586 

587 def restart_lammps(self, atoms): 

588 if self.started: 

589 self.lmp.command("clear") 

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

591 self.started = False 

592 self.initialized = False 

593 self.previous_atoms_numbers = [] 

594 self.start_lammps() 

595 self.initialise_lammps(atoms) 

596 

597 def start_lammps(self): 

598 # Only import lammps when running a calculation 

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

600 # module 

601 from lammps import LMP_STYLE_ATOM, LMP_TYPE_VECTOR, lammps 

602 

603 self.LMP_STYLE_ATOM = LMP_STYLE_ATOM 

604 self.LMP_TYPE_VECTOR = LMP_TYPE_VECTOR 

605 

606 # start lammps process 

607 if self.parameters.log_file is None: 

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

609 '-nocite'] 

610 else: 

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

612 '-screen', 'none', '-nocite'] 

613 

614 self.cmd_args = cmd_args + list(self.parameters.extra_cmd_args) 

615 

616 if self.lmp is None: 

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

618 comm=self.parameters.comm) 

619 

620 if self.parameters.initializer is not None: 

621 self.parameters.initializer(self.lmp) 

622 

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

624 for cmd in self.parameters.lammps_header: 

625 self.lmp.command(cmd) 

626 

627 for cmd in self.parameters.lammps_header: 

628 if "units" in cmd: 

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

630 

631 if 'lammps_header_extra' in self.parameters: 

632 if self.parameters.lammps_header_extra is not None: 

633 for cmd in self.parameters.lammps_header_extra: 

634 self.lmp.command(cmd) 

635 

636 self.started = True 

637 

638 def initialise_lammps(self, atoms): 

639 # Initialising commands 

640 if self.parameters.boundary: 

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

642 # otherwise use atoms pbc 

643 for cmd in self.parameters.lmpcmds: 

644 if 'boundary' in cmd: 

645 break 

646 else: 

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

648 

649 # Initialize cell 

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

651 

652 if self.parameters.atom_types is None: 

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

654 s = atoms.get_chemical_symbols() 

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

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

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

658 

659 # Initialize box 

660 if self.parameters.create_box: 

661 # count number of known types 

662 n_types = len(self.parameters.atom_types) 

663 create_box_command = f'create_box {n_types} cell' 

664 self.lmp.command(create_box_command) 

665 

666 # Initialize the atoms with their types 

667 # positions do not matter here 

668 if self.parameters.create_atoms: 

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

670 self.rebuild(atoms) 

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

672 else: 

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

674 

675 # execute the user commands 

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

677 self.lmp.command(cmd) 

678 

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

680 # EAM-provided masses 

681 for sym in self.parameters.atom_types: 

682 if self.parameters.atom_type_masses is None: 

683 mass = ase_atomic_masses[ase_atomic_numbers[sym]] 

684 else: 

685 mass = self.parameters.atom_type_masses[sym] 

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

687 self.parameters.atom_types[sym], 

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

689 

690 # Define force & energy variables for extraction 

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

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

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

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

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

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

697 

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

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

700 # ensure the cell stresses are calculated 

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

702 

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

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

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

706 

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

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

709 

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

711 

712 self.initialized = True