Coverage for /builds/kinetik161/ase/ase/calculators/lammpslib.py: 73.50%

283 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1"""ASE LAMMPS Calculator Library Version""" 

2 

3 

4import ctypes 

5 

6import numpy as np 

7from numpy.linalg import norm 

8 

9from ase import Atoms 

10from ase.calculators.calculator import Calculator 

11from ase.calculators.lammps import Prism, convert 

12from ase.data import atomic_masses as ase_atomic_masses 

13from ase.data import atomic_numbers as ase_atomic_numbers 

14from ase.data import chemical_symbols as ase_chemical_symbols 

15from ase.utils import deprecated 

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 

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

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

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

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

32 .. deprecated:: 3.23.0 

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

34 """ 

35 # must be (n x n) matrix 

36 assert len(arr.shape) == 2 

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

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

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

40 

41 

42@deprecated( 

43 "Please use " 

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

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

46 "not return the conversion matrix." 

47) 

48def convert_cell(ase_cell): 

49 """ 

50 Convert a parallelepiped (forming right hand basis) 

51 to lower triangular matrix LAMMPS can accept. This 

52 function transposes cell matrix so the bases are column vectors 

53 

54 .. deprecated:: 3.23.0 

55 Please use 

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

57 """ 

58 cell = ase_cell.T 

59 

60 if not is_upper_triangular(cell): 

61 # rotate bases into triangular matrix 

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

63 A = cell[:, 0] 

64 B = cell[:, 1] 

65 C = cell[:, 2] 

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

67 Ahat = A / norm(A) 

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

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

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

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

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

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

74 

75 # create and save the transformation for coordinates 

76 volume = np.linalg.det(ase_cell) 

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

78 trans /= volume 

79 coord_transform = np.dot(tri_mat, trans) 

80 

81 return tri_mat, coord_transform 

82 else: 

83 return cell, None 

84 

85 

86class LAMMPSlib(Calculator): 

87 r""" 

88**Introduction** 

89 

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

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

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

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

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

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

96is still experimental code. 

97 

98**Arguments** 

99 

100======================= ====================================================== 

101Keyword Description 

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

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

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

105 

106 ["pair_style eam/alloy", 

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

108 

109``atom_types`` dictionary of ``atomic_symbol :lammps_atom_type`` 

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

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

112 lammps atom types in order that they appear in the 

113 first used atoms object. 

114 

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

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

117 override default ase.data.atomic_masses. Note that 

118 since unit conversion is done automatically in this 

119 module, these quantities must be given in the 

120 standard ase mass units (g/mol) 

121 

122``log_file`` string 

123 path to the desired LAMMPS log file 

124 

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

126 metal units and simple atom simulation. 

127 

128 lammps_header=['units metal', 

129 'atom_style atomic', 

130 'atom_modify map array sort 0 0']) 

131 

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

133 post initialization. (Use: Initialization amendments) 

134 e.g. 

135 

136 ["mass 1 58.6934"] 

137 

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

139 after any LAMMPS 'change_box' command is performed by 

140 the calculator. This is relevant because some 

141 potentials either themselves depend on the geometry 

142 and boundary conditions of the simulation box, or are 

143 frequently coupled with other LAMMPS commands that 

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

145 used with the kspace_* commands, which are sensitive 

146 to the periodicity of the simulation box. 

147 

148``keep_alive`` Boolean 

149 whether to keep the lammps routine alive for more 

150 commands. Default is True. 

151 

152======================= ====================================================== 

153 

154 

155**Requirements** 

156 

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

158enable the python interface. See the LAMMPS manual. 

159 

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

161 

162 >>> from lammps import lammps 

163 >>> lmp = lammps() 

164 

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

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

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

168to change to the earlier format. 

169 

170**LAMMPS and LAMMPSlib** 

171 

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

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

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

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

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

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

178extracted and returned to python. 

179 

180**Example** 

181 

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

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

184potentials) 

185 

186:: 

187 

188 from ase import Atom, Atoms 

189 from ase.build import bulk 

190 from ase.calculators.lammpslib import LAMMPSlib 

191 

192 cmds = ["pair_style eam/alloy", 

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

194 

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

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

197 NiH = Ni + H 

198 

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

200 

201 NiH.calc = lammps 

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

203 

204 

205**Implementation** 

206 

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

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

209interface are:: 

210 

211 from lammps import lammps 

212 

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

214 

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

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

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

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

219 lmp.close() # close the lammps object 

220 

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

222by invoking the get_potential_energy() method:: 

223 

224 units metal 

225 atom_style atomic 

226 atom_modify map array sort 0 0 

227 

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

229 create_box 1 cell 

230 create_atoms 1 single 0 0 0 units box 

231 mass * 1.0 

232 

233 ## user lmpcmds get executed here 

234 pair_style eam/alloy 

235 pair_coeff * * NiAlH_jea.eam.alloy Ni 

236 ## end of user lmmpcmds 

237 

238 run 0 

239 

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

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

242 

243 

244**Notes** 

245 

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

247 

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

249 and for compatibility with ASE Stress is in GPa. 

250 

251* The global energy is currently extracted from LAMMPS using 

252 extract_variable since lammps.lammps currently extract_global only 

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

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

255 

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

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

258 

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

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

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

262 

263""" 

264 

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

266 'energies'] 

267 

268 started = False 

269 initialized = False 

270 

271 default_parameters = dict( 

272 atom_types=None, 

273 atom_type_masses=None, 

274 log_file=None, 

275 lammps_name='', 

276 keep_alive=True, 

277 lammps_header=['units metal', 

278 'atom_style atomic', 

279 'atom_modify map array sort 0 0'], 

280 amendments=None, 

281 post_changebox_cmds=None, 

282 boundary=True, 

283 create_box=True, 

284 create_atoms=True, 

285 read_molecular_info=False, 

286 comm=None) 

287 

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

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

290 self.lmp = None 

291 

292 def __enter__(self): 

293 return self 

294 

295 def __exit__(self, *args): 

296 self.clean() 

297 

298 def clean(self): 

299 if self.started: 

300 self.lmp.close() 

301 self.started = False 

302 self.initialized = False 

303 self.lmp = None 

304 

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

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

307 _ = self.prism.get_lammps_prism() 

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

309 box_hi = [xhi, yhi, zhi] 

310 

311 if change: 

312 cell_cmd = ('change_box all ' 

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

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

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

316 if self.parameters.post_changebox_cmds is not None: 

317 for cmd in self.parameters.post_changebox_cmds: 

318 self.lmp.command(cmd) 

319 else: 

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

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

322 # any calculation 

323 if self.parameters.create_box: 

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

325 

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

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

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

329 # atoms to avoid losing any 

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

331 if 's' in lammps_boundary_conditions: 

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

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

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

335 

336 for i in range(0, 3): 

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

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

339 

340 cell_cmd = ('region cell prism ' 

341 '0 {} 0 {} 0 {} ' 

342 '{} {} {} units box' 

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

344 

345 self.lmp.command(cell_cmd) 

346 

347 def set_lammps_pos(self, atoms: Atoms): 

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

349 # directions 

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

351 

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

353 # lammps neighbor list bugs. 

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

355 

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

357 # contiguous in memory 

358 lmp_positions = list(pos.ravel()) 

359 

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

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

362 lmp_c_positions = c_double_array(*lmp_positions) 

363 # self.lmp.put_coosrds(lmp_c_positions) 

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

365 

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

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

368 

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

370 dt_not_real_time=False, velocity_field=None): 

371 """"atoms: Atoms object 

372 Contains positions, unit-cell, ... 

373 properties: list of str 

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

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

376 and 'magmoms'. 

377 system_changes: list of str 

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

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

380 'pbc', 'charges' and 'magmoms'. 

381 """ 

382 if len(system_changes) == 0: 

383 return 

384 

385 if not self.started: 

386 self.start_lammps() 

387 if not self.initialized: 

388 self.initialise_lammps(atoms) 

389 else: # still need to reset cell 

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

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

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

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

394 # after this initial change_box call. 

395 

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

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

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

399 # dimensions 

400 if 'pbc' in system_changes: 

401 change_box_str = 'change_box all boundary {}' 

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

403 self.lmp.command(change_box_cmd) 

404 

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

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

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

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

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

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

411 # careful with parallelism. 

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

413 self.set_cell(atoms, change=True) 

414 

415 if self.parameters.atom_types is None: 

416 raise NameError("atom_types are mandatory.") 

417 

418 do_rebuild = (not np.array_equal(atoms.numbers, 

419 self.previous_atoms_numbers) 

420 or ("numbers" in system_changes)) 

421 if not do_rebuild: 

422 do_redo_atom_types = not np.array_equal( 

423 atoms.numbers, self.previous_atoms_numbers) 

424 else: 

425 do_redo_atom_types = False 

426 

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

428 if do_rebuild: 

429 self.rebuild(atoms) 

430 elif do_redo_atom_types: 

431 self.redo_atom_types(atoms) 

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

433 

434 self.set_lammps_pos(atoms) 

435 

436 if self.parameters.amendments is not None: 

437 for cmd in self.parameters.amendments: 

438 self.lmp.command(cmd) 

439 

440 if n_steps > 0: 

441 if velocity_field is None: 

442 vel = convert( 

443 atoms.get_velocities(), 

444 "velocity", 

445 "ASE", 

446 self.units) 

447 else: 

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

449 # here? 

450 vel = atoms.arrays[velocity_field] 

451 

452 # Transform the velocities to new coordinate system 

453 vel = self.prism.vector_to_lammps(vel) 

454 

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

456 lmp_velocities = list(vel.ravel()) 

457 

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

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

460 lmp_c_velocities = c_double_array(*lmp_velocities) 

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

462 

463 # Run for 0 time to calculate 

464 if dt is not None: 

465 if dt_not_real_time: 

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

467 else: 

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

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

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

471 

472 if n_steps > 0: 

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

474 pos = np.array( 

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

476 pos = self.prism.vector_to_ase(pos) 

477 

478 # Convert from LAMMPS units to ASE units 

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

480 

481 atoms.set_positions(pos) 

482 

483 vel = np.array( 

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

485 vel = self.prism.vector_to_lammps(vel) 

486 if velocity_field is None: 

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

488 'ASE')) 

489 

490 # Extract the forces and energy 

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

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

493 "energy", self.units, "ASE" 

494 ) 

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

496 

497 ids = self.lmp.numpy.extract_atom("id") 

498 # if ids doesn't match atoms then data is MPI distributed, which 

499 # we can't handle 

500 assert len(ids) == len(atoms) 

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

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

503 self.LMP_STYLE_ATOM, 

504 self.LMP_TYPE_VECTOR), 

505 "energy", self.units, "ASE" 

506 ) 

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

508 

509 stress = np.empty(6) 

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

511 

512 for i, var in enumerate(stress_vars): 

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

514 

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

516 stress_mat[0, 0] = stress[0] 

517 stress_mat[1, 1] = stress[1] 

518 stress_mat[2, 2] = stress[2] 

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

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

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

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

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

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

525 

526 stress_mat = self.prism.tensor2_to_ase(stress_mat) 

527 

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

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

530 stress[2] = stress_mat[2, 2] 

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

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

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

534 

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

536 

537 # definitely yields atom-id ordered force array 

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

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

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

541 

542 # otherwise check_state will always trigger a new calculation 

543 self.atoms = atoms.copy() 

544 

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

546 self.clean() 

547 

548 def lammpsbc(self, atoms): 

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

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

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

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

553 

554 retval = '' 

555 pbc = atoms.get_pbc() 

556 if np.all(pbc): 

557 retval = 'p p p' 

558 else: 

559 cell = atoms.get_cell() 

560 for i in range(0, 3): 

561 if pbc[i]: 

562 retval += 'p ' 

563 else: 

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

565 # direction 

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

567 retval += 's ' 

568 else: 

569 retval += 'f ' 

570 

571 return retval.strip() 

572 

573 def rebuild(self, atoms): 

574 try: 

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

576 except Exception: # XXX Which kind of exception? 

577 n_diff = len(atoms.numbers) 

578 

579 if n_diff > 0: 

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

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

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

583 

584 for cmd in self.parameters.lmpcmds: 

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

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

587 self.lmp.command(cmd) 

588 

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

590 self.lmp.command(cmd) 

591 elif n_diff < 0: 

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

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

594 self.lmp.command(cmd) 

595 cmd = "delete_atoms group delatoms" 

596 self.lmp.command(cmd) 

597 

598 self.redo_atom_types(atoms) 

599 

600 def redo_atom_types(self, atoms): 

601 current_types = { 

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

603 in enumerate(atoms.get_chemical_symbols())} 

604 

605 try: 

606 previous_types = { 

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

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

609 except Exception: # XXX which kind of exception? 

610 previous_types = set() 

611 

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

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

614 self.lmp.command(cmd) 

615 

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

617 

618 def restart_lammps(self, atoms): 

619 if self.started: 

620 self.lmp.command("clear") 

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

622 self.started = False 

623 self.initialized = False 

624 self.previous_atoms_numbers = [] 

625 self.start_lammps() 

626 self.initialise_lammps(atoms) 

627 

628 def start_lammps(self): 

629 # Only import lammps when running a calculation 

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

631 # module 

632 from lammps import LMP_STYLE_ATOM, LMP_TYPE_VECTOR, lammps 

633 

634 self.LMP_STYLE_ATOM = LMP_STYLE_ATOM 

635 self.LMP_TYPE_VECTOR = LMP_TYPE_VECTOR 

636 

637 # start lammps process 

638 if self.parameters.log_file is None: 

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

640 '-nocite'] 

641 else: 

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

643 '-screen', 'none', '-nocite'] 

644 

645 self.cmd_args = cmd_args 

646 

647 if self.lmp is None: 

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

649 comm=self.parameters.comm) 

650 

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

652 for cmd in self.parameters.lammps_header: 

653 self.lmp.command(cmd) 

654 

655 for cmd in self.parameters.lammps_header: 

656 if "units" in cmd: 

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

658 

659 if 'lammps_header_extra' in self.parameters: 

660 if self.parameters.lammps_header_extra is not None: 

661 for cmd in self.parameters.lammps_header_extra: 

662 self.lmp.command(cmd) 

663 

664 self.started = True 

665 

666 def initialise_lammps(self, atoms): 

667 # Initialising commands 

668 if self.parameters.boundary: 

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

670 # otherwise use atoms pbc 

671 for cmd in self.parameters.lmpcmds: 

672 if 'boundary' in cmd: 

673 break 

674 else: 

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

676 

677 # Initialize cell 

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

679 

680 if self.parameters.atom_types is None: 

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

682 s = atoms.get_chemical_symbols() 

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

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

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

686 

687 # Initialize box 

688 if self.parameters.create_box: 

689 # count number of known types 

690 n_types = len(self.parameters.atom_types) 

691 create_box_command = f'create_box {n_types} cell' 

692 self.lmp.command(create_box_command) 

693 

694 # Initialize the atoms with their types 

695 # positions do not matter here 

696 if self.parameters.create_atoms: 

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

698 self.rebuild(atoms) 

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

700 else: 

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

702 

703 # execute the user commands 

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

705 self.lmp.command(cmd) 

706 

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

708 # EAM-provided masses 

709 for sym in self.parameters.atom_types: 

710 if self.parameters.atom_type_masses is None: 

711 mass = ase_atomic_masses[ase_atomic_numbers[sym]] 

712 else: 

713 mass = self.parameters.atom_type_masses[sym] 

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

715 self.parameters.atom_types[sym], 

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

717 

718 # Define force & energy variables for extraction 

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

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

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

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

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

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

725 

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

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

728 # ensure the cell stresses are calculated 

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

730 

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

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

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

734 

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

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

737 

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

739 

740 self.initialized = True