Coverage for /builds/kinetik161/ase/ase/filters.py: 86.24%

298 statements  

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

1"""Filters""" 

2from itertools import product 

3from warnings import warn 

4 

5import numpy as np 

6 

7from ase.calculators.calculator import PropertyNotImplementedError 

8from ase.stress import full_3x3_to_voigt_6_stress, voigt_6_to_full_3x3_stress 

9from ase.utils import deprecated, lazyproperty 

10from ase.utils.abc import Optimizable 

11 

12__all__ = [ 

13 'Filter', 'StrainFilter', 'UnitCellFilter', 'FrechetCellFilter', 

14 'ExpCellFilter' 

15] 

16 

17 

18class OptimizableFilter(Optimizable): 

19 def __init__(self, filterobj): 

20 self.filterobj = filterobj 

21 

22 def get_positions(self): 

23 return self.filterobj.get_positions() 

24 

25 def set_positions(self, positions): 

26 self.filterobj.set_positions(positions) 

27 

28 def get_forces(self): 

29 return self.filterobj.get_forces() 

30 

31 @lazyproperty 

32 def _use_force_consistent_energy(self): 

33 # This boolean is in principle invalidated if the 

34 # calculator changes. This can lead to weird things 

35 # in multi-step optimizations. 

36 try: 

37 self.filterobj.get_potential_energy(force_consistent=True) 

38 except PropertyNotImplementedError: 

39 return False 

40 else: 

41 return True 

42 

43 def get_potential_energy(self): 

44 force_consistent = self._use_force_consistent_energy 

45 return self.filterobj.get_potential_energy( 

46 force_consistent=force_consistent) 

47 

48 def __len__(self): 

49 return len(self.filterobj) 

50 

51 def iterimages(self): 

52 return self.filterobj.iterimages() 

53 

54 

55class Filter: 

56 """Subset filter class.""" 

57 

58 def __init__(self, atoms, indices=None, mask=None): 

59 """Filter atoms. 

60 

61 This filter can be used to hide degrees of freedom in an Atoms 

62 object. 

63 

64 Parameters 

65 ---------- 

66 indices : list of int 

67 Indices for those atoms that should remain visible. 

68 mask : list of bool 

69 One boolean per atom indicating if the atom should remain 

70 visible or not. 

71 

72 If a Trajectory tries to save this object, it will instead 

73 save the underlying Atoms object. To prevent this, override 

74 the iterimages method. 

75 """ 

76 

77 self.atoms = atoms 

78 self.constraints = [] 

79 # Make self.info a reference to the underlying atoms' info dictionary. 

80 self.info = self.atoms.info 

81 

82 if indices is None and mask is None: 

83 raise ValueError('Use "indices" or "mask".') 

84 if indices is not None and mask is not None: 

85 raise ValueError('Use only one of "indices" and "mask".') 

86 

87 if mask is not None: 

88 self.index = np.asarray(mask, bool) 

89 self.n = self.index.sum() 

90 else: 

91 self.index = np.asarray(indices, int) 

92 self.n = len(self.index) 

93 

94 def iterimages(self): 

95 # Present the real atoms object to Trajectory and friends 

96 return self.atoms.iterimages() 

97 

98 def get_cell(self): 

99 """Returns the computational cell. 

100 

101 The computational cell is the same as for the original system. 

102 """ 

103 return self.atoms.get_cell() 

104 

105 def get_pbc(self): 

106 """Returns the periodic boundary conditions. 

107 

108 The boundary conditions are the same as for the original system. 

109 """ 

110 return self.atoms.get_pbc() 

111 

112 def get_positions(self): 

113 'Return the positions of the visible atoms.' 

114 return self.atoms.get_positions()[self.index] 

115 

116 def set_positions(self, positions, **kwargs): 

117 'Set the positions of the visible atoms.' 

118 pos = self.atoms.get_positions() 

119 pos[self.index] = positions 

120 self.atoms.set_positions(pos, **kwargs) 

121 

122 positions = property(get_positions, set_positions, 

123 doc='Positions of the atoms') 

124 

125 def get_momenta(self): 

126 'Return the momenta of the visible atoms.' 

127 return self.atoms.get_momenta()[self.index] 

128 

129 def set_momenta(self, momenta, **kwargs): 

130 'Set the momenta of the visible atoms.' 

131 mom = self.atoms.get_momenta() 

132 mom[self.index] = momenta 

133 self.atoms.set_momenta(mom, **kwargs) 

134 

135 def get_atomic_numbers(self): 

136 'Return the atomic numbers of the visible atoms.' 

137 return self.atoms.get_atomic_numbers()[self.index] 

138 

139 def set_atomic_numbers(self, atomic_numbers): 

140 'Set the atomic numbers of the visible atoms.' 

141 z = self.atoms.get_atomic_numbers() 

142 z[self.index] = atomic_numbers 

143 self.atoms.set_atomic_numbers(z) 

144 

145 def get_tags(self): 

146 'Return the tags of the visible atoms.' 

147 return self.atoms.get_tags()[self.index] 

148 

149 def set_tags(self, tags): 

150 'Set the tags of the visible atoms.' 

151 tg = self.atoms.get_tags() 

152 tg[self.index] = tags 

153 self.atoms.set_tags(tg) 

154 

155 def get_forces(self, *args, **kwargs): 

156 return self.atoms.get_forces(*args, **kwargs)[self.index] 

157 

158 def get_stress(self, *args, **kwargs): 

159 return self.atoms.get_stress(*args, **kwargs) 

160 

161 def get_stresses(self, *args, **kwargs): 

162 return self.atoms.get_stresses(*args, **kwargs)[self.index] 

163 

164 def get_masses(self): 

165 return self.atoms.get_masses()[self.index] 

166 

167 def get_potential_energy(self, **kwargs): 

168 """Calculate potential energy. 

169 

170 Returns the potential energy of the full system. 

171 """ 

172 return self.atoms.get_potential_energy(**kwargs) 

173 

174 def get_chemical_symbols(self): 

175 return self.atoms.get_chemical_symbols() 

176 

177 def get_initial_magnetic_moments(self): 

178 return self.atoms.get_initial_magnetic_moments() 

179 

180 def get_calculator(self): 

181 """Returns the calculator. 

182 

183 WARNING: The calculator is unaware of this filter, and sees a 

184 different number of atoms. 

185 """ 

186 return self.atoms.calc 

187 

188 @property 

189 def calc(self): 

190 return self.atoms.calc 

191 

192 def get_celldisp(self): 

193 return self.atoms.get_celldisp() 

194 

195 def has(self, name): 

196 'Check for existence of array.' 

197 return self.atoms.has(name) 

198 

199 def __len__(self): 

200 'Return the number of movable atoms.' 

201 return self.n 

202 

203 def __getitem__(self, i): 

204 'Return an atom.' 

205 return self.atoms[self.index[i]] 

206 

207 def __ase_optimizable__(self): 

208 return OptimizableFilter(self) 

209 

210 

211class StrainFilter(Filter): 

212 """Modify the supercell while keeping the scaled positions fixed. 

213 

214 Presents the strain of the supercell as the generalized positions, 

215 and the global stress tensor (times the volume) as the generalized 

216 force. 

217 

218 This filter can be used to relax the unit cell until the stress is 

219 zero. If MDMin is used for this, the timestep (dt) to be used 

220 depends on the system size. 0.01/x where x is a typical dimension 

221 seems like a good choice. 

222 

223 The stress and strain are presented as 6-vectors, the order of the 

224 components follow the standard engingeering practice: xx, yy, zz, 

225 yz, xz, xy. 

226 

227 """ 

228 

229 def __init__(self, atoms, mask=None, include_ideal_gas=False): 

230 """Create a filter applying a homogeneous strain to a list of atoms. 

231 

232 The first argument, atoms, is the atoms object. 

233 

234 The optional second argument, mask, is a list of six booleans, 

235 indicating which of the six independent components of the 

236 strain that are allowed to become non-zero. It defaults to 

237 [1,1,1,1,1,1]. 

238 

239 """ 

240 

241 self.strain = np.zeros(6) 

242 self.include_ideal_gas = include_ideal_gas 

243 

244 if mask is None: 

245 mask = np.ones(6) 

246 else: 

247 mask = np.array(mask) 

248 

249 Filter.__init__(self, atoms=atoms, mask=mask) 

250 self.mask = mask 

251 self.origcell = atoms.get_cell() 

252 

253 def get_positions(self): 

254 return self.strain.reshape((2, 3)).copy() 

255 

256 def set_positions(self, new): 

257 new = new.ravel() * self.mask 

258 eps = np.array([[1.0 + new[0], 0.5 * new[5], 0.5 * new[4]], 

259 [0.5 * new[5], 1.0 + new[1], 0.5 * new[3]], 

260 [0.5 * new[4], 0.5 * new[3], 1.0 + new[2]]]) 

261 

262 self.atoms.set_cell(np.dot(self.origcell, eps), scale_atoms=True) 

263 self.strain[:] = new 

264 

265 def get_forces(self, **kwargs): 

266 stress = self.atoms.get_stress(include_ideal_gas=self.include_ideal_gas) 

267 return -self.atoms.get_volume() * (stress * self.mask).reshape((2, 3)) 

268 

269 def has(self, x): 

270 return self.atoms.has(x) 

271 

272 def __len__(self): 

273 return 2 

274 

275 

276class UnitCellFilter(Filter): 

277 """Modify the supercell and the atom positions. """ 

278 

279 def __init__(self, atoms, mask=None, 

280 cell_factor=None, 

281 hydrostatic_strain=False, 

282 constant_volume=False, 

283 orig_cell=None, 

284 scalar_pressure=0.0): 

285 """Create a filter that returns the atomic forces and unit cell 

286 stresses together, so they can simultaneously be minimized. 

287 

288 The first argument, atoms, is the atoms object. The optional second 

289 argument, mask, is a list of booleans, indicating which of the six 

290 independent components of the strain are relaxed. 

291 

292 - True = relax to zero 

293 - False = fixed, ignore this component 

294 

295 Degrees of freedom are the positions in the original undeformed cell, 

296 plus the deformation tensor (extra 3 "atoms"). This gives forces 

297 consistent with numerical derivatives of the potential energy 

298 with respect to the cell degreees of freedom. 

299 

300 For full details see: 

301 E. B. Tadmor, G. S. Smith, N. Bernstein, and E. Kaxiras, 

302 Phys. Rev. B 59, 235 (1999) 

303 

304 You can still use constraints on the atoms, e.g. FixAtoms, to control 

305 the relaxation of the atoms. 

306 

307 >>> # this should be equivalent to the StrainFilter 

308 >>> atoms = Atoms(...) 

309 >>> atoms.set_constraint(FixAtoms(mask=[True for atom in atoms])) 

310 >>> ucf = UnitCellFilter(atoms) 

311 

312 You should not attach this UnitCellFilter object to a 

313 trajectory. Instead, create a trajectory for the atoms, and 

314 attach it to an optimizer like this: 

315 

316 >>> atoms = Atoms(...) 

317 >>> ucf = UnitCellFilter(atoms) 

318 >>> qn = QuasiNewton(ucf) 

319 >>> traj = Trajectory('TiO2.traj', 'w', atoms) 

320 >>> qn.attach(traj) 

321 >>> qn.run(fmax=0.05) 

322 

323 Helpful conversion table: 

324 

325 - 0.05 eV/A^3 = 8 GPA 

326 - 0.003 eV/A^3 = 0.48 GPa 

327 - 0.0006 eV/A^3 = 0.096 GPa 

328 - 0.0003 eV/A^3 = 0.048 GPa 

329 - 0.0001 eV/A^3 = 0.02 GPa 

330 

331 Additional optional arguments: 

332 

333 cell_factor: float (default float(len(atoms))) 

334 Factor by which deformation gradient is multiplied to put 

335 it on the same scale as the positions when assembling 

336 the combined position/cell vector. The stress contribution to 

337 the forces is scaled down by the same factor. This can be thought 

338 of as a very simple preconditioners. Default is number of atoms 

339 which gives approximately the correct scaling. 

340 

341 hydrostatic_strain: bool (default False) 

342 Constrain the cell by only allowing hydrostatic deformation. 

343 The virial tensor is replaced by np.diag([np.trace(virial)]*3). 

344 

345 constant_volume: bool (default False) 

346 Project out the diagonal elements of the virial tensor to allow 

347 relaxations at constant volume, e.g. for mapping out an 

348 energy-volume curve. Note: this only approximately conserves 

349 the volume and breaks energy/force consistency so can only be 

350 used with optimizers that do require do a line minimisation 

351 (e.g. FIRE). 

352 

353 scalar_pressure: float (default 0.0) 

354 Applied pressure to use for enthalpy pV term. As above, this 

355 breaks energy/force consistency. 

356 """ 

357 

358 Filter.__init__(self, atoms=atoms, indices=range(len(atoms))) 

359 self.atoms = atoms 

360 if orig_cell is None: 

361 self.orig_cell = atoms.get_cell() 

362 else: 

363 self.orig_cell = orig_cell 

364 self.stress = None 

365 

366 if mask is None: 

367 mask = np.ones(6) 

368 mask = np.asarray(mask) 

369 if mask.shape == (6,): 

370 self.mask = voigt_6_to_full_3x3_stress(mask) 

371 elif mask.shape == (3, 3): 

372 self.mask = mask 

373 else: 

374 raise ValueError('shape of mask should be (3,3) or (6,)') 

375 

376 if cell_factor is None: 

377 cell_factor = float(len(atoms)) 

378 self.hydrostatic_strain = hydrostatic_strain 

379 self.constant_volume = constant_volume 

380 self.scalar_pressure = scalar_pressure 

381 self.cell_factor = cell_factor 

382 self.copy = self.atoms.copy 

383 self.arrays = self.atoms.arrays 

384 

385 def deform_grad(self): 

386 return np.linalg.solve(self.orig_cell, self.atoms.cell).T 

387 

388 def get_positions(self): 

389 """ 

390 this returns an array with shape (natoms + 3,3). 

391 

392 the first natoms rows are the positions of the atoms, the last 

393 three rows are the deformation tensor associated with the unit cell, 

394 scaled by self.cell_factor. 

395 """ 

396 

397 cur_deform_grad = self.deform_grad() 

398 natoms = len(self.atoms) 

399 pos = np.zeros((natoms + 3, 3)) 

400 # UnitCellFilter's positions are the self.atoms.positions but without 

401 # the applied deformation gradient 

402 pos[:natoms] = np.linalg.solve(cur_deform_grad, 

403 self.atoms.positions.T).T 

404 # UnitCellFilter's cell DOFs are the deformation gradient times a 

405 # scaling factor 

406 pos[natoms:] = self.cell_factor * cur_deform_grad 

407 return pos 

408 

409 def set_positions(self, new, **kwargs): 

410 """ 

411 new is an array with shape (natoms+3,3). 

412 

413 the first natoms rows are the positions of the atoms, the last 

414 three rows are the deformation tensor used to change the cell shape. 

415 

416 the new cell is first set from original cell transformed by the new 

417 deformation gradient, then the positions are set with respect to the 

418 current cell by transforming them with the same deformation gradient 

419 """ 

420 

421 natoms = len(self.atoms) 

422 new_atom_positions = new[:natoms] 

423 new_deform_grad = new[natoms:] / self.cell_factor 

424 # Set the new cell from the original cell and the new 

425 # deformation gradient. Both current and final structures should 

426 # preserve symmetry, so if set_cell() calls FixSymmetry.adjust_cell(), 

427 # it should be OK 

428 self.atoms.set_cell(self.orig_cell @ new_deform_grad.T, 

429 scale_atoms=True) 

430 # Set the positions from the ones passed in (which are without the 

431 # deformation gradient applied) and the new deformation gradient. 

432 # This should also preserve symmetry, so if set_positions() calls 

433 # FixSymmetyr.adjust_positions(), it should be OK 

434 self.atoms.set_positions(new_atom_positions @ new_deform_grad.T, 

435 **kwargs) 

436 

437 def get_potential_energy(self, force_consistent=True): 

438 """ 

439 returns potential energy including enthalpy PV term. 

440 """ 

441 atoms_energy = self.atoms.get_potential_energy( 

442 force_consistent=force_consistent) 

443 return atoms_energy + self.scalar_pressure * self.atoms.get_volume() 

444 

445 def get_forces(self, **kwargs): 

446 """ 

447 returns an array with shape (natoms+3,3) of the atomic forces 

448 and unit cell stresses. 

449 

450 the first natoms rows are the forces on the atoms, the last 

451 three rows are the forces on the unit cell, which are 

452 computed from the stress tensor. 

453 """ 

454 

455 stress = self.atoms.get_stress(**kwargs) 

456 atoms_forces = self.atoms.get_forces(**kwargs) 

457 

458 volume = self.atoms.get_volume() 

459 virial = -volume * (voigt_6_to_full_3x3_stress(stress) + 

460 np.diag([self.scalar_pressure] * 3)) 

461 cur_deform_grad = self.deform_grad() 

462 atoms_forces = atoms_forces @ cur_deform_grad 

463 virial = np.linalg.solve(cur_deform_grad, virial.T).T 

464 

465 if self.hydrostatic_strain: 

466 vtr = virial.trace() 

467 virial = np.diag([vtr / 3.0, vtr / 3.0, vtr / 3.0]) 

468 

469 # Zero out components corresponding to fixed lattice elements 

470 if (self.mask != 1.0).any(): 

471 virial *= self.mask 

472 

473 if self.constant_volume: 

474 vtr = virial.trace() 

475 np.fill_diagonal(virial, np.diag(virial) - vtr / 3.0) 

476 

477 natoms = len(self.atoms) 

478 forces = np.zeros((natoms + 3, 3)) 

479 forces[:natoms] = atoms_forces 

480 forces[natoms:] = virial / self.cell_factor 

481 

482 self.stress = -full_3x3_to_voigt_6_stress(virial) / volume 

483 return forces 

484 

485 def get_stress(self): 

486 raise PropertyNotImplementedError 

487 

488 def has(self, x): 

489 return self.atoms.has(x) 

490 

491 def __len__(self): 

492 return (len(self.atoms) + 3) 

493 

494 

495class FrechetCellFilter(UnitCellFilter): 

496 """Modify the supercell and the atom positions.""" 

497 

498 def __init__(self, atoms, mask=None, 

499 exp_cell_factor=None, 

500 hydrostatic_strain=False, 

501 constant_volume=False, 

502 scalar_pressure=0.0): 

503 r"""Create a filter that returns the atomic forces and unit cell 

504 stresses together, so they can simultaneously be minimized. 

505 

506 The first argument, atoms, is the atoms object. The optional second 

507 argument, mask, is a list of booleans, indicating which of the six 

508 independent components of the strain are relaxed. 

509 

510 - True = relax to zero 

511 - False = fixed, ignore this component 

512 

513 Degrees of freedom are the positions in the original undeformed cell, 

514 plus the log of the deformation tensor (extra 3 "atoms"). This gives 

515 forces consistent with numerical derivatives of the potential energy 

516 with respect to the cell degrees of freedom. 

517 

518 You can still use constraints on the atoms, e.g. FixAtoms, to control 

519 the relaxation of the atoms. 

520 

521 >>> # this should be equivalent to the StrainFilter 

522 >>> atoms = Atoms(...) 

523 >>> atoms.set_constraint(FixAtoms(mask=[True for atom in atoms])) 

524 >>> ecf = FrechetCellFilter(atoms) 

525 

526 You should not attach this FrechetCellFilter object to a 

527 trajectory. Instead, create a trajectory for the atoms, and 

528 attach it to an optimizer like this: 

529 

530 >>> atoms = Atoms(...) 

531 >>> ecf = FrechetCellFilter(atoms) 

532 >>> qn = QuasiNewton(ecf) 

533 >>> traj = Trajectory('TiO2.traj', 'w', atoms) 

534 >>> qn.attach(traj) 

535 >>> qn.run(fmax=0.05) 

536 

537 Helpful conversion table: 

538 

539 - 0.05 eV/A^3 = 8 GPA 

540 - 0.003 eV/A^3 = 0.48 GPa 

541 - 0.0006 eV/A^3 = 0.096 GPa 

542 - 0.0003 eV/A^3 = 0.048 GPa 

543 - 0.0001 eV/A^3 = 0.02 GPa 

544 

545 Additional optional arguments: 

546 

547 exp_cell_factor: float (default float(len(atoms))) 

548 Scaling factor for cell variables. The cell gradients in 

549 FrechetCellFilter.get_forces() is divided by exp_cell_factor. 

550 By default, set the number of atoms. We recommend to set 

551 an extensive value for this parameter. 

552 

553 hydrostatic_strain: bool (default False) 

554 Constrain the cell by only allowing hydrostatic deformation. 

555 The virial tensor is replaced by np.diag([np.trace(virial)]*3). 

556 

557 constant_volume: bool (default False) 

558 Project out the diagonal elements of the virial tensor to allow 

559 relaxations at constant volume, e.g. for mapping out an 

560 energy-volume curve. 

561 

562 scalar_pressure: float (default 0.0) 

563 Applied pressure to use for enthalpy pV term. As above, this 

564 breaks energy/force consistency. 

565 

566 Implementation note: 

567 

568 The implementation is based on that of Christoph Ortner in JuLIP.jl: 

569 https://github.com/JuliaMolSim/JuLIP.jl/blob/master/src/expcell.jl 

570 

571 The initial implementation of ExpCellFilter gave inconsistent gradients 

572 for cell variables (matrix log of the deformation tensor). If you would 

573 like to keep the previous behavior, please use ExpCellFilter. 

574 

575 The derivation of gradients of energy w.r.t positions and the log of the 

576 deformation tensor is given in 

577 https://github.com/lan496/lan496.github.io/blob/main/notes/cell_grad.pdf 

578 """ 

579 

580 Filter.__init__(self, atoms=atoms, indices=range(len(atoms))) 

581 UnitCellFilter.__init__(self, atoms=atoms, mask=mask, 

582 hydrostatic_strain=hydrostatic_strain, 

583 constant_volume=constant_volume, 

584 scalar_pressure=scalar_pressure) 

585 

586 # We defer the scipy import to avoid high immediate import overhead 

587 from scipy.linalg import expm, expm_frechet, logm 

588 self.expm = expm 

589 self.logm = logm 

590 self.expm_frechet = expm_frechet 

591 

592 # Scaling factor for cell gradients 

593 if exp_cell_factor is None: 

594 exp_cell_factor = float(len(atoms)) 

595 self.exp_cell_factor = exp_cell_factor 

596 

597 def get_positions(self): 

598 pos = UnitCellFilter.get_positions(self) 

599 natoms = len(self.atoms) 

600 pos[natoms:] = self.logm(pos[natoms:]) * self.exp_cell_factor 

601 return pos 

602 

603 def set_positions(self, new, **kwargs): 

604 natoms = len(self.atoms) 

605 new2 = new.copy() 

606 new2[natoms:] = self.expm(new[natoms:] / self.exp_cell_factor) 

607 UnitCellFilter.set_positions(self, new2, **kwargs) 

608 

609 def get_forces(self, **kwargs): 

610 # forces on atoms are same as UnitCellFilter, we just 

611 # need to modify the stress contribution 

612 stress = self.atoms.get_stress(**kwargs) 

613 volume = self.atoms.get_volume() 

614 virial = -volume * (voigt_6_to_full_3x3_stress(stress) + 

615 np.diag([self.scalar_pressure] * 3)) 

616 

617 cur_deform_grad = self.deform_grad() 

618 cur_deform_grad_log = self.logm(cur_deform_grad) 

619 

620 if self.hydrostatic_strain: 

621 vtr = virial.trace() 

622 virial = np.diag([vtr / 3.0, vtr / 3.0, vtr / 3.0]) 

623 

624 # Zero out components corresponding to fixed lattice elements 

625 if (self.mask != 1.0).any(): 

626 virial *= self.mask 

627 

628 # Cell gradient for UnitCellFilter 

629 ucf_cell_grad = virial @ np.linalg.inv(cur_deform_grad.T) 

630 

631 # Cell gradient for FrechetCellFilter 

632 deform_grad_log_force = np.zeros((3, 3)) 

633 for mu, nu in product(range(3), repeat=2): 

634 dir = np.zeros((3, 3)) 

635 dir[mu, nu] = 1.0 

636 # Directional derivative of deformation to (mu, nu) strain direction 

637 expm_der = self.expm_frechet( 

638 cur_deform_grad_log, 

639 dir, 

640 compute_expm=False 

641 ) 

642 deform_grad_log_force[mu, nu] = np.sum(expm_der * ucf_cell_grad) 

643 

644 # Cauchy stress used for convergence testing 

645 convergence_crit_stress = -(virial / volume) 

646 if self.constant_volume: 

647 # apply constraint to force 

648 dglf_trace = deform_grad_log_force.trace() 

649 np.fill_diagonal(deform_grad_log_force, 

650 np.diag(deform_grad_log_force) - dglf_trace / 3.0) 

651 # apply constraint to Cauchy stress used for convergence testing 

652 ccs_trace = convergence_crit_stress.trace() 

653 np.fill_diagonal(convergence_crit_stress, 

654 np.diag(convergence_crit_stress) - ccs_trace / 3.0) 

655 

656 atoms_forces = self.atoms.get_forces(**kwargs) 

657 atoms_forces = atoms_forces @ cur_deform_grad 

658 

659 # pack gradients into vector 

660 natoms = len(self.atoms) 

661 forces = np.zeros((natoms + 3, 3)) 

662 forces[:natoms] = atoms_forces 

663 forces[natoms:] = deform_grad_log_force / self.exp_cell_factor 

664 self.stress = full_3x3_to_voigt_6_stress(convergence_crit_stress) 

665 return forces 

666 

667 

668class ExpCellFilter(UnitCellFilter): 

669 

670 @deprecated(DeprecationWarning( 

671 'Use FrechetCellFilter for better convergence w.r.t. cell variables.' 

672 )) 

673 def __init__(self, atoms, mask=None, 

674 cell_factor=None, 

675 hydrostatic_strain=False, 

676 constant_volume=False, 

677 scalar_pressure=0.0): 

678 r"""Create a filter that returns the atomic forces and unit cell 

679 stresses together, so they can simultaneously be minimized. 

680 

681 The first argument, atoms, is the atoms object. The optional second 

682 argument, mask, is a list of booleans, indicating which of the six 

683 independent components of the strain are relaxed. 

684 

685 - True = relax to zero 

686 - False = fixed, ignore this component 

687 

688 Degrees of freedom are the positions in the original undeformed 

689 cell, plus the log of the deformation tensor (extra 3 "atoms"). This 

690 gives forces consistent with numerical derivatives of the potential 

691 energy with respect to the cell degrees of freedom. 

692 

693 For full details see: 

694 E. B. Tadmor, G. S. Smith, N. Bernstein, and E. Kaxiras, 

695 Phys. Rev. B 59, 235 (1999) 

696 

697 You can still use constraints on the atoms, e.g. FixAtoms, to 

698 control the relaxation of the atoms. 

699 

700 >>> # this should be equivalent to the StrainFilter 

701 >>> atoms = Atoms(...) 

702 >>> atoms.set_constraint(FixAtoms(mask=[True for atom in atoms])) 

703 >>> ecf = ExpCellFilter(atoms) 

704 

705 You should not attach this ExpCellFilter object to a 

706 trajectory. Instead, create a trajectory for the atoms, and 

707 attach it to an optimizer like this: 

708 

709 >>> atoms = Atoms(...) 

710 >>> ecf = ExpCellFilter(atoms) 

711 >>> qn = QuasiNewton(ecf) 

712 >>> traj = Trajectory('TiO2.traj', 'w', atoms) 

713 >>> qn.attach(traj) 

714 >>> qn.run(fmax=0.05) 

715 

716 Helpful conversion table: 

717 

718 - 0.05 eV/A^3 = 8 GPA 

719 - 0.003 eV/A^3 = 0.48 GPa 

720 - 0.0006 eV/A^3 = 0.096 GPa 

721 - 0.0003 eV/A^3 = 0.048 GPa 

722 - 0.0001 eV/A^3 = 0.02 GPa 

723 

724 Additional optional arguments: 

725 

726 cell_factor: (DEPRECATED) 

727 Retained for backwards compatibility, but no longer used. 

728 

729 hydrostatic_strain: bool (default False) 

730 Constrain the cell by only allowing hydrostatic deformation. 

731 The virial tensor is replaced by np.diag([np.trace(virial)]*3). 

732 

733 constant_volume: bool (default False) 

734 Project out the diagonal elements of the virial tensor to allow 

735 relaxations at constant volume, e.g. for mapping out an 

736 energy-volume curve. 

737 

738 scalar_pressure: float (default 0.0) 

739 Applied pressure to use for enthalpy pV term. As above, this 

740 breaks energy/force consistency. 

741 

742 Implementation details: 

743 

744 The implementation is based on that of Christoph Ortner in JuLIP.jl: 

745 https://github.com/libAtoms/JuLIP.jl/blob/expcell/src/Constraints.jl#L244 

746 

747 We decompose the deformation gradient as 

748 

749 F = exp(U) F0 

750 x = F * F0^{-1} z = exp(U) z 

751 

752 If we write the energy as a function of U we can transform the 

753 stress associated with a perturbation V into a derivative using a 

754 linear map V -> L(U, V). 

755 

756 \phi( exp(U+tV) (z+tv) ) ~ \phi'(x) . (exp(U) v) + \phi'(x) . 

757 ( L(U, V) exp(-U) exp(U) z ) 

758 

759 where 

760 

761 \nabla E(U) : V = [S exp(-U)'] : L(U,V) 

762 = L'(U, S exp(-U)') : V 

763 = L(U', S exp(-U)') : V 

764 = L(U, S exp(-U)) : V (provided U = U') 

765 

766 where the : operator represents double contraction, 

767 i.e. A:B = trace(A'B), and 

768 

769 F = deformation tensor - 3x3 matrix 

770 F0 = reference deformation tensor - 3x3 matrix, np.eye(3) here 

771 U = cell degrees of freedom used here - 3x3 matrix 

772 V = perturbation to cell DoFs - 3x3 matrix 

773 v = perturbation to position DoFs 

774 x = atomic positions in deformed cell 

775 z = atomic positions in original cell 

776 \phi = potential energy 

777 S = stress tensor [3x3 matrix] 

778 L(U, V) = directional derivative of exp at U in direction V, i.e 

779 d/dt exp(U + t V)|_{t=0} = L(U, V) 

780 

781 This means we can write 

782 

783 d/dt E(U + t V)|_{t=0} = L(U, S exp (-U)) : V 

784 

785 and therefore the contribution to the gradient of the energy is 

786 

787 \nabla E(U) / \nabla U_ij = [L(U, S exp(-U))]_ij 

788 

789 .. deprecated:: 3.23.0 

790 Use :class:`~ase.filters.FrechetCellFilter` for better convergence 

791 w.r.t. cell variables. 

792 """ 

793 Filter.__init__(self, atoms=atoms, indices=range(len(atoms))) 

794 UnitCellFilter.__init__(self, atoms=atoms, mask=mask, 

795 cell_factor=cell_factor, 

796 hydrostatic_strain=hydrostatic_strain, 

797 constant_volume=constant_volume, 

798 scalar_pressure=scalar_pressure) 

799 if cell_factor is not None: 

800 # cell_factor used in UnitCellFilter does not affect on gradients of 

801 # ExpCellFilter. 

802 warn("cell_factor is deprecated") 

803 self.cell_factor = 1.0 

804 

805 # We defer the scipy import to avoid high immediate import overhead 

806 from scipy.linalg import expm, logm 

807 self.expm = expm 

808 self.logm = logm 

809 

810 def get_forces(self, **kwargs): 

811 forces = UnitCellFilter.get_forces(self, **kwargs) 

812 

813 # forces on atoms are same as UnitCellFilter, we just 

814 # need to modify the stress contribution 

815 stress = self.atoms.get_stress(**kwargs) 

816 volume = self.atoms.get_volume() 

817 virial = -volume * (voigt_6_to_full_3x3_stress(stress) + 

818 np.diag([self.scalar_pressure] * 3)) 

819 

820 cur_deform_grad = self.deform_grad() 

821 cur_deform_grad_log = self.logm(cur_deform_grad) 

822 

823 if self.hydrostatic_strain: 

824 vtr = virial.trace() 

825 virial = np.diag([vtr / 3.0, vtr / 3.0, vtr / 3.0]) 

826 

827 # Zero out components corresponding to fixed lattice elements 

828 if (self.mask != 1.0).any(): 

829 virial *= self.mask 

830 

831 deform_grad_log_force_naive = virial.copy() 

832 Y = np.zeros((6, 6)) 

833 Y[0:3, 0:3] = cur_deform_grad_log 

834 Y[3:6, 3:6] = cur_deform_grad_log 

835 Y[0:3, 3:6] = - virial @ self.expm(-cur_deform_grad_log) 

836 deform_grad_log_force = -self.expm(Y)[0:3, 3:6] 

837 for (i1, i2) in [(0, 1), (0, 2), (1, 2)]: 

838 ff = 0.5 * (deform_grad_log_force[i1, i2] + 

839 deform_grad_log_force[i2, i1]) 

840 deform_grad_log_force[i1, i2] = ff 

841 deform_grad_log_force[i2, i1] = ff 

842 

843 # check for reasonable alignment between naive and 

844 # exact search directions 

845 all_are_equal = np.all(np.isclose(deform_grad_log_force, 

846 deform_grad_log_force_naive)) 

847 if all_are_equal or \ 

848 (np.sum(deform_grad_log_force * deform_grad_log_force_naive) / 

849 np.sqrt(np.sum(deform_grad_log_force**2) * 

850 np.sum(deform_grad_log_force_naive**2)) > 0.8): 

851 deform_grad_log_force = deform_grad_log_force_naive 

852 

853 # Cauchy stress used for convergence testing 

854 convergence_crit_stress = -(virial / volume) 

855 if self.constant_volume: 

856 # apply constraint to force 

857 dglf_trace = deform_grad_log_force.trace() 

858 np.fill_diagonal(deform_grad_log_force, 

859 np.diag(deform_grad_log_force) - dglf_trace / 3.0) 

860 # apply constraint to Cauchy stress used for convergence testing 

861 ccs_trace = convergence_crit_stress.trace() 

862 np.fill_diagonal(convergence_crit_stress, 

863 np.diag(convergence_crit_stress) - ccs_trace / 3.0) 

864 

865 # pack gradients into vector 

866 natoms = len(self.atoms) 

867 forces[natoms:] = deform_grad_log_force 

868 self.stress = full_3x3_to_voigt_6_stress(convergence_crit_stress) 

869 return forces