Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

2 

3 

4import ctypes 

5 

6import numpy as np 

7from numpy.linalg import norm 

8 

9from ase.calculators.calculator import Calculator 

10from ase.data import (atomic_numbers as ase_atomic_numbers, 

11 chemical_symbols as ase_chemical_symbols, 

12 atomic_masses as ase_atomic_masses) 

13from ase.calculators.lammps import convert 

14from ase.geometry import wrap_positions 

15 

16# TODO 

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

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

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

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

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

22# into a python function that can be called 

23# 8. make matscipy as fallback 

24# 9. keep_alive not needed with no system changes 

25# 10. it may be a good idea to unify the cell handling with the one found in 

26# lammpsrun.py 

27 

28 

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

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

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

32 # must be (n x n) matrix 

33 assert len(arr.shape) == 2 

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

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

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

37 

38 

39def convert_cell(ase_cell): 

40 """ 

41 Convert a parallelepiped (forming right hand basis) 

42 to lower triangular matrix LAMMPS can accept. This 

43 function transposes cell matrix so the bases are column vectors 

44 """ 

45 cell = ase_cell.T 

46 

47 if not is_upper_triangular(cell): 

48 # rotate bases into triangular matrix 

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

50 A = cell[:, 0] 

51 B = cell[:, 1] 

52 C = cell[:, 2] 

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

54 Ahat = A / norm(A) 

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

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

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

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

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

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

61 

62 # create and save the transformation for coordinates 

63 volume = np.linalg.det(ase_cell) 

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

65 trans /= volume 

66 coord_transform = np.dot(tri_mat, trans) 

67 

68 return tri_mat, coord_transform 

69 else: 

70 return cell, None 

71 

72 

73class LAMMPSlib(Calculator): 

74 r""" 

75**Introduction** 

76 

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

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

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

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

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

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

83is still experimental code. 

84 

85**Arguments** 

86 

87======================= ====================================================== 

88Keyword Description 

89======================= ====================================================== 

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

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

92 

93 ["pair_style eam/alloy", 

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

95 

96``atom_types`` dictionary of ``atomic_symbol :lammps_atom_type`` 

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

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

99 lammps atom types in order that they appear in the 

100 first used atoms object. 

101 

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

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

104 override default ase.data.atomic_masses. Note that 

105 since unit conversion is done automatically in this 

106 module, these quantities must be given in the 

107 standard ase mass units (g/mol) 

108 

109``log_file`` string 

110 path to the desired LAMMPS log file 

111 

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

113 metal units and simple atom simulation. 

114 

115 lammps_header=['units metal', 

116 'atom_style atomic', 

117 'atom_modify map array sort 0 0']) 

118 

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

120 post initialization. (Use: Initialization amendments) 

121 e.g. 

122 

123 ["mass 1 58.6934"] 

124 

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

126 after any LAMMPS 'change_box' command is performed by 

127 the calculator. This is relevant because some 

128 potentials either themselves depend on the geometry 

129 and boundary conditions of the simulation box, or are 

130 frequently coupled with other LAMMPS commands that 

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

132 used with the kspace_* commands, which are sensitive 

133 to the periodicity of the simulation box. 

134 

135``keep_alive`` Boolean 

136 whether to keep the lammps routine alive for more 

137 commands 

138 

139======================= ====================================================== 

140 

141 

142**Requirements** 

143 

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

145enable the python interface. See the LAMMPS manual. 

146 

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

148 

149 >>> from lammps import lammps 

150 >>> lmp = lammps() 

151 

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

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

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

155to change to the earlier format. 

156 

157**LAMMPS and LAMMPSlib** 

158 

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

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

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

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

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

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

165extracted and returned to python. 

166 

167**Example** 

168 

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

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

171potentials) 

172 

173:: 

174 

175 from ase import Atom, Atoms 

176 from ase.build import bulk 

177 from ase.calculators.lammpslib import LAMMPSlib 

178 

179 cmds = ["pair_style eam/alloy", 

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

181 

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

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

184 NiH = Ni + H 

185 

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

187 

188 NiH.calc = lammps 

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

190 

191 

192**Implementation** 

193 

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

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

196interface are:: 

197 

198 from lammps import lammps 

199 

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

201 

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

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

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

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

206 lmp.close() # close the lammps object 

207 

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

209by invoking the get_potential_energy() method:: 

210 

211 units metal 

212 atom_style atomic 

213 atom_modify map array sort 0 0 

214 

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

216 create_box 1 cell 

217 create_atoms 1 single 0 0 0 units box 

218 mass * 1.0 

219 

220 ## user lmpcmds get executed here 

221 pair_style eam/alloy 

222 pair_coeff * * NiAlH_jea.eam.alloy Ni 

223 ## end of user lmmpcmds 

224 

225 run 0 

226 

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

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

229 

230 

231**Notes** 

232 

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

234 

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

236 and for compatibility with ASE Stress is in GPa. 

237 

238* The global energy is currently extracted from LAMMPS using 

239 extract_variable since lammps.lammps currently extract_global only 

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

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

242 

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

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

245 

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

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

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

249 

250""" 

251 

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

253 'energies'] 

254 

255 started = False 

256 initialized = False 

257 

258 default_parameters = dict( 

259 atom_types=None, 

260 atom_type_masses=None, 

261 log_file=None, 

262 lammps_name='', 

263 keep_alive=False, 

264 lammps_header=['units metal', 

265 'atom_style atomic', 

266 'atom_modify map array sort 0 0'], 

267 amendments=None, 

268 post_changebox_cmds=None, 

269 boundary=True, 

270 create_box=True, 

271 create_atoms=True, 

272 read_molecular_info=False, 

273 comm=None) 

274 

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

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

277 self.lmp = None 

278 

279 def __del__(self): 

280 if self.started: 

281 self.lmp.close() 

282 self.started = False 

283 self.lmp = None 

284 

285 def set_cell(self, atoms, change=False): 

286 lammps_cell, self.coord_transform = convert_cell(atoms.get_cell()) 

287 

288 xhi, xy, xz, _, yhi, yz, _, _, zhi = convert( 

289 lammps_cell.flatten(order='C'), "distance", "ASE", self.units) 

290 box_hi = [xhi, yhi, zhi] 

291 

292 if change: 

293 cell_cmd = ('change_box all ' 

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

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

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

297 if self.parameters.post_changebox_cmds is not None: 

298 for cmd in self.parameters.post_changebox_cmds: 

299 self.lmp.command(cmd) 

300 else: 

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

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

303 # any calculation 

304 if self.parameters.create_box: 

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

306 

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

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

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

310 # atoms to avoid losing any 

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

312 if 's' in lammps_boundary_conditions: 

313 pos = atoms.get_positions() 

314 if self.coord_transform is not None: 

315 pos = np.dot(self.coord_transform, pos.transpose()) 

316 pos = pos.transpose() 

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

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

319 

320 for i in range(0, 3): 

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

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

323 

324 cell_cmd = ('region cell prism ' 

325 '0 {} 0 {} 0 {} ' 

326 '{} {} {} units box' 

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

328 

329 self.lmp.command(cell_cmd) 

330 

331 def set_lammps_pos(self, atoms): 

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

333 # directions 

334 cell = convert(atoms.cell, "distance", "ASE", self.units) 

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

336 

337 # If necessary, transform the positions to new coordinate system 

338 if self.coord_transform is not None: 

339 pos = np.dot(pos, self.coord_transform.T) 

340 cell = np.dot(cell, self.coord_transform.T) 

341 

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

343 # lammps neighbor list bugs. 

344 pos = wrap_positions(pos, cell, atoms.get_pbc()) 

345 

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

347 # contiguous in memory 

348 lmp_positions = list(pos.ravel()) 

349 

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

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

352 lmp_c_positions = c_double_array(*lmp_positions) 

353 # self.lmp.put_coosrds(lmp_c_positions) 

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

355 

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

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

358 

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

360 dt_not_real_time=False, velocity_field=None): 

361 """"atoms: Atoms object 

362 Contains positions, unit-cell, ... 

363 properties: list of str 

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

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

366 and 'magmoms'. 

367 system_changes: list of str 

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

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

370 'pbc', 'charges' and 'magmoms'. 

371 """ 

372 if len(system_changes) == 0: 

373 return 

374 

375 self.coord_transform = None 

376 

377 if not self.started: 

378 self.start_lammps() 

379 if not self.initialized: 

380 self.initialise_lammps(atoms) 

381 else: # still need to reset cell 

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

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

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

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

386 # after this initial change_box call. 

387 

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

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

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

391 # dimensions 

392 if 'pbc' in system_changes: 

393 change_box_str = 'change_box all boundary {}' 

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

395 self.lmp.command(change_box_cmd) 

396 

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

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

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

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

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

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

403 # careful with parallelism. 

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

405 self.set_cell(atoms, change=True) 

406 

407 if self.parameters.atom_types is None: 

408 raise NameError("atom_types are mandatory.") 

409 

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

411 self.previous_atoms_numbers) 

412 or ("numbers" in system_changes)) 

413 if not do_rebuild: 

414 do_redo_atom_types = not np.array_equal( 

415 atoms.numbers, self.previous_atoms_numbers) 

416 else: 

417 do_redo_atom_types = False 

418 

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

420 if do_rebuild: 

421 self.rebuild(atoms) 

422 elif do_redo_atom_types: 

423 self.redo_atom_types(atoms) 

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

425 

426 self.set_lammps_pos(atoms) 

427 

428 if self.parameters.amendments is not None: 

429 for cmd in self.parameters.amendments: 

430 self.lmp.command(cmd) 

431 

432 if n_steps > 0: 

433 if velocity_field is None: 

434 vel = convert( 

435 atoms.get_velocities(), 

436 "velocity", 

437 "ASE", 

438 self.units) 

439 else: 

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

441 # here? 

442 vel = atoms.arrays[velocity_field] 

443 

444 # If necessary, transform the velocities to new coordinate system 

445 if self.coord_transform is not None: 

446 vel = np.dot(self.coord_transform, vel.T).T 

447 

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

449 lmp_velocities = list(vel.ravel()) 

450 

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

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

453 lmp_c_velocities = c_double_array(*lmp_velocities) 

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

455 

456 # Run for 0 time to calculate 

457 if dt is not None: 

458 if dt_not_real_time: 

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

460 else: 

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

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

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

464 

465 if n_steps > 0: 

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

467 pos = np.array( 

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

469 if self.coord_transform is not None: 

470 pos = np.dot(pos, self.coord_transform) 

471 

472 # Convert from LAMMPS units to ASE units 

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

474 

475 atoms.set_positions(pos) 

476 

477 vel = np.array( 

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

479 if self.coord_transform is not None: 

480 vel = np.dot(vel, self.coord_transform) 

481 if velocity_field is None: 

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

483 'ASE')) 

484 

485 # Extract the forces and energy 

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

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

488 "energy", self.units, "ASE" 

489 ) 

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

491 

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

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

494 # we can't handle 

495 assert len(ids) == len(atoms) 

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

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

498 self.LMP_STYLE_ATOM, 

499 self.LMP_TYPE_VECTOR), 

500 "energy", self.units, "ASE" 

501 ) 

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

503 

504 stress = np.empty(6) 

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

506 

507 for i, var in enumerate(stress_vars): 

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

509 

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

511 stress_mat[0, 0] = stress[0] 

512 stress_mat[1, 1] = stress[1] 

513 stress_mat[2, 2] = stress[2] 

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

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

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

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

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

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

520 if self.coord_transform is not None: 

521 stress_mat = np.dot(self.coord_transform.T, 

522 np.dot(stress_mat, self.coord_transform)) 

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

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

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

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

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

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

529 

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

531 

532 # definitely yields atom-id ordered force array 

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

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

535 

536 if self.coord_transform is not None: 

537 self.results['forces'] = np.dot(f, self.coord_transform) 

538 else: 

539 self.results['forces'] = f.copy() 

540 

541 # otherwise check_state will always trigger a new calculation 

542 self.atoms = atoms.copy() 

543 

544 if not self.parameters.keep_alive: 

545 self.lmp.close() 

546 

547 def lammpsbc(self, atoms): 

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

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

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

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

552 

553 retval = '' 

554 pbc = atoms.get_pbc() 

555 if np.all(pbc): 

556 retval = 'p p p' 

557 else: 

558 cell = atoms.get_cell() 

559 for i in range(0, 3): 

560 if pbc[i]: 

561 retval += 'p ' 

562 else: 

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

564 # direction 

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

566 retval += 's ' 

567 else: 

568 retval += 'f ' 

569 

570 return retval.strip() 

571 

572 def rebuild(self, atoms): 

573 try: 

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

575 except Exception: # XXX Which kind of exception? 

576 n_diff = len(atoms.numbers) 

577 

578 if n_diff > 0: 

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

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

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

582 

583 for cmd in self.parameters.lmpcmds: 

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

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

586 self.lmp.command(cmd) 

587 

588 cmd = "create_atoms 1 random {} 1 NULL".format(n_diff) 

589 self.lmp.command(cmd) 

590 elif n_diff < 0: 

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

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

593 self.lmp.command(cmd) 

594 cmd = "delete_atoms group delatoms" 

595 self.lmp.command(cmd) 

596 

597 self.redo_atom_types(atoms) 

598 

599 def redo_atom_types(self, atoms): 

600 current_types = set( 

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

602 in enumerate(atoms.get_chemical_symbols())) 

603 

604 try: 

605 previous_types = set( 

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

607 for i, Z in enumerate(self.previous_atoms_numbers)) 

608 except Exception: # XXX which kind of exception? 

609 previous_types = set() 

610 

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

612 cmd = "set atom {} type {}".format(i, i_type) 

613 self.lmp.command(cmd) 

614 

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

616 

617 def restart_lammps(self, atoms): 

618 if self.started: 

619 self.lmp.command("clear") 

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

621 self.started = False 

622 self.initialized = False 

623 self.previous_atoms_numbers = [] 

624 self.start_lammps() 

625 self.initialise_lammps(atoms) 

626 

627 def start_lammps(self): 

628 # Only import lammps when running a calculation 

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

630 # module 

631 from lammps import lammps, LMP_STYLE_ATOM, LMP_TYPE_VECTOR 

632 

633 self.LMP_STYLE_ATOM = LMP_STYLE_ATOM 

634 self.LMP_TYPE_VECTOR = LMP_TYPE_VECTOR 

635 

636 # start lammps process 

637 if self.parameters.log_file is None: 

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

639 '-nocite'] 

640 else: 

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

642 '-screen', 'none', '-nocite'] 

643 

644 self.cmd_args = cmd_args 

645 

646 if self.lmp is None: 

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

648 comm=self.parameters.comm) 

649 

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

651 for cmd in self.parameters.lammps_header: 

652 self.lmp.command(cmd) 

653 

654 for cmd in self.parameters.lammps_header: 

655 if "units" in cmd: 

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

657 

658 if 'lammps_header_extra' in self.parameters: 

659 if self.parameters.lammps_header_extra is not None: 

660 for cmd in self.parameters.lammps_header_extra: 

661 self.lmp.command(cmd) 

662 

663 self.started = True 

664 

665 def initialise_lammps(self, atoms): 

666 # Initialising commands 

667 if self.parameters.boundary: 

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

669 # otherwise use atoms pbc 

670 for cmd in self.parameters.lmpcmds: 

671 if 'boundary' in cmd: 

672 break 

673 else: 

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

675 

676 # Initialize cell 

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

678 

679 if self.parameters.atom_types is None: 

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

681 s = atoms.get_chemical_symbols() 

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

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

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

685 

686 # Initialize box 

687 if self.parameters.create_box: 

688 # count number of known types 

689 n_types = len(self.parameters.atom_types) 

690 create_box_command = 'create_box {} cell'.format(n_types) 

691 self.lmp.command(create_box_command) 

692 

693 # Initialize the atoms with their types 

694 # positions do not matter here 

695 if self.parameters.create_atoms: 

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

697 self.rebuild(atoms) 

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

699 else: 

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

701 

702 # execute the user commands 

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

704 self.lmp.command(cmd) 

705 

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

707 # EAM-provided masses 

708 for sym in self.parameters.atom_types: 

709 if self.parameters.atom_type_masses is None: 

710 mass = ase_atomic_masses[ase_atomic_numbers[sym]] 

711 else: 

712 mass = self.parameters.atom_type_masses[sym] 

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

714 self.parameters.atom_types[sym], 

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

716 

717 # Define force & energy variables for extraction 

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

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

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

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

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

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

724 

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

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

727 # ensure the cell stresses are calculated 

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

729 

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

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

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

733 

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

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

736 

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

738 

739 self.initialized = True