Coverage for /builds/alexhroom/ase/ase/constraints.py: 87.12%

1219 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2024-08-05 14:37 +0000

1"""Constraints""" 

2from typing import Sequence 

3from warnings import warn 

4 

5import numpy as np 

6 

7from ase import Atoms 

8from ase.filters import ExpCellFilter as ExpCellFilterOld 

9from ase.filters import Filter as FilterOld 

10from ase.filters import StrainFilter as StrainFilterOld 

11from ase.filters import UnitCellFilter as UnitCellFilterOld 

12from ase.geometry import (conditional_find_mic, find_mic, get_angles, 

13 get_angles_derivatives, get_dihedrals, 

14 get_dihedrals_derivatives, get_distances_derivatives, 

15 wrap_positions) 

16from ase.spacegroup.symmetrize import (prep_symmetry, refine_symmetry, 

17 symmetrize_rank1, symmetrize_rank2) 

18from ase.stress import full_3x3_to_voigt_6_stress, voigt_6_to_full_3x3_stress 

19from ase.utils import deprecated 

20from ase.utils.parsemath import eval_expression 

21 

22__all__ = [ 

23 'FixCartesian', 'FixBondLength', 'FixedMode', 

24 'FixAtoms', 'FixScaled', 'FixCom', 'FixSubsetCom', 'FixedPlane', 

25 'FixConstraint', 'FixedLine', 'FixBondLengths', 'FixLinearTriatomic', 

26 'FixInternals', 'Hookean', 'ExternalForce', 'MirrorForce', 'MirrorTorque', 

27 'FixScaledParametricRelations', 'FixCartesianParametricRelations', 

28 'FixSymmetry'] 

29 

30 

31def dict2constraint(dct): 

32 if dct['name'] not in __all__: 

33 raise ValueError 

34 return globals()[dct['name']](**dct['kwargs']) 

35 

36 

37def slice2enlist(s, n): 

38 """Convert a slice object into a list of (new, old) tuples.""" 

39 if isinstance(s, slice): 

40 return enumerate(range(*s.indices(n))) 

41 return enumerate(s) 

42 

43 

44def constrained_indices(atoms, only_include=None): 

45 """Returns a list of indices for the atoms that are constrained 

46 by a constraint that is applied. By setting only_include to a 

47 specific type of constraint you can make it only look for that 

48 given constraint. 

49 """ 

50 indices = [] 

51 for constraint in atoms.constraints: 

52 if only_include is not None: 

53 if not isinstance(constraint, only_include): 

54 continue 

55 indices.extend(np.array(constraint.get_indices())) 

56 return np.array(np.unique(indices)) 

57 

58 

59class FixConstraint: 

60 """Base class for classes that fix one or more atoms in some way.""" 

61 

62 def index_shuffle(self, atoms: Atoms, ind): 

63 """Change the indices. 

64 

65 When the ordering of the atoms in the Atoms object changes, 

66 this method can be called to shuffle the indices of the 

67 constraints. 

68 

69 ind -- List or tuple of indices. 

70 

71 """ 

72 raise NotImplementedError 

73 

74 def repeat(self, m: int, n: int): 

75 """ basic method to multiply by m, needs to know the length 

76 of the underlying atoms object for the assignment of 

77 multiplied constraints to work. 

78 """ 

79 msg = ("Repeat is not compatible with your atoms' constraints." 

80 ' Use atoms.set_constraint() before calling repeat to ' 

81 'remove your constraints.') 

82 raise NotImplementedError(msg) 

83 

84 def get_removed_dof(self, atoms: Atoms): 

85 """Get number of removed degrees of freedom due to constraint.""" 

86 raise NotImplementedError 

87 

88 def adjust_positions(self, atoms: Atoms, new): 

89 """Adjust positions.""" 

90 

91 def adjust_momenta(self, atoms: Atoms, momenta): 

92 """Adjust momenta.""" 

93 # The default is in identical manner to forces. 

94 # TODO: The default is however not always reasonable. 

95 self.adjust_forces(atoms, momenta) 

96 

97 def adjust_forces(self, atoms: Atoms, forces): 

98 """Adjust forces.""" 

99 

100 def copy(self): 

101 """Copy constraint.""" 

102 return dict2constraint(self.todict().copy()) 

103 

104 def todict(self): 

105 """Convert constraint to dictionary.""" 

106 

107 

108class IndexedConstraint(FixConstraint): 

109 def __init__(self, indices=None, mask=None): 

110 """Constrain chosen atoms. 

111 

112 Parameters 

113 ---------- 

114 indices : sequence of int 

115 Indices for those atoms that should be constrained. 

116 mask : sequence of bool 

117 One boolean per atom indicating if the atom should be 

118 constrained or not. 

119 """ 

120 

121 if mask is not None: 

122 if indices is not None: 

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

124 indices = mask 

125 indices = np.atleast_1d(indices) 

126 if np.ndim(indices) > 1: 

127 raise ValueError('indices has wrong amount of dimensions. ' 

128 f'Got {np.ndim(indices)}, expected ndim <= 1') 

129 

130 if indices.dtype == bool: 

131 indices = np.arange(len(indices))[indices] 

132 elif len(indices) == 0: 

133 indices = np.empty(0, dtype=int) 

134 elif not np.issubdtype(indices.dtype, np.integer): 

135 raise ValueError('Indices must be integers or boolean mask, ' 

136 f'not dtype={indices.dtype}') 

137 

138 if len(set(indices)) < len(indices): 

139 raise ValueError( 

140 'The indices array contains duplicates. ' 

141 'Perhaps you want to specify a mask instead, but ' 

142 'forgot the mask= keyword.') 

143 

144 self.index = indices 

145 

146 def index_shuffle(self, atoms, ind): 

147 # See docstring of superclass 

148 index = [] 

149 

150 # Resolve negative indices: 

151 actual_indices = set(np.arange(len(atoms))[self.index]) 

152 

153 for new, old in slice2enlist(ind, len(atoms)): 

154 if old in actual_indices: 

155 index.append(new) 

156 if len(index) == 0: 

157 raise IndexError('All indices in FixAtoms not part of slice') 

158 self.index = np.asarray(index, int) 

159 # XXX make immutable 

160 

161 def get_indices(self): 

162 return self.index.copy() 

163 

164 def repeat(self, m, n): 

165 i0 = 0 

166 natoms = 0 

167 if isinstance(m, int): 

168 m = (m, m, m) 

169 index_new = [] 

170 for _ in range(m[2]): 

171 for _ in range(m[1]): 

172 for _ in range(m[0]): 

173 i1 = i0 + n 

174 index_new += [i + natoms for i in self.index] 

175 i0 = i1 

176 natoms += n 

177 self.index = np.asarray(index_new, int) 

178 # XXX make immutable 

179 return self 

180 

181 def delete_atoms(self, indices, natoms): 

182 """Removes atoms from the index array, if present. 

183 

184 Required for removing atoms with existing constraint. 

185 """ 

186 

187 i = np.zeros(natoms, int) - 1 

188 new = np.delete(np.arange(natoms), indices) 

189 i[new] = np.arange(len(new)) 

190 index = i[self.index] 

191 self.index = index[index >= 0] 

192 # XXX make immutable 

193 if len(self.index) == 0: 

194 return None 

195 return self 

196 

197 

198class FixAtoms(IndexedConstraint): 

199 """Fix chosen atoms. 

200 

201 Examples 

202 -------- 

203 Fix all Copper atoms: 

204 

205 >>> from ase.build import bulk 

206 

207 >>> atoms = bulk('Cu', 'fcc', a=3.6) 

208 >>> mask = (atoms.symbols == 'Cu') 

209 >>> c = FixAtoms(mask=mask) 

210 >>> atoms.set_constraint(c) 

211 

212 Fix all atoms with z-coordinate less than 1.0 Angstrom: 

213 

214 >>> c = FixAtoms(mask=atoms.positions[:, 2] < 1.0) 

215 >>> atoms.set_constraint(c) 

216 """ 

217 

218 def get_removed_dof(self, atoms): 

219 return 3 * len(self.index) 

220 

221 def adjust_positions(self, atoms, new): 

222 new[self.index] = atoms.positions[self.index] 

223 

224 def adjust_forces(self, atoms, forces): 

225 forces[self.index] = 0.0 

226 

227 def __repr__(self): 

228 clsname = type(self).__name__ 

229 indices = ints2string(self.index) 

230 return f'{clsname}(indices={indices})' 

231 

232 def todict(self): 

233 return {'name': 'FixAtoms', 

234 'kwargs': {'indices': self.index.tolist()}} 

235 

236 

237class FixCom(FixConstraint): 

238 """Constraint class for fixing the center of mass.""" 

239 

240 index = slice(None) # all atoms 

241 

242 def get_removed_dof(self, atoms): 

243 return 3 

244 

245 def adjust_positions(self, atoms, new): 

246 masses = atoms.get_masses()[self.index] 

247 old_cm = atoms.get_center_of_mass(indices=self.index) 

248 new_cm = masses @ new[self.index] / masses.sum() 

249 diff = old_cm - new_cm 

250 new += diff 

251 

252 def adjust_momenta(self, atoms, momenta): 

253 """Adjust momenta so that the center-of-mass velocity is zero.""" 

254 masses = atoms.get_masses()[self.index] 

255 velocity_com = momenta[self.index].sum(axis=0) / masses.sum() 

256 momenta[self.index] -= masses[:, None] * velocity_com 

257 

258 def adjust_forces(self, atoms, forces): 

259 # Eqs. (3) and (7) in https://doi.org/10.1021/jp9722824 

260 masses = atoms.get_masses()[self.index] 

261 lmd = masses @ forces[self.index] / sum(masses**2) 

262 forces[self.index] -= masses[:, None] * lmd 

263 

264 def todict(self): 

265 return {'name': 'FixCom', 

266 'kwargs': {}} 

267 

268 

269class FixSubsetCom(FixCom, IndexedConstraint): 

270 """Constraint class for fixing the center of mass of a subset of atoms.""" 

271 

272 def __init__(self, indices): 

273 super().__init__(indices=indices) 

274 

275 def todict(self): 

276 return {'name': self.__class__.__name__, 

277 'kwargs': {'indices': self.index.tolist()}} 

278 

279 

280def ints2string(x, threshold=None): 

281 """Convert ndarray of ints to string.""" 

282 if threshold is None or len(x) <= threshold: 

283 return str(x.tolist()) 

284 return str(x[:threshold].tolist())[:-1] + ', ...]' 

285 

286 

287class FixBondLengths(FixConstraint): 

288 maxiter = 500 

289 

290 def __init__(self, pairs, tolerance=1e-13, 

291 bondlengths=None, iterations=None): 

292 """iterations: 

293 Ignored""" 

294 self.pairs = np.asarray(pairs) 

295 self.tolerance = tolerance 

296 self.bondlengths = bondlengths 

297 

298 def get_removed_dof(self, atoms): 

299 return len(self.pairs) 

300 

301 def adjust_positions(self, atoms, new): 

302 old = atoms.positions 

303 masses = atoms.get_masses() 

304 

305 if self.bondlengths is None: 

306 self.bondlengths = self.initialize_bond_lengths(atoms) 

307 

308 for i in range(self.maxiter): 

309 converged = True 

310 for j, ab in enumerate(self.pairs): 

311 a = ab[0] 

312 b = ab[1] 

313 cd = self.bondlengths[j] 

314 r0 = old[a] - old[b] 

315 d0, _ = find_mic(r0, atoms.cell, atoms.pbc) 

316 d1 = new[a] - new[b] - r0 + d0 

317 m = 1 / (1 / masses[a] + 1 / masses[b]) 

318 x = 0.5 * (cd**2 - np.dot(d1, d1)) / np.dot(d0, d1) 

319 if abs(x) > self.tolerance: 

320 new[a] += x * m / masses[a] * d0 

321 new[b] -= x * m / masses[b] * d0 

322 converged = False 

323 if converged: 

324 break 

325 else: 

326 raise RuntimeError('Did not converge') 

327 

328 def adjust_momenta(self, atoms, p): 

329 old = atoms.positions 

330 masses = atoms.get_masses() 

331 

332 if self.bondlengths is None: 

333 self.bondlengths = self.initialize_bond_lengths(atoms) 

334 

335 for i in range(self.maxiter): 

336 converged = True 

337 for j, ab in enumerate(self.pairs): 

338 a = ab[0] 

339 b = ab[1] 

340 cd = self.bondlengths[j] 

341 d = old[a] - old[b] 

342 d, _ = find_mic(d, atoms.cell, atoms.pbc) 

343 dv = p[a] / masses[a] - p[b] / masses[b] 

344 m = 1 / (1 / masses[a] + 1 / masses[b]) 

345 x = -np.dot(dv, d) / cd**2 

346 if abs(x) > self.tolerance: 

347 p[a] += x * m * d 

348 p[b] -= x * m * d 

349 converged = False 

350 if converged: 

351 break 

352 else: 

353 raise RuntimeError('Did not converge') 

354 

355 def adjust_forces(self, atoms, forces): 

356 self.constraint_forces = -forces 

357 self.adjust_momenta(atoms, forces) 

358 self.constraint_forces += forces 

359 

360 def initialize_bond_lengths(self, atoms): 

361 bondlengths = np.zeros(len(self.pairs)) 

362 

363 for i, ab in enumerate(self.pairs): 

364 bondlengths[i] = atoms.get_distance(ab[0], ab[1], mic=True) 

365 

366 return bondlengths 

367 

368 def get_indices(self): 

369 return np.unique(self.pairs.ravel()) 

370 

371 def todict(self): 

372 return {'name': 'FixBondLengths', 

373 'kwargs': {'pairs': self.pairs.tolist(), 

374 'tolerance': self.tolerance}} 

375 

376 def index_shuffle(self, atoms, ind): 

377 """Shuffle the indices of the two atoms in this constraint""" 

378 map = np.zeros(len(atoms), int) 

379 map[ind] = 1 

380 n = map.sum() 

381 map[:] = -1 

382 map[ind] = range(n) 

383 pairs = map[self.pairs] 

384 self.pairs = pairs[(pairs != -1).all(1)] 

385 if len(self.pairs) == 0: 

386 raise IndexError('Constraint not part of slice') 

387 

388 

389def FixBondLength(a1, a2): 

390 """Fix distance between atoms with indices a1 and a2.""" 

391 return FixBondLengths([(a1, a2)]) 

392 

393 

394class FixLinearTriatomic(FixConstraint): 

395 """Holonomic constraints for rigid linear triatomic molecules.""" 

396 

397 def __init__(self, triples): 

398 """Apply RATTLE-type bond constraints between outer atoms n and m 

399 and linear vectorial constraints to the position of central 

400 atoms o to fix the geometry of linear triatomic molecules of the 

401 type: 

402 

403 n--o--m 

404 

405 Parameters: 

406 

407 triples: list 

408 Indices of the atoms forming the linear molecules to constrain 

409 as triples. Sequence should be (n, o, m) or (m, o, n). 

410 

411 When using these constraints in molecular dynamics or structure 

412 optimizations, atomic forces need to be redistributed within a 

413 triple. The function redistribute_forces_optimization implements 

414 the redistribution of forces for structure optimization, while 

415 the function redistribute_forces_md implements the redistribution 

416 for molecular dynamics. 

417 

418 References: 

419 

420 Ciccotti et al. Molecular Physics 47 (1982) 

421 :doi:`10.1080/00268978200100942` 

422 """ 

423 self.triples = np.asarray(triples) 

424 if self.triples.shape[1] != 3: 

425 raise ValueError('"triples" has wrong size') 

426 self.bondlengths = None 

427 

428 def get_removed_dof(self, atoms): 

429 return 4 * len(self.triples) 

430 

431 @property 

432 def n_ind(self): 

433 return self.triples[:, 0] 

434 

435 @property 

436 def m_ind(self): 

437 return self.triples[:, 2] 

438 

439 @property 

440 def o_ind(self): 

441 return self.triples[:, 1] 

442 

443 def initialize(self, atoms): 

444 masses = atoms.get_masses() 

445 self.mass_n, self.mass_m, self.mass_o = self.get_slices(masses) 

446 

447 self.bondlengths = self.initialize_bond_lengths(atoms) 

448 self.bondlengths_nm = self.bondlengths.sum(axis=1) 

449 

450 C1 = self.bondlengths[:, ::-1] / self.bondlengths_nm[:, None] 

451 C2 = (C1[:, 0] ** 2 * self.mass_o * self.mass_m + 

452 C1[:, 1] ** 2 * self.mass_n * self.mass_o + 

453 self.mass_n * self.mass_m) 

454 C2 = C1 / C2[:, None] 

455 C3 = self.mass_n * C1[:, 1] - self.mass_m * C1[:, 0] 

456 C3 = C2 * self.mass_o[:, None] * C3[:, None] 

457 C3[:, 1] *= -1 

458 C3 = (C3 + 1) / np.vstack((self.mass_n, self.mass_m)).T 

459 C4 = (C1[:, 0]**2 + C1[:, 1]**2 + 1) 

460 C4 = C1 / C4[:, None] 

461 

462 self.C1 = C1 

463 self.C2 = C2 

464 self.C3 = C3 

465 self.C4 = C4 

466 

467 def adjust_positions(self, atoms, new): 

468 old = atoms.positions 

469 new_n, new_m, new_o = self.get_slices(new) 

470 

471 if self.bondlengths is None: 

472 self.initialize(atoms) 

473 

474 r0 = old[self.n_ind] - old[self.m_ind] 

475 d0, _ = find_mic(r0, atoms.cell, atoms.pbc) 

476 d1 = new_n - new_m - r0 + d0 

477 a = np.einsum('ij,ij->i', d0, d0) 

478 b = np.einsum('ij,ij->i', d1, d0) 

479 c = np.einsum('ij,ij->i', d1, d1) - self.bondlengths_nm ** 2 

480 g = (b - (b**2 - a * c)**0.5) / (a * self.C3.sum(axis=1)) 

481 g = g[:, None] * self.C3 

482 new_n -= g[:, 0, None] * d0 

483 new_m += g[:, 1, None] * d0 

484 if np.allclose(d0, r0): 

485 new_o = (self.C1[:, 0, None] * new_n 

486 + self.C1[:, 1, None] * new_m) 

487 else: 

488 v1, _ = find_mic(new_n, atoms.cell, atoms.pbc) 

489 v2, _ = find_mic(new_m, atoms.cell, atoms.pbc) 

490 rb = self.C1[:, 0, None] * v1 + self.C1[:, 1, None] * v2 

491 new_o = wrap_positions(rb, atoms.cell, atoms.pbc) 

492 

493 self.set_slices(new_n, new_m, new_o, new) 

494 

495 def adjust_momenta(self, atoms, p): 

496 old = atoms.positions 

497 p_n, p_m, p_o = self.get_slices(p) 

498 

499 if self.bondlengths is None: 

500 self.initialize(atoms) 

501 

502 mass_nn = self.mass_n[:, None] 

503 mass_mm = self.mass_m[:, None] 

504 mass_oo = self.mass_o[:, None] 

505 

506 d = old[self.n_ind] - old[self.m_ind] 

507 d, _ = find_mic(d, atoms.cell, atoms.pbc) 

508 dv = p_n / mass_nn - p_m / mass_mm 

509 k = np.einsum('ij,ij->i', dv, d) / self.bondlengths_nm ** 2 

510 k = self.C3 / (self.C3.sum(axis=1)[:, None]) * k[:, None] 

511 p_n -= k[:, 0, None] * mass_nn * d 

512 p_m += k[:, 1, None] * mass_mm * d 

513 p_o = (mass_oo * (self.C1[:, 0, None] * p_n / mass_nn + 

514 self.C1[:, 1, None] * p_m / mass_mm)) 

515 

516 self.set_slices(p_n, p_m, p_o, p) 

517 

518 def adjust_forces(self, atoms, forces): 

519 

520 if self.bondlengths is None: 

521 self.initialize(atoms) 

522 

523 A = self.C4 * np.diff(self.C1) 

524 A[:, 0] *= -1 

525 A -= 1 

526 B = np.diff(self.C4) / (A.sum(axis=1))[:, None] 

527 A /= (A.sum(axis=1))[:, None] 

528 

529 self.constraint_forces = -forces 

530 old = atoms.positions 

531 

532 fr_n, fr_m, fr_o = self.redistribute_forces_optimization(forces) 

533 

534 d = old[self.n_ind] - old[self.m_ind] 

535 d, _ = find_mic(d, atoms.cell, atoms.pbc) 

536 df = fr_n - fr_m 

537 k = -np.einsum('ij,ij->i', df, d) / self.bondlengths_nm ** 2 

538 forces[self.n_ind] = fr_n + k[:, None] * d * A[:, 0, None] 

539 forces[self.m_ind] = fr_m - k[:, None] * d * A[:, 1, None] 

540 forces[self.o_ind] = fr_o + k[:, None] * d * B 

541 

542 self.constraint_forces += forces 

543 

544 def redistribute_forces_optimization(self, forces): 

545 """Redistribute forces within a triple when performing structure 

546 optimizations. 

547 

548 The redistributed forces needs to be further adjusted using the 

549 appropriate Lagrange multipliers as implemented in adjust_forces.""" 

550 forces_n, forces_m, forces_o = self.get_slices(forces) 

551 C1_1 = self.C1[:, 0, None] 

552 C1_2 = self.C1[:, 1, None] 

553 C4_1 = self.C4[:, 0, None] 

554 C4_2 = self.C4[:, 1, None] 

555 

556 fr_n = ((1 - C4_1 * C1_1) * forces_n - 

557 C4_1 * (C1_2 * forces_m - forces_o)) 

558 fr_m = ((1 - C4_2 * C1_2) * forces_m - 

559 C4_2 * (C1_1 * forces_n - forces_o)) 

560 fr_o = ((1 - 1 / (C1_1**2 + C1_2**2 + 1)) * forces_o + 

561 C4_1 * forces_n + C4_2 * forces_m) 

562 

563 return fr_n, fr_m, fr_o 

564 

565 def redistribute_forces_md(self, atoms, forces, rand=False): 

566 """Redistribute forces within a triple when performing molecular 

567 dynamics. 

568 

569 When rand=True, use the equations for random force terms, as 

570 used e.g. by Langevin dynamics, otherwise apply the standard 

571 equations for deterministic forces (see Ciccotti et al. Molecular 

572 Physics 47 (1982)).""" 

573 if self.bondlengths is None: 

574 self.initialize(atoms) 

575 forces_n, forces_m, forces_o = self.get_slices(forces) 

576 C1_1 = self.C1[:, 0, None] 

577 C1_2 = self.C1[:, 1, None] 

578 C2_1 = self.C2[:, 0, None] 

579 C2_2 = self.C2[:, 1, None] 

580 mass_nn = self.mass_n[:, None] 

581 mass_mm = self.mass_m[:, None] 

582 mass_oo = self.mass_o[:, None] 

583 if rand: 

584 mr1 = (mass_mm / mass_nn) ** 0.5 

585 mr2 = (mass_oo / mass_nn) ** 0.5 

586 mr3 = (mass_nn / mass_mm) ** 0.5 

587 mr4 = (mass_oo / mass_mm) ** 0.5 

588 else: 

589 mr1 = 1.0 

590 mr2 = 1.0 

591 mr3 = 1.0 

592 mr4 = 1.0 

593 

594 fr_n = ((1 - C1_1 * C2_1 * mass_oo * mass_mm) * forces_n - 

595 C2_1 * (C1_2 * mr1 * mass_oo * mass_nn * forces_m - 

596 mr2 * mass_mm * mass_nn * forces_o)) 

597 

598 fr_m = ((1 - C1_2 * C2_2 * mass_oo * mass_nn) * forces_m - 

599 C2_2 * (C1_1 * mr3 * mass_oo * mass_mm * forces_n - 

600 mr4 * mass_mm * mass_nn * forces_o)) 

601 

602 self.set_slices(fr_n, fr_m, 0.0, forces) 

603 

604 def get_slices(self, a): 

605 a_n = a[self.n_ind] 

606 a_m = a[self.m_ind] 

607 a_o = a[self.o_ind] 

608 

609 return a_n, a_m, a_o 

610 

611 def set_slices(self, a_n, a_m, a_o, a): 

612 a[self.n_ind] = a_n 

613 a[self.m_ind] = a_m 

614 a[self.o_ind] = a_o 

615 

616 def initialize_bond_lengths(self, atoms): 

617 bondlengths = np.zeros((len(self.triples), 2)) 

618 

619 for i in range(len(self.triples)): 

620 bondlengths[i, 0] = atoms.get_distance(self.n_ind[i], 

621 self.o_ind[i], mic=True) 

622 bondlengths[i, 1] = atoms.get_distance(self.o_ind[i], 

623 self.m_ind[i], mic=True) 

624 

625 return bondlengths 

626 

627 def get_indices(self): 

628 return np.unique(self.triples.ravel()) 

629 

630 def todict(self): 

631 return {'name': 'FixLinearTriatomic', 

632 'kwargs': {'triples': self.triples.tolist()}} 

633 

634 def index_shuffle(self, atoms, ind): 

635 """Shuffle the indices of the three atoms in this constraint""" 

636 map = np.zeros(len(atoms), int) 

637 map[ind] = 1 

638 n = map.sum() 

639 map[:] = -1 

640 map[ind] = range(n) 

641 triples = map[self.triples] 

642 self.triples = triples[(triples != -1).all(1)] 

643 if len(self.triples) == 0: 

644 raise IndexError('Constraint not part of slice') 

645 

646 

647class FixedMode(FixConstraint): 

648 """Constrain atoms to move along directions orthogonal to 

649 a given mode only. Initialize with a mode, such as one produced by 

650 ase.vibrations.Vibrations.get_mode().""" 

651 

652 def __init__(self, mode): 

653 mode = np.asarray(mode) 

654 self.mode = (mode / np.sqrt((mode**2).sum())).reshape(-1) 

655 

656 def get_removed_dof(self, atoms): 

657 return len(atoms) 

658 

659 def adjust_positions(self, atoms, newpositions): 

660 newpositions = newpositions.ravel() 

661 oldpositions = atoms.positions.ravel() 

662 step = newpositions - oldpositions 

663 newpositions -= self.mode * np.dot(step, self.mode) 

664 

665 def adjust_forces(self, atoms, forces): 

666 forces = forces.ravel() 

667 forces -= self.mode * np.dot(forces, self.mode) 

668 

669 def index_shuffle(self, atoms, ind): 

670 eps = 1e-12 

671 mode = self.mode.reshape(-1, 3) 

672 excluded = np.ones(len(mode), dtype=bool) 

673 excluded[ind] = False 

674 if (abs(mode[excluded]) > eps).any(): 

675 raise IndexError('All nonzero parts of mode not in slice') 

676 self.mode = mode[ind].ravel() 

677 

678 def get_indices(self): 

679 # This function will never properly work because it works on all 

680 # atoms and it has no idea how to tell how many atoms it is 

681 # attached to. If it is being used, surely the user knows 

682 # everything is being constrained. 

683 return [] 

684 

685 def todict(self): 

686 return {'name': 'FixedMode', 

687 'kwargs': {'mode': self.mode.tolist()}} 

688 

689 def __repr__(self): 

690 return f'FixedMode({self.mode.tolist()})' 

691 

692 

693def _normalize(direction): 

694 if np.shape(direction) != (3,): 

695 raise ValueError("len(direction) is {len(direction)}. Has to be 3") 

696 

697 direction = np.asarray(direction) / np.linalg.norm(direction) 

698 return direction 

699 

700 

701class FixedPlane(IndexedConstraint): 

702 """ 

703 Constraint object for fixing chosen atoms to only move in a plane. 

704 

705 The plane is defined by its normal vector *direction* 

706 """ 

707 

708 def __init__(self, indices, direction): 

709 """Constrain chosen atoms. 

710 

711 Parameters 

712 ---------- 

713 indices : int or list of int 

714 Index or indices for atoms that should be constrained 

715 direction : list of 3 int 

716 Direction of the normal vector 

717 

718 Examples 

719 -------- 

720 Fix all Copper atoms to only move in the yz-plane: 

721 

722 >>> from ase.build import bulk 

723 >>> from ase.constraints import FixedPlane 

724 

725 >>> atoms = bulk('Cu', 'fcc', a=3.6) 

726 >>> c = FixedPlane( 

727 ... indices=[atom.index for atom in atoms if atom.symbol == 'Cu'], 

728 ... direction=[1, 0, 0], 

729 ... ) 

730 >>> atoms.set_constraint(c) 

731 

732 or constrain a single atom with the index 0 to move in the xy-plane: 

733 

734 >>> c = FixedPlane(indices=0, direction=[0, 0, 1]) 

735 >>> atoms.set_constraint(c) 

736 """ 

737 super().__init__(indices=indices) 

738 self.dir = _normalize(direction) 

739 

740 def adjust_positions(self, atoms, newpositions): 

741 step = newpositions[self.index] - atoms.positions[self.index] 

742 newpositions[self.index] -= _projection(step, self.dir) 

743 

744 def adjust_forces(self, atoms, forces): 

745 forces[self.index] -= _projection(forces[self.index], self.dir) 

746 

747 def get_removed_dof(self, atoms): 

748 return len(self.index) 

749 

750 def todict(self): 

751 return { 

752 'name': 'FixedPlane', 

753 'kwargs': {'indices': self.index.tolist(), 

754 'direction': self.dir.tolist()} 

755 } 

756 

757 def __repr__(self): 

758 return f'FixedPlane(indices={self.index}, {self.dir.tolist()})' 

759 

760 

761def _projection(vectors, direction): 

762 dotprods = vectors @ direction 

763 projection = direction[None, :] * dotprods[:, None] 

764 return projection 

765 

766 

767class FixedLine(IndexedConstraint): 

768 """ 

769 Constrain an atom index or a list of atom indices to move on a line only. 

770 

771 The line is defined by its vector *direction* 

772 """ 

773 

774 def __init__(self, indices, direction): 

775 """Constrain chosen atoms. 

776 

777 Parameters 

778 ---------- 

779 indices : int or list of int 

780 Index or indices for atoms that should be constrained 

781 direction : list of 3 int 

782 Direction of the vector defining the line 

783 

784 Examples 

785 -------- 

786 Fix all Copper atoms to only move in the x-direction: 

787 

788 >>> from ase.constraints import FixedLine 

789 >>> c = FixedLine( 

790 ... indices=[atom.index for atom in atoms if atom.symbol == 'Cu'], 

791 ... direction=[1, 0, 0], 

792 ... ) 

793 >>> atoms.set_constraint(c) 

794 

795 or constrain a single atom with the index 0 to move in the z-direction: 

796 

797 >>> c = FixedLine(indices=0, direction=[0, 0, 1]) 

798 >>> atoms.set_constraint(c) 

799 """ 

800 super().__init__(indices) 

801 self.dir = _normalize(direction) 

802 

803 def adjust_positions(self, atoms, newpositions): 

804 step = newpositions[self.index] - atoms.positions[self.index] 

805 projection = _projection(step, self.dir) 

806 newpositions[self.index] = atoms.positions[self.index] + projection 

807 

808 def adjust_forces(self, atoms, forces): 

809 forces[self.index] = _projection(forces[self.index], self.dir) 

810 

811 def get_removed_dof(self, atoms): 

812 return 2 * len(self.index) 

813 

814 def __repr__(self): 

815 return f'FixedLine(indices={self.index}, {self.dir.tolist()})' 

816 

817 def todict(self): 

818 return { 

819 'name': 'FixedLine', 

820 'kwargs': {'indices': self.index.tolist(), 

821 'direction': self.dir.tolist()} 

822 } 

823 

824 

825class FixCartesian(IndexedConstraint): 

826 """Fix atoms in the directions of the cartesian coordinates. 

827 

828 Parameters 

829 ---------- 

830 a : Sequence[int] 

831 Indices of atoms to be fixed. 

832 mask : tuple[bool, bool, bool], default: (True, True, True) 

833 Cartesian directions to be fixed. (False: unfixed, True: fixed) 

834 """ 

835 

836 def __init__(self, a, mask=(True, True, True)): 

837 super().__init__(indices=a) 

838 self.mask = np.asarray(mask, bool) 

839 

840 def get_removed_dof(self, atoms: Atoms): 

841 return self.mask.sum() * len(self.index) 

842 

843 def adjust_positions(self, atoms: Atoms, new): 

844 new[self.index] = np.where( 

845 self.mask[None, :], 

846 atoms.positions[self.index], 

847 new[self.index], 

848 ) 

849 

850 def adjust_forces(self, atoms: Atoms, forces): 

851 forces[self.index] *= ~self.mask[None, :] 

852 

853 def todict(self): 

854 return {'name': 'FixCartesian', 

855 'kwargs': {'a': self.index.tolist(), 

856 'mask': self.mask.tolist()}} 

857 

858 def __repr__(self): 

859 name = type(self).__name__ 

860 return f'{name}(indices={self.index.tolist()}, {self.mask.tolist()})' 

861 

862 

863class FixScaled(IndexedConstraint): 

864 """Fix atoms in the directions of the unit vectors. 

865 

866 Parameters 

867 ---------- 

868 a : Sequence[int] 

869 Indices of atoms to be fixed. 

870 mask : tuple[bool, bool, bool], default: (True, True, True) 

871 Cell directions to be fixed. (False: unfixed, True: fixed) 

872 """ 

873 

874 def __init__(self, a, mask=(True, True, True), cell=None): 

875 # XXX The unused cell keyword is there for compatibility 

876 # with old trajectory files. 

877 super().__init__(indices=a) 

878 self.mask = np.asarray(mask, bool) 

879 

880 def get_removed_dof(self, atoms: Atoms): 

881 return self.mask.sum() * len(self.index) 

882 

883 def adjust_positions(self, atoms: Atoms, new): 

884 cell = atoms.cell 

885 scaled_old = cell.scaled_positions(atoms.positions[self.index]) 

886 scaled_new = cell.scaled_positions(new[self.index]) 

887 scaled_new[:, self.mask] = scaled_old[:, self.mask] 

888 new[self.index] = cell.cartesian_positions(scaled_new) 

889 

890 def adjust_forces(self, atoms: Atoms, forces): 

891 # Forces are covariant to the coordinate transformation, 

892 # use the inverse transformations 

893 cell = atoms.cell 

894 scaled_forces = cell.cartesian_positions(forces[self.index]) 

895 scaled_forces *= -(self.mask - 1) 

896 forces[self.index] = cell.scaled_positions(scaled_forces) 

897 

898 def todict(self): 

899 return {'name': 'FixScaled', 

900 'kwargs': {'a': self.index.tolist(), 

901 'mask': self.mask.tolist()}} 

902 

903 def __repr__(self): 

904 name = type(self).__name__ 

905 return f'{name}(indices={self.index.tolist()}, {self.mask.tolist()})' 

906 

907 

908# TODO: Better interface might be to use dictionaries in place of very 

909# nested lists/tuples 

910class FixInternals(FixConstraint): 

911 """Constraint object for fixing multiple internal coordinates. 

912 

913 Allows fixing bonds, angles, dihedrals as well as linear combinations 

914 of bonds (bondcombos). 

915 

916 Please provide angular units in degrees using `angles_deg` and 

917 `dihedrals_deg`. 

918 Fixing planar angles is not supported at the moment. 

919 """ 

920 

921 def __init__(self, bonds=None, angles=None, dihedrals=None, 

922 angles_deg=None, dihedrals_deg=None, 

923 bondcombos=None, 

924 mic=False, epsilon=1.e-7): 

925 """ 

926 A constrained internal coordinate is defined as a nested list: 

927 '[value, [atom indices]]'. The constraint is initialized with a list of 

928 constrained internal coordinates, i.e. '[[value, [atom indices]], ...]'. 

929 If 'value' is None, the current value of the coordinate is constrained. 

930 

931 Parameters 

932 ---------- 

933 bonds: nested python list, optional 

934 List with targetvalue and atom indices defining the fixed bonds, 

935 i.e. [[targetvalue, [index0, index1]], ...] 

936 

937 angles_deg: nested python list, optional 

938 List with targetvalue and atom indices defining the fixedangles, 

939 i.e. [[targetvalue, [index0, index1, index3]], ...] 

940 

941 dihedrals_deg: nested python list, optional 

942 List with targetvalue and atom indices defining the fixed dihedrals, 

943 i.e. [[targetvalue, [index0, index1, index3]], ...] 

944 

945 bondcombos: nested python list, optional 

946 List with targetvalue, atom indices and linear coefficient defining 

947 the fixed linear combination of bonds, 

948 i.e. [[targetvalue, [[index0, index1, coefficient_for_bond], 

949 [index1, index2, coefficient_for_bond]]], ...] 

950 

951 mic: bool, optional, default: False 

952 Minimum image convention. 

953 

954 epsilon: float, optional, default: 1e-7 

955 Convergence criterion. 

956 """ 

957 warn_msg = 'Please specify {} in degrees using the {} argument.' 

958 if angles: 

959 warn(warn_msg.format('angles', 'angle_deg'), FutureWarning) 

960 angles = np.asarray(angles) 

961 angles[:, 0] = angles[:, 0] / np.pi * 180 

962 angles = angles.tolist() 

963 else: 

964 angles = angles_deg 

965 if dihedrals: 

966 warn(warn_msg.format('dihedrals', 'dihedrals_deg'), FutureWarning) 

967 dihedrals = np.asarray(dihedrals) 

968 dihedrals[:, 0] = dihedrals[:, 0] / np.pi * 180 

969 dihedrals = dihedrals.tolist() 

970 else: 

971 dihedrals = dihedrals_deg 

972 

973 self.bonds = bonds or [] 

974 self.angles = angles or [] 

975 self.dihedrals = dihedrals or [] 

976 self.bondcombos = bondcombos or [] 

977 self.mic = mic 

978 self.epsilon = epsilon 

979 

980 self.n = (len(self.bonds) + len(self.angles) + len(self.dihedrals) 

981 + len(self.bondcombos)) 

982 

983 # Initialize these at run-time: 

984 self.constraints = [] 

985 self.initialized = False 

986 

987 def get_removed_dof(self, atoms): 

988 return self.n 

989 

990 def initialize(self, atoms): 

991 if self.initialized: 

992 return 

993 masses = np.repeat(atoms.get_masses(), 3) 

994 cell = None 

995 pbc = None 

996 if self.mic: 

997 cell = atoms.cell 

998 pbc = atoms.pbc 

999 self.constraints = [] 

1000 for data, ConstrClass in [(self.bonds, self.FixBondLengthAlt), 

1001 (self.angles, self.FixAngle), 

1002 (self.dihedrals, self.FixDihedral), 

1003 (self.bondcombos, self.FixBondCombo)]: 

1004 for datum in data: 

1005 targetvalue = datum[0] 

1006 if targetvalue is None: # set to current value 

1007 targetvalue = ConstrClass.get_value(atoms, datum[1], 

1008 self.mic) 

1009 constr = ConstrClass(targetvalue, datum[1], masses, cell, pbc) 

1010 self.constraints.append(constr) 

1011 self.initialized = True 

1012 

1013 @staticmethod 

1014 def get_bondcombo(atoms, indices, mic=False): 

1015 """Convenience function to return the value of the bondcombo coordinate 

1016 (linear combination of bond lengths) for the given Atoms object 'atoms'. 

1017 Example: Get the current value of the linear combination of two bond 

1018 lengths defined as `bondcombo = [[0, 1, 1.0], [2, 3, -1.0]]`.""" 

1019 c = sum(df[2] * atoms.get_distance(*df[:2], mic=mic) for df in indices) 

1020 return c 

1021 

1022 def get_subconstraint(self, atoms, definition): 

1023 """Get pointer to a specific subconstraint. 

1024 Identification by its definition via indices (and coefficients).""" 

1025 self.initialize(atoms) 

1026 for subconstr in self.constraints: 

1027 if isinstance(definition[0], Sequence): # Combo constraint 

1028 defin = [d + [c] for d, c in zip(subconstr.indices, 

1029 subconstr.coefs)] 

1030 if defin == definition: 

1031 return subconstr 

1032 else: # identify primitive constraints by their indices 

1033 if subconstr.indices == [definition]: 

1034 return subconstr 

1035 raise ValueError('Given `definition` not found on Atoms object.') 

1036 

1037 def shuffle_definitions(self, shuffle_dic, internal_type): 

1038 dfns = [] # definitions 

1039 for dfn in internal_type: # e.g. for bond in self.bonds 

1040 append = True 

1041 new_dfn = [dfn[0], list(dfn[1])] 

1042 for old in dfn[1]: 

1043 if old in shuffle_dic: 

1044 new_dfn[1][dfn[1].index(old)] = shuffle_dic[old] 

1045 else: 

1046 append = False 

1047 break 

1048 if append: 

1049 dfns.append(new_dfn) 

1050 return dfns 

1051 

1052 def shuffle_combos(self, shuffle_dic, internal_type): 

1053 dfns = [] # definitions 

1054 for dfn in internal_type: # i.e. for bondcombo in self.bondcombos 

1055 append = True 

1056 all_indices = [idx[0:-1] for idx in dfn[1]] 

1057 new_dfn = [dfn[0], list(dfn[1])] 

1058 for i, indices in enumerate(all_indices): 

1059 for old in indices: 

1060 if old in shuffle_dic: 

1061 new_dfn[1][i][indices.index(old)] = shuffle_dic[old] 

1062 else: 

1063 append = False 

1064 break 

1065 if not append: 

1066 break 

1067 if append: 

1068 dfns.append(new_dfn) 

1069 return dfns 

1070 

1071 def index_shuffle(self, atoms, ind): 

1072 # See docstring of superclass 

1073 self.initialize(atoms) 

1074 shuffle_dic = dict(slice2enlist(ind, len(atoms))) 

1075 shuffle_dic = {old: new for new, old in shuffle_dic.items()} 

1076 self.bonds = self.shuffle_definitions(shuffle_dic, self.bonds) 

1077 self.angles = self.shuffle_definitions(shuffle_dic, self.angles) 

1078 self.dihedrals = self.shuffle_definitions(shuffle_dic, self.dihedrals) 

1079 self.bondcombos = self.shuffle_combos(shuffle_dic, self.bondcombos) 

1080 self.initialized = False 

1081 self.initialize(atoms) 

1082 if len(self.constraints) == 0: 

1083 raise IndexError('Constraint not part of slice') 

1084 

1085 def get_indices(self): 

1086 cons = [] 

1087 for dfn in self.bonds + self.dihedrals + self.angles: 

1088 cons.extend(dfn[1]) 

1089 for dfn in self.bondcombos: 

1090 for partial_dfn in dfn[1]: 

1091 cons.extend(partial_dfn[0:-1]) # last index is the coefficient 

1092 return list(set(cons)) 

1093 

1094 def todict(self): 

1095 return {'name': 'FixInternals', 

1096 'kwargs': {'bonds': self.bonds, 

1097 'angles_deg': self.angles, 

1098 'dihedrals_deg': self.dihedrals, 

1099 'bondcombos': self.bondcombos, 

1100 'mic': self.mic, 

1101 'epsilon': self.epsilon}} 

1102 

1103 def adjust_positions(self, atoms, newpos): 

1104 self.initialize(atoms) 

1105 for constraint in self.constraints: 

1106 constraint.setup_jacobian(atoms.positions) 

1107 for _ in range(50): 

1108 maxerr = 0.0 

1109 for constraint in self.constraints: 

1110 constraint.adjust_positions(atoms.positions, newpos) 

1111 maxerr = max(abs(constraint.sigma), maxerr) 

1112 if maxerr < self.epsilon: 

1113 return 

1114 msg = 'FixInternals.adjust_positions did not converge.' 

1115 if any(constr.targetvalue > 175. or constr.targetvalue < 5. for constr 

1116 in self.constraints if isinstance(constr, self.FixAngle)): 

1117 msg += (' This may be caused by an almost planar angle.' 

1118 ' Support for planar angles would require the' 

1119 ' implementation of ghost, i.e. dummy, atoms.' 

1120 ' See issue #868.') 

1121 raise ValueError(msg) 

1122 

1123 def adjust_forces(self, atoms, forces): 

1124 """Project out translations and rotations and all other constraints""" 

1125 self.initialize(atoms) 

1126 positions = atoms.positions 

1127 N = len(forces) 

1128 list2_constraints = list(np.zeros((6, N, 3))) 

1129 tx, ty, tz, rx, ry, rz = list2_constraints 

1130 

1131 list_constraints = [r.ravel() for r in list2_constraints] 

1132 

1133 tx[:, 0] = 1.0 

1134 ty[:, 1] = 1.0 

1135 tz[:, 2] = 1.0 

1136 ff = forces.ravel() 

1137 

1138 # Calculate the center of mass 

1139 center = positions.sum(axis=0) / N 

1140 

1141 rx[:, 1] = -(positions[:, 2] - center[2]) 

1142 rx[:, 2] = positions[:, 1] - center[1] 

1143 ry[:, 0] = positions[:, 2] - center[2] 

1144 ry[:, 2] = -(positions[:, 0] - center[0]) 

1145 rz[:, 0] = -(positions[:, 1] - center[1]) 

1146 rz[:, 1] = positions[:, 0] - center[0] 

1147 

1148 # Normalizing transl., rotat. constraints 

1149 for r in list2_constraints: 

1150 r /= np.linalg.norm(r.ravel()) 

1151 

1152 # Add all angle, etc. constraint vectors 

1153 for constraint in self.constraints: 

1154 constraint.setup_jacobian(positions) 

1155 constraint.adjust_forces(positions, forces) 

1156 list_constraints.insert(0, constraint.jacobian) 

1157 # QR DECOMPOSITION - GRAM SCHMIDT 

1158 

1159 list_constraints = [r.ravel() for r in list_constraints] 

1160 aa = np.column_stack(list_constraints) 

1161 (aa, bb) = np.linalg.qr(aa) 

1162 # Projection 

1163 hh = [] 

1164 for i, constraint in enumerate(self.constraints): 

1165 hh.append(aa[:, i] * np.vstack(aa[:, i])) 

1166 

1167 txx = aa[:, self.n] * np.vstack(aa[:, self.n]) 

1168 tyy = aa[:, self.n + 1] * np.vstack(aa[:, self.n + 1]) 

1169 tzz = aa[:, self.n + 2] * np.vstack(aa[:, self.n + 2]) 

1170 rxx = aa[:, self.n + 3] * np.vstack(aa[:, self.n + 3]) 

1171 ryy = aa[:, self.n + 4] * np.vstack(aa[:, self.n + 4]) 

1172 rzz = aa[:, self.n + 5] * np.vstack(aa[:, self.n + 5]) 

1173 T = txx + tyy + tzz + rxx + ryy + rzz 

1174 for vec in hh: 

1175 T += vec 

1176 ff = np.dot(T, np.vstack(ff)) 

1177 forces[:, :] -= np.dot(T, np.vstack(ff)).reshape(-1, 3) 

1178 

1179 def __repr__(self): 

1180 constraints = [repr(constr) for constr in self.constraints] 

1181 return f'FixInternals(_copy_init={constraints}, epsilon={self.epsilon})' 

1182 

1183 # Classes for internal use in FixInternals 

1184 class FixInternalsBase: 

1185 """Base class for subclasses of FixInternals.""" 

1186 

1187 def __init__(self, targetvalue, indices, masses, cell, pbc): 

1188 self.targetvalue = targetvalue # constant target value 

1189 self.indices = [defin[0:-1] for defin in indices] # indices, defs 

1190 self.coefs = np.asarray([defin[-1] for defin in indices]) 

1191 self.masses = masses 

1192 self.jacobian = [] # geometric Jacobian matrix, Wilson B-matrix 

1193 self.sigma = 1. # difference between current and target value 

1194 self.projected_force = None # helps optimizers scan along constr. 

1195 self.cell = cell 

1196 self.pbc = pbc 

1197 

1198 def finalize_jacobian(self, pos, n_internals, n, derivs): 

1199 """Populate jacobian with derivatives for `n_internals` defined 

1200 internals. n = 2 (bonds), 3 (angles), 4 (dihedrals).""" 

1201 jacobian = np.zeros((n_internals, *pos.shape)) 

1202 for i, idx in enumerate(self.indices): 

1203 for j in range(n): 

1204 jacobian[i, idx[j]] = derivs[i, j] 

1205 jacobian = jacobian.reshape((n_internals, 3 * len(pos))) 

1206 return self.coefs @ jacobian 

1207 

1208 def finalize_positions(self, newpos): 

1209 jacobian = self.jacobian / self.masses 

1210 lamda = -self.sigma / (jacobian @ self.get_jacobian(newpos)) 

1211 dnewpos = lamda * jacobian 

1212 newpos += dnewpos.reshape(newpos.shape) 

1213 

1214 def adjust_forces(self, positions, forces): 

1215 self.projected_forces = ((self.jacobian @ forces.ravel()) 

1216 * self.jacobian) 

1217 self.jacobian /= np.linalg.norm(self.jacobian) 

1218 

1219 class FixBondCombo(FixInternalsBase): 

1220 """Constraint subobject for fixing linear combination of bond lengths 

1221 within FixInternals. 

1222 

1223 sum_i( coef_i * bond_length_i ) = constant 

1224 """ 

1225 

1226 def get_jacobian(self, pos): 

1227 bondvectors = [pos[k] - pos[h] for h, k in self.indices] 

1228 derivs = get_distances_derivatives(bondvectors, cell=self.cell, 

1229 pbc=self.pbc) 

1230 return self.finalize_jacobian(pos, len(bondvectors), 2, derivs) 

1231 

1232 def setup_jacobian(self, pos): 

1233 self.jacobian = self.get_jacobian(pos) 

1234 

1235 def adjust_positions(self, oldpos, newpos): 

1236 bondvectors = [newpos[k] - newpos[h] for h, k in self.indices] 

1237 (_, ), (dists, ) = conditional_find_mic([bondvectors], 

1238 cell=self.cell, 

1239 pbc=self.pbc) 

1240 value = self.coefs @ dists 

1241 self.sigma = value - self.targetvalue 

1242 self.finalize_positions(newpos) 

1243 

1244 @staticmethod 

1245 def get_value(atoms, indices, mic): 

1246 return FixInternals.get_bondcombo(atoms, indices, mic) 

1247 

1248 def __repr__(self): 

1249 return (f'FixBondCombo({self.targetvalue}, {self.indices}, ' 

1250 '{self.coefs})') 

1251 

1252 class FixBondLengthAlt(FixBondCombo): 

1253 """Constraint subobject for fixing bond length within FixInternals. 

1254 Fix distance between atoms with indices a1, a2.""" 

1255 

1256 def __init__(self, targetvalue, indices, masses, cell, pbc): 

1257 if targetvalue <= 0.: 

1258 raise ZeroDivisionError('Invalid targetvalue for fixed bond') 

1259 indices = [list(indices) + [1.]] # bond definition with coef 1. 

1260 super().__init__(targetvalue, indices, masses, cell=cell, pbc=pbc) 

1261 

1262 @staticmethod 

1263 def get_value(atoms, indices, mic): 

1264 return atoms.get_distance(*indices, mic=mic) 

1265 

1266 def __repr__(self): 

1267 return f'FixBondLengthAlt({self.targetvalue}, {self.indices})' 

1268 

1269 class FixAngle(FixInternalsBase): 

1270 """Constraint subobject for fixing an angle within FixInternals. 

1271 

1272 Convergence is potentially problematic for angles very close to 

1273 0 or 180 degrees as there is a singularity in the Cartesian derivative. 

1274 Fixing planar angles is therefore not supported at the moment. 

1275 """ 

1276 

1277 def __init__(self, targetvalue, indices, masses, cell, pbc): 

1278 """Fix atom movement to construct a constant angle.""" 

1279 if targetvalue <= 0. or targetvalue >= 180.: 

1280 raise ZeroDivisionError('Invalid targetvalue for fixed angle') 

1281 indices = [list(indices) + [1.]] # angle definition with coef 1. 

1282 super().__init__(targetvalue, indices, masses, cell=cell, pbc=pbc) 

1283 

1284 def gather_vectors(self, pos): 

1285 v0 = [pos[h] - pos[k] for h, k, l in self.indices] 

1286 v1 = [pos[l] - pos[k] for h, k, l in self.indices] 

1287 return v0, v1 

1288 

1289 def get_jacobian(self, pos): 

1290 v0, v1 = self.gather_vectors(pos) 

1291 derivs = get_angles_derivatives(v0, v1, cell=self.cell, 

1292 pbc=self.pbc) 

1293 return self.finalize_jacobian(pos, len(v0), 3, derivs) 

1294 

1295 def setup_jacobian(self, pos): 

1296 self.jacobian = self.get_jacobian(pos) 

1297 

1298 def adjust_positions(self, oldpos, newpos): 

1299 v0, v1 = self.gather_vectors(newpos) 

1300 value = get_angles(v0, v1, cell=self.cell, pbc=self.pbc) 

1301 self.sigma = value - self.targetvalue 

1302 self.finalize_positions(newpos) 

1303 

1304 @staticmethod 

1305 def get_value(atoms, indices, mic): 

1306 return atoms.get_angle(*indices, mic=mic) 

1307 

1308 def __repr__(self): 

1309 return f'FixAngle({self.targetvalue}, {self.indices})' 

1310 

1311 class FixDihedral(FixInternalsBase): 

1312 """Constraint subobject for fixing a dihedral angle within FixInternals. 

1313 

1314 A dihedral becomes undefined when at least one of the inner two angles 

1315 becomes planar. Make sure to avoid this situation. 

1316 """ 

1317 

1318 def __init__(self, targetvalue, indices, masses, cell, pbc): 

1319 indices = [list(indices) + [1.]] # dihedral def. with coef 1. 

1320 super().__init__(targetvalue, indices, masses, cell=cell, pbc=pbc) 

1321 

1322 def gather_vectors(self, pos): 

1323 v0 = [pos[k] - pos[h] for h, k, l, m in self.indices] 

1324 v1 = [pos[l] - pos[k] for h, k, l, m in self.indices] 

1325 v2 = [pos[m] - pos[l] for h, k, l, m in self.indices] 

1326 return v0, v1, v2 

1327 

1328 def get_jacobian(self, pos): 

1329 v0, v1, v2 = self.gather_vectors(pos) 

1330 derivs = get_dihedrals_derivatives(v0, v1, v2, cell=self.cell, 

1331 pbc=self.pbc) 

1332 return self.finalize_jacobian(pos, len(v0), 4, derivs) 

1333 

1334 def setup_jacobian(self, pos): 

1335 self.jacobian = self.get_jacobian(pos) 

1336 

1337 def adjust_positions(self, oldpos, newpos): 

1338 v0, v1, v2 = self.gather_vectors(newpos) 

1339 value = get_dihedrals(v0, v1, v2, cell=self.cell, pbc=self.pbc) 

1340 # apply minimum dihedral difference 'convention': (diff <= 180) 

1341 self.sigma = (value - self.targetvalue + 180) % 360 - 180 

1342 self.finalize_positions(newpos) 

1343 

1344 @staticmethod 

1345 def get_value(atoms, indices, mic): 

1346 return atoms.get_dihedral(*indices, mic=mic) 

1347 

1348 def __repr__(self): 

1349 return f'FixDihedral({self.targetvalue}, {self.indices})' 

1350 

1351 

1352class FixParametricRelations(FixConstraint): 

1353 

1354 def __init__( 

1355 self, 

1356 indices, 

1357 Jacobian, 

1358 const_shift, 

1359 params=None, 

1360 eps=1e-12, 

1361 use_cell=False, 

1362 ): 

1363 """Constrains the degrees of freedom to act in a reduced parameter 

1364 space defined by the Jacobian 

1365 

1366 These constraints are based off the work in: 

1367 https://arxiv.org/abs/1908.01610 

1368 

1369 The constraints linearly maps the full 3N degrees of freedom, 

1370 where N is number of active lattice vectors/atoms onto a 

1371 reduced subset of M free parameters, where M <= 3*N. The 

1372 Jacobian matrix and constant shift vector map the full set of 

1373 degrees of freedom onto the reduced parameter space. 

1374 

1375 Currently the constraint is set up to handle either atomic 

1376 positions or lattice vectors at one time, but not both. To do 

1377 both simply add a two constraints for each set. This is done 

1378 to keep the mathematics behind the operations separate. 

1379 

1380 It would be possible to extend these constraints to allow 

1381 non-linear transformations if functionality to update the 

1382 Jacobian at each position update was included. This would 

1383 require passing an update function evaluate it every time 

1384 adjust_positions is callled. This is currently NOT supported, 

1385 and there are no plans to implement it in the future. 

1386 

1387 Args: 

1388 indices (list of int): indices of the constrained atoms 

1389 (if not None or empty then cell_indices must be None or Empty) 

1390 Jacobian (np.ndarray(shape=(3*len(indices), len(params)))): 

1391 The Jacobian describing 

1392 the parameter space transformation 

1393 const_shift (np.ndarray(shape=(3*len(indices)))): 

1394 A vector describing the constant term 

1395 in the transformation not accounted for in the Jacobian 

1396 params (list of str): 

1397 parameters used in the parametric representation 

1398 if None a list is generated based on the shape of the Jacobian 

1399 eps (float): a small number to compare the similarity of 

1400 numbers and set the precision used 

1401 to generate the constraint expressions 

1402 use_cell (bool): if True then act on the cell object 

1403 

1404 """ 

1405 self.indices = np.array(indices) 

1406 self.Jacobian = np.array(Jacobian) 

1407 self.const_shift = np.array(const_shift) 

1408 

1409 assert self.const_shift.shape[0] == 3 * len(self.indices) 

1410 assert self.Jacobian.shape[0] == 3 * len(self.indices) 

1411 

1412 self.eps = eps 

1413 self.use_cell = use_cell 

1414 

1415 if params is None: 

1416 params = [] 

1417 if self.Jacobian.shape[1] > 0: 

1418 int_fmt_str = "{:0" + \ 

1419 str(int(np.ceil(np.log10(self.Jacobian.shape[1])))) + "d}" 

1420 for param_ind in range(self.Jacobian.shape[1]): 

1421 params.append("param_" + int_fmt_str.format(param_ind)) 

1422 else: 

1423 assert len(params) == self.Jacobian.shape[-1] 

1424 

1425 self.params = params 

1426 

1427 self.Jacobian_inv = np.linalg.inv( 

1428 self.Jacobian.T @ self.Jacobian) @ self.Jacobian.T 

1429 

1430 @classmethod 

1431 def from_expressions(cls, indices, params, expressions, 

1432 eps=1e-12, use_cell=False): 

1433 """Converts the expressions into a Jacobian Matrix/const_shift 

1434 vector and constructs a FixParametricRelations constraint 

1435 

1436 The expressions must be a list like object of size 3*N and 

1437 elements must be ordered as: 

1438 [n_0,i; n_0,j; n_0,k; n_1,i; n_1,j; .... ; n_N-1,i; n_N-1,j; n_N-1,k], 

1439 where i, j, and k are the first, second and third 

1440 component of the atomic position/lattice 

1441 vector. Currently only linear operations are allowed to be 

1442 included in the expressions so 

1443 only terms like: 

1444 - const * param_0 

1445 - sqrt[const] * param_1 

1446 - const * param_0 +/- const * param_1 +/- ... +/- const * param_M 

1447 where const is any real number and param_0, param_1, ..., param_M are 

1448 the parameters passed in 

1449 params, are allowed. 

1450 

1451 For example, fractional atomic position constraints for wurtzite are: 

1452 params = ["z1", "z2"] 

1453 expressions = [ 

1454 "1.0/3.0", "2.0/3.0", "z1", 

1455 "2.0/3.0", "1.0/3.0", "0.5 + z1", 

1456 "1.0/3.0", "2.0/3.0", "z2", 

1457 "2.0/3.0", "1.0/3.0", "0.5 + z2", 

1458 ] 

1459 

1460 For diamond are: 

1461 params = [] 

1462 expressions = [ 

1463 "0.0", "0.0", "0.0", 

1464 "0.25", "0.25", "0.25", 

1465 ], 

1466 

1467 and for stannite are 

1468 params=["x4", "z4"] 

1469 expressions = [ 

1470 "0.0", "0.0", "0.0", 

1471 "0.0", "0.5", "0.5", 

1472 "0.75", "0.25", "0.5", 

1473 "0.25", "0.75", "0.5", 

1474 "x4 + z4", "x4 + z4", "2*x4", 

1475 "x4 - z4", "x4 - z4", "-2*x4", 

1476 "0.0", "-1.0 * (x4 + z4)", "x4 - z4", 

1477 "0.0", "x4 - z4", "-1.0 * (x4 + z4)", 

1478 ] 

1479 

1480 Args: 

1481 indices (list of int): indices of the constrained atoms 

1482 (if not None or empty then cell_indices must be None or Empty) 

1483 params (list of str): parameters used in the 

1484 parametric representation 

1485 expressions (list of str): expressions used to convert from the 

1486 parametric to the real space representation 

1487 eps (float): a small number to compare the similarity of 

1488 numbers and set the precision used 

1489 to generate the constraint expressions 

1490 use_cell (bool): if True then act on the cell object 

1491 

1492 Returns: 

1493 cls( 

1494 indices, 

1495 Jacobian generated from expressions, 

1496 const_shift generated from expressions, 

1497 params, 

1498 eps-12, 

1499 use_cell, 

1500 ) 

1501 """ 

1502 Jacobian = np.zeros((3 * len(indices), len(params))) 

1503 const_shift = np.zeros(3 * len(indices)) 

1504 

1505 for expr_ind, expression in enumerate(expressions): 

1506 expression = expression.strip() 

1507 

1508 # Convert subtraction to addition 

1509 expression = expression.replace("-", "+(-1.0)*") 

1510 if expression[0] == "+": 

1511 expression = expression[1:] 

1512 elif expression[:2] == "(+": 

1513 expression = "(" + expression[2:] 

1514 

1515 # Explicitly add leading zeros so when replacing param_1 with 0.0 

1516 # param_11 does not become 0.01 

1517 int_fmt_str = "{:0" + \ 

1518 str(int(np.ceil(np.log10(len(params) + 1)))) + "d}" 

1519 

1520 param_dct = {} 

1521 param_map = {} 

1522 

1523 # Construct a standardized param template for A/B filling 

1524 for param_ind, param in enumerate(params): 

1525 param_str = "param_" + int_fmt_str.format(param_ind) 

1526 param_map[param] = param_str 

1527 param_dct[param_str] = 0.0 

1528 

1529 # Replace the parameters according to the map 

1530 # Sort by string length (long to short) to prevent cases like x11 

1531 # becoming f"{param_map["x1"]}1" 

1532 for param in sorted(params, key=lambda s: -1.0 * len(s)): 

1533 expression = expression.replace(param, param_map[param]) 

1534 

1535 # Partial linearity check 

1536 for express_sec in expression.split("+"): 

1537 in_sec = [param in express_sec for param in param_dct] 

1538 n_params_in_sec = len(np.where(np.array(in_sec))[0]) 

1539 if n_params_in_sec > 1: 

1540 raise ValueError( 

1541 "FixParametricRelations expressions must be linear.") 

1542 

1543 const_shift[expr_ind] = float( 

1544 eval_expression(expression, param_dct)) 

1545 

1546 for param_ind in range(len(params)): 

1547 param_str = "param_" + int_fmt_str.format(param_ind) 

1548 if param_str not in expression: 

1549 Jacobian[expr_ind, param_ind] = 0.0 

1550 continue 

1551 param_dct[param_str] = 1.0 

1552 test_1 = float(eval_expression(expression, param_dct)) 

1553 test_1 -= const_shift[expr_ind] 

1554 Jacobian[expr_ind, param_ind] = test_1 

1555 

1556 param_dct[param_str] = 2.0 

1557 test_2 = float(eval_expression(expression, param_dct)) 

1558 test_2 -= const_shift[expr_ind] 

1559 if abs(test_2 / test_1 - 2.0) > eps: 

1560 raise ValueError( 

1561 "FixParametricRelations expressions must be linear.") 

1562 param_dct[param_str] = 0.0 

1563 

1564 args = [ 

1565 indices, 

1566 Jacobian, 

1567 const_shift, 

1568 params, 

1569 eps, 

1570 use_cell, 

1571 ] 

1572 if cls is FixScaledParametricRelations: 

1573 args = args[:-1] 

1574 return cls(*args) 

1575 

1576 @property 

1577 def expressions(self): 

1578 """Generate the expressions represented by the current self.Jacobian 

1579 and self.const_shift objects""" 

1580 expressions = [] 

1581 per = int(round(-1 * np.log10(self.eps))) 

1582 fmt_str = "{:." + str(per + 1) + "g}" 

1583 for index, shift_val in enumerate(self.const_shift): 

1584 exp = "" 

1585 if np.all(np.abs(self.Jacobian[index]) < self.eps) or np.abs( 

1586 shift_val) > self.eps: 

1587 exp += fmt_str.format(shift_val) 

1588 

1589 param_exp = "" 

1590 for param_index, jacob_val in enumerate(self.Jacobian[index]): 

1591 abs_jacob_val = np.round(np.abs(jacob_val), per + 1) 

1592 if abs_jacob_val < self.eps: 

1593 continue 

1594 

1595 param = self.params[param_index] 

1596 if param_exp or exp: 

1597 if jacob_val > -1.0 * self.eps: 

1598 param_exp += " + " 

1599 else: 

1600 param_exp += " - " 

1601 elif (not exp) and (not param_exp) and ( 

1602 jacob_val < -1.0 * self.eps): 

1603 param_exp += "-" 

1604 

1605 if np.abs(abs_jacob_val - 1.0) <= self.eps: 

1606 param_exp += f"{param:s}" 

1607 else: 

1608 param_exp += (fmt_str + 

1609 "*{:s}").format(abs_jacob_val, param) 

1610 

1611 exp += param_exp 

1612 

1613 expressions.append(exp) 

1614 return np.array(expressions).reshape((-1, 3)) 

1615 

1616 def todict(self): 

1617 """Create a dictionary representation of the constraint""" 

1618 return { 

1619 "name": type(self).__name__, 

1620 "kwargs": { 

1621 "indices": self.indices, 

1622 "params": self.params, 

1623 "Jacobian": self.Jacobian, 

1624 "const_shift": self.const_shift, 

1625 "eps": self.eps, 

1626 "use_cell": self.use_cell, 

1627 } 

1628 } 

1629 

1630 def __repr__(self): 

1631 """The str representation of the constraint""" 

1632 if len(self.indices) > 1: 

1633 indices_str = "[{:d}, ..., {:d}]".format( 

1634 self.indices[0], self.indices[-1]) 

1635 else: 

1636 indices_str = f"[{self.indices[0]:d}]" 

1637 

1638 if len(self.params) > 1: 

1639 params_str = "[{:s}, ..., {:s}]".format( 

1640 self.params[0], self.params[-1]) 

1641 elif len(self.params) == 1: 

1642 params_str = f"[{self.params[0]:s}]" 

1643 else: 

1644 params_str = "[]" 

1645 

1646 return '{:s}({:s}, {:s}, ..., {:e})'.format( 

1647 type(self).__name__, 

1648 indices_str, 

1649 params_str, 

1650 self.eps 

1651 ) 

1652 

1653 

1654class FixScaledParametricRelations(FixParametricRelations): 

1655 

1656 def __init__( 

1657 self, 

1658 indices, 

1659 Jacobian, 

1660 const_shift, 

1661 params=None, 

1662 eps=1e-12, 

1663 ): 

1664 """The fractional coordinate version of FixParametricRelations 

1665 

1666 All arguments are the same, but since this is for fractional 

1667 coordinates use_cell is false""" 

1668 super().__init__( 

1669 indices, 

1670 Jacobian, 

1671 const_shift, 

1672 params, 

1673 eps, 

1674 False, 

1675 ) 

1676 

1677 def adjust_contravariant(self, cell, vecs, B): 

1678 """Adjust the values of a set of vectors that are contravariant 

1679 with the unit transformation""" 

1680 scaled = cell.scaled_positions(vecs).flatten() 

1681 scaled = self.Jacobian_inv @ (scaled - B) 

1682 scaled = ((self.Jacobian @ scaled) + B).reshape((-1, 3)) 

1683 

1684 return cell.cartesian_positions(scaled) 

1685 

1686 def adjust_positions(self, atoms, positions): 

1687 """Adjust positions of the atoms to match the constraints""" 

1688 positions[self.indices] = self.adjust_contravariant( 

1689 atoms.cell, 

1690 positions[self.indices], 

1691 self.const_shift, 

1692 ) 

1693 positions[self.indices] = self.adjust_B( 

1694 atoms.cell, positions[self.indices]) 

1695 

1696 def adjust_B(self, cell, positions): 

1697 """Wraps the positions back to the unit cell and adjust B to 

1698 keep track of this change""" 

1699 fractional = cell.scaled_positions(positions) 

1700 wrapped_fractional = (fractional % 1.0) % 1.0 

1701 self.const_shift += np.round(wrapped_fractional - fractional).flatten() 

1702 return cell.cartesian_positions(wrapped_fractional) 

1703 

1704 def adjust_momenta(self, atoms, momenta): 

1705 """Adjust momenta of the atoms to match the constraints""" 

1706 momenta[self.indices] = self.adjust_contravariant( 

1707 atoms.cell, 

1708 momenta[self.indices], 

1709 np.zeros(self.const_shift.shape), 

1710 ) 

1711 

1712 def adjust_forces(self, atoms, forces): 

1713 """Adjust forces of the atoms to match the constraints""" 

1714 # Forces are coavarient to the coordinate transformation, use the 

1715 # inverse transformations 

1716 cart2frac_jacob = np.zeros(2 * (3 * len(atoms),)) 

1717 for i_atom in range(len(atoms)): 

1718 cart2frac_jacob[3 * i_atom:3 * (i_atom + 1), 

1719 3 * i_atom:3 * (i_atom + 1)] = atoms.cell.T 

1720 

1721 jacobian = cart2frac_jacob @ self.Jacobian 

1722 jacobian_inv = np.linalg.inv(jacobian.T @ jacobian) @ jacobian.T 

1723 

1724 reduced_forces = jacobian.T @ forces.flatten() 

1725 forces[self.indices] = (jacobian_inv.T @ reduced_forces).reshape(-1, 3) 

1726 

1727 def todict(self): 

1728 """Create a dictionary representation of the constraint""" 

1729 dct = super().todict() 

1730 del dct["kwargs"]["use_cell"] 

1731 return dct 

1732 

1733 

1734class FixCartesianParametricRelations(FixParametricRelations): 

1735 

1736 def __init__( 

1737 self, 

1738 indices, 

1739 Jacobian, 

1740 const_shift, 

1741 params=None, 

1742 eps=1e-12, 

1743 use_cell=False, 

1744 ): 

1745 """The Cartesian coordinate version of FixParametricRelations""" 

1746 super().__init__( 

1747 indices, 

1748 Jacobian, 

1749 const_shift, 

1750 params, 

1751 eps, 

1752 use_cell, 

1753 ) 

1754 

1755 def adjust_contravariant(self, vecs, B): 

1756 """Adjust the values of a set of vectors that are contravariant with 

1757 the unit transformation""" 

1758 vecs = self.Jacobian_inv @ (vecs.flatten() - B) 

1759 vecs = ((self.Jacobian @ vecs) + B).reshape((-1, 3)) 

1760 return vecs 

1761 

1762 def adjust_positions(self, atoms, positions): 

1763 """Adjust positions of the atoms to match the constraints""" 

1764 if self.use_cell: 

1765 return 

1766 positions[self.indices] = self.adjust_contravariant( 

1767 positions[self.indices], 

1768 self.const_shift, 

1769 ) 

1770 

1771 def adjust_momenta(self, atoms, momenta): 

1772 """Adjust momenta of the atoms to match the constraints""" 

1773 if self.use_cell: 

1774 return 

1775 momenta[self.indices] = self.adjust_contravariant( 

1776 momenta[self.indices], 

1777 np.zeros(self.const_shift.shape), 

1778 ) 

1779 

1780 def adjust_forces(self, atoms, forces): 

1781 """Adjust forces of the atoms to match the constraints""" 

1782 if self.use_cell: 

1783 return 

1784 

1785 forces_reduced = self.Jacobian.T @ forces[self.indices].flatten() 

1786 forces[self.indices] = (self.Jacobian_inv.T @ 

1787 forces_reduced).reshape(-1, 3) 

1788 

1789 def adjust_cell(self, atoms, cell): 

1790 """Adjust the cell of the atoms to match the constraints""" 

1791 if not self.use_cell: 

1792 return 

1793 cell[self.indices] = self.adjust_contravariant( 

1794 cell[self.indices], 

1795 np.zeros(self.const_shift.shape), 

1796 ) 

1797 

1798 def adjust_stress(self, atoms, stress): 

1799 """Adjust the stress of the atoms to match the constraints""" 

1800 if not self.use_cell: 

1801 return 

1802 

1803 stress_3x3 = voigt_6_to_full_3x3_stress(stress) 

1804 stress_reduced = self.Jacobian.T @ stress_3x3[self.indices].flatten() 

1805 stress_3x3[self.indices] = ( 

1806 self.Jacobian_inv.T @ stress_reduced).reshape(-1, 3) 

1807 

1808 stress[:] = full_3x3_to_voigt_6_stress(stress_3x3) 

1809 

1810 

1811class Hookean(FixConstraint): 

1812 """Applies a Hookean restorative force between a pair of atoms, an atom 

1813 and a point, or an atom and a plane.""" 

1814 

1815 def __init__(self, a1, a2, k, rt=None): 

1816 """Forces two atoms to stay close together by applying no force if 

1817 they are below a threshold length, rt, and applying a Hookean 

1818 restorative force when the distance between them exceeds rt. Can 

1819 also be used to tether an atom to a fixed point in space or to a 

1820 distance above a plane. 

1821 

1822 a1 : int 

1823 Index of atom 1 

1824 a2 : one of three options 

1825 1) index of atom 2 

1826 2) a fixed point in cartesian space to which to tether a1 

1827 3) a plane given as (A, B, C, D) in A x + B y + C z + D = 0. 

1828 k : float 

1829 Hooke's law (spring) constant to apply when distance 

1830 exceeds threshold_length. Units of eV A^-2. 

1831 rt : float 

1832 The threshold length below which there is no force. The 

1833 length is 1) between two atoms, 2) between atom and point. 

1834 This argument is not supplied in case 3. Units of A. 

1835 

1836 If a plane is specified, the Hooke's law force is applied if the atom 

1837 is on the normal side of the plane. For instance, the plane with 

1838 (A, B, C, D) = (0, 0, 1, -7) defines a plane in the xy plane with a z 

1839 intercept of +7 and a normal vector pointing in the +z direction. 

1840 If the atom has z > 7, then a downward force would be applied of 

1841 k * (atom.z - 7). The same plane with the normal vector pointing in 

1842 the -z direction would be given by (A, B, C, D) = (0, 0, -1, 7). 

1843 

1844 References: 

1845 

1846 Andrew A. Peterson, Topics in Catalysis volume 57, pages40–53 (2014) 

1847 https://link.springer.com/article/10.1007%2Fs11244-013-0161-8 

1848 """ 

1849 

1850 if isinstance(a2, int): 

1851 self._type = 'two atoms' 

1852 self.indices = [a1, a2] 

1853 elif len(a2) == 3: 

1854 self._type = 'point' 

1855 self.index = a1 

1856 self.origin = np.array(a2) 

1857 elif len(a2) == 4: 

1858 self._type = 'plane' 

1859 self.index = a1 

1860 self.plane = a2 

1861 else: 

1862 raise RuntimeError('Unknown type for a2') 

1863 self.threshold = rt 

1864 self.spring = k 

1865 

1866 def get_removed_dof(self, atoms): 

1867 return 0 

1868 

1869 def todict(self): 

1870 dct = {'name': 'Hookean'} 

1871 dct['kwargs'] = {'rt': self.threshold, 

1872 'k': self.spring} 

1873 if self._type == 'two atoms': 

1874 dct['kwargs']['a1'] = self.indices[0] 

1875 dct['kwargs']['a2'] = self.indices[1] 

1876 elif self._type == 'point': 

1877 dct['kwargs']['a1'] = self.index 

1878 dct['kwargs']['a2'] = self.origin 

1879 elif self._type == 'plane': 

1880 dct['kwargs']['a1'] = self.index 

1881 dct['kwargs']['a2'] = self.plane 

1882 else: 

1883 raise NotImplementedError(f'Bad type: {self._type}') 

1884 return dct 

1885 

1886 def adjust_positions(self, atoms, newpositions): 

1887 pass 

1888 

1889 def adjust_momenta(self, atoms, momenta): 

1890 pass 

1891 

1892 def adjust_forces(self, atoms, forces): 

1893 positions = atoms.positions 

1894 if self._type == 'plane': 

1895 A, B, C, D = self.plane 

1896 x, y, z = positions[self.index] 

1897 d = ((A * x + B * y + C * z + D) / 

1898 np.sqrt(A**2 + B**2 + C**2)) 

1899 if d < 0: 

1900 return 

1901 magnitude = self.spring * d 

1902 direction = - np.array((A, B, C)) / np.linalg.norm((A, B, C)) 

1903 forces[self.index] += direction * magnitude 

1904 return 

1905 if self._type == 'two atoms': 

1906 p1, p2 = positions[self.indices] 

1907 elif self._type == 'point': 

1908 p1 = positions[self.index] 

1909 p2 = self.origin 

1910 displace, _ = find_mic(p2 - p1, atoms.cell, atoms.pbc) 

1911 bondlength = np.linalg.norm(displace) 

1912 if bondlength > self.threshold: 

1913 magnitude = self.spring * (bondlength - self.threshold) 

1914 direction = displace / np.linalg.norm(displace) 

1915 if self._type == 'two atoms': 

1916 forces[self.indices[0]] += direction * magnitude 

1917 forces[self.indices[1]] -= direction * magnitude 

1918 else: 

1919 forces[self.index] += direction * magnitude 

1920 

1921 def adjust_potential_energy(self, atoms): 

1922 """Returns the difference to the potential energy due to an active 

1923 constraint. (That is, the quantity returned is to be added to the 

1924 potential energy.)""" 

1925 positions = atoms.positions 

1926 if self._type == 'plane': 

1927 A, B, C, D = self.plane 

1928 x, y, z = positions[self.index] 

1929 d = ((A * x + B * y + C * z + D) / 

1930 np.sqrt(A**2 + B**2 + C**2)) 

1931 if d > 0: 

1932 return 0.5 * self.spring * d**2 

1933 else: 

1934 return 0. 

1935 if self._type == 'two atoms': 

1936 p1, p2 = positions[self.indices] 

1937 elif self._type == 'point': 

1938 p1 = positions[self.index] 

1939 p2 = self.origin 

1940 displace, _ = find_mic(p2 - p1, atoms.cell, atoms.pbc) 

1941 bondlength = np.linalg.norm(displace) 

1942 if bondlength > self.threshold: 

1943 return 0.5 * self.spring * (bondlength - self.threshold)**2 

1944 else: 

1945 return 0. 

1946 

1947 def get_indices(self): 

1948 if self._type == 'two atoms': 

1949 return self.indices 

1950 elif self._type == 'point': 

1951 return self.index 

1952 elif self._type == 'plane': 

1953 return self.index 

1954 

1955 def index_shuffle(self, atoms, ind): 

1956 # See docstring of superclass 

1957 if self._type == 'two atoms': 

1958 newa = [-1, -1] # Signal error 

1959 for new, old in slice2enlist(ind, len(atoms)): 

1960 for i, a in enumerate(self.indices): 

1961 if old == a: 

1962 newa[i] = new 

1963 if newa[0] == -1 or newa[1] == -1: 

1964 raise IndexError('Constraint not part of slice') 

1965 self.indices = newa 

1966 elif (self._type == 'point') or (self._type == 'plane'): 

1967 newa = -1 # Signal error 

1968 for new, old in slice2enlist(ind, len(atoms)): 

1969 if old == self.index: 

1970 newa = new 

1971 break 

1972 if newa == -1: 

1973 raise IndexError('Constraint not part of slice') 

1974 self.index = newa 

1975 

1976 def __repr__(self): 

1977 if self._type == 'two atoms': 

1978 return 'Hookean(%d, %d)' % tuple(self.indices) 

1979 elif self._type == 'point': 

1980 return 'Hookean(%d) to cartesian' % self.index 

1981 else: 

1982 return 'Hookean(%d) to plane' % self.index 

1983 

1984 

1985class ExternalForce(FixConstraint): 

1986 """Constraint object for pulling two atoms apart by an external force. 

1987 

1988 You can combine this constraint for example with FixBondLength but make 

1989 sure that *ExternalForce* comes first in the list if there are overlaps 

1990 between atom1-2 and atom3-4: 

1991 

1992 >>> from ase.build import bulk 

1993 

1994 >>> atoms = bulk('Cu', 'fcc', a=3.6) 

1995 >>> atom1, atom2, atom3, atom4 = atoms[:4] 

1996 >>> fext = 1.0 

1997 >>> con1 = ExternalForce(atom1, atom2, f_ext) 

1998 >>> con2 = FixBondLength(atom3, atom4) 

1999 >>> atoms.set_constraint([con1, con2]) 

2000 

2001 see ase/test/external_force.py""" 

2002 

2003 def __init__(self, a1, a2, f_ext): 

2004 self.indices = [a1, a2] 

2005 self.external_force = f_ext 

2006 

2007 def get_removed_dof(self, atoms): 

2008 return 0 

2009 

2010 def adjust_positions(self, atoms, new): 

2011 pass 

2012 

2013 def adjust_forces(self, atoms, forces): 

2014 dist = np.subtract.reduce(atoms.positions[self.indices]) 

2015 force = self.external_force * dist / np.linalg.norm(dist) 

2016 forces[self.indices] += (force, -force) 

2017 

2018 def adjust_potential_energy(self, atoms): 

2019 dist = np.subtract.reduce(atoms.positions[self.indices]) 

2020 return -np.linalg.norm(dist) * self.external_force 

2021 

2022 def index_shuffle(self, atoms, ind): 

2023 """Shuffle the indices of the two atoms in this constraint""" 

2024 newa = [-1, -1] # Signal error 

2025 for new, old in slice2enlist(ind, len(atoms)): 

2026 for i, a in enumerate(self.indices): 

2027 if old == a: 

2028 newa[i] = new 

2029 if newa[0] == -1 or newa[1] == -1: 

2030 raise IndexError('Constraint not part of slice') 

2031 self.indices = newa 

2032 

2033 def __repr__(self): 

2034 return 'ExternalForce(%d, %d, %f)' % (self.indices[0], 

2035 self.indices[1], 

2036 self.external_force) 

2037 

2038 def todict(self): 

2039 return {'name': 'ExternalForce', 

2040 'kwargs': {'a1': self.indices[0], 'a2': self.indices[1], 

2041 'f_ext': self.external_force}} 

2042 

2043 

2044class MirrorForce(FixConstraint): 

2045 """Constraint object for mirroring the force between two atoms. 

2046 

2047 This class is designed to find a transition state with the help of a 

2048 single optimization. It can be used if the transition state belongs to a 

2049 bond breaking reaction. First the given bond length will be fixed until 

2050 all other degrees of freedom are optimized, then the forces of the two 

2051 atoms will be mirrored to find the transition state. The mirror plane is 

2052 perpendicular to the connecting line of the atoms. Transition states in 

2053 dependence of the force can be obtained by stretching the molecule and 

2054 fixing its total length with *FixBondLength* or by using *ExternalForce* 

2055 during the optimization with *MirrorForce*. 

2056 

2057 Parameters 

2058 ---------- 

2059 a1: int 

2060 First atom index. 

2061 a2: int 

2062 Second atom index. 

2063 max_dist: float 

2064 Upper limit of the bond length interval where the transition state 

2065 can be found. 

2066 min_dist: float 

2067 Lower limit of the bond length interval where the transition state 

2068 can be found. 

2069 fmax: float 

2070 Maximum force used for the optimization. 

2071 

2072 Notes 

2073 ----- 

2074 You can combine this constraint for example with FixBondLength but make 

2075 sure that *MirrorForce* comes first in the list if there are overlaps 

2076 between atom1-2 and atom3-4: 

2077 

2078 >>> from ase.build import bulk 

2079 

2080 >>> atoms = bulk('Cu', 'fcc', a=3.6) 

2081 >>> atom1, atom2, atom3, atom4 = atoms[:4] 

2082 >>> con1 = MirrorForce(atom1, atom2) 

2083 >>> con2 = FixBondLength(atom3, atom4) 

2084 >>> atoms.set_constraint([con1, con2]) 

2085 

2086 """ 

2087 

2088 def __init__(self, a1, a2, max_dist=2.5, min_dist=1., fmax=0.1): 

2089 self.indices = [a1, a2] 

2090 self.min_dist = min_dist 

2091 self.max_dist = max_dist 

2092 self.fmax = fmax 

2093 

2094 def adjust_positions(self, atoms, new): 

2095 pass 

2096 

2097 def adjust_forces(self, atoms, forces): 

2098 dist = np.subtract.reduce(atoms.positions[self.indices]) 

2099 d = np.linalg.norm(dist) 

2100 if (d < self.min_dist) or (d > self.max_dist): 

2101 # Stop structure optimization 

2102 forces[:] *= 0 

2103 return 

2104 dist /= d 

2105 df = np.subtract.reduce(forces[self.indices]) 

2106 f = df.dot(dist) 

2107 con_saved = atoms.constraints 

2108 try: 

2109 con = [con for con in con_saved 

2110 if not isinstance(con, MirrorForce)] 

2111 atoms.set_constraint(con) 

2112 forces_copy = atoms.get_forces() 

2113 finally: 

2114 atoms.set_constraint(con_saved) 

2115 df1 = -1 / 2. * f * dist 

2116 forces_copy[self.indices] += (df1, -df1) 

2117 # Check if forces would be converged if the bond with mirrored forces 

2118 # would also be fixed 

2119 if (forces_copy**2).sum(axis=1).max() < self.fmax**2: 

2120 factor = 1. 

2121 else: 

2122 factor = 0. 

2123 df1 = -(1 + factor) / 2. * f * dist 

2124 forces[self.indices] += (df1, -df1) 

2125 

2126 def index_shuffle(self, atoms, ind): 

2127 """Shuffle the indices of the two atoms in this constraint 

2128 

2129 """ 

2130 newa = [-1, -1] # Signal error 

2131 for new, old in slice2enlist(ind, len(atoms)): 

2132 for i, a in enumerate(self.indices): 

2133 if old == a: 

2134 newa[i] = new 

2135 if newa[0] == -1 or newa[1] == -1: 

2136 raise IndexError('Constraint not part of slice') 

2137 self.indices = newa 

2138 

2139 def __repr__(self): 

2140 return 'MirrorForce(%d, %d, %f, %f, %f)' % ( 

2141 self.indices[0], self.indices[1], self.max_dist, self.min_dist, 

2142 self.fmax) 

2143 

2144 def todict(self): 

2145 return {'name': 'MirrorForce', 

2146 'kwargs': {'a1': self.indices[0], 'a2': self.indices[1], 

2147 'max_dist': self.max_dist, 

2148 'min_dist': self.min_dist, 'fmax': self.fmax}} 

2149 

2150 

2151class MirrorTorque(FixConstraint): 

2152 """Constraint object for mirroring the torque acting on a dihedral 

2153 angle defined by four atoms. 

2154 

2155 This class is designed to find a transition state with the help of a 

2156 single optimization. It can be used if the transition state belongs to a 

2157 cis-trans-isomerization with a change of dihedral angle. First the given 

2158 dihedral angle will be fixed until all other degrees of freedom are 

2159 optimized, then the torque acting on the dihedral angle will be mirrored 

2160 to find the transition state. Transition states in 

2161 dependence of the force can be obtained by stretching the molecule and 

2162 fixing its total length with *FixBondLength* or by using *ExternalForce* 

2163 during the optimization with *MirrorTorque*. 

2164 

2165 This constraint can be used to find 

2166 transition states of cis-trans-isomerization. 

2167 

2168 a1 a4 

2169 | | 

2170 a2 __ a3 

2171 

2172 Parameters 

2173 ---------- 

2174 a1: int 

2175 First atom index. 

2176 a2: int 

2177 Second atom index. 

2178 a3: int 

2179 Third atom index. 

2180 a4: int 

2181 Fourth atom index. 

2182 max_angle: float 

2183 Upper limit of the dihedral angle interval where the transition state 

2184 can be found. 

2185 min_angle: float 

2186 Lower limit of the dihedral angle interval where the transition state 

2187 can be found. 

2188 fmax: float 

2189 Maximum force used for the optimization. 

2190 

2191 Notes 

2192 ----- 

2193 You can combine this constraint for example with FixBondLength but make 

2194 sure that *MirrorTorque* comes first in the list if there are overlaps 

2195 between atom1-4 and atom5-6: 

2196 

2197 >>> from ase.build import bulk 

2198 

2199 >>> atoms = bulk('Cu', 'fcc', a=3.6) 

2200 >>> atom1, atom2, atom3, atom4, atom5, atom6 = atoms[:6] 

2201 >>> con1 = MirrorTorque(atom1, atom2, atom3, atom4) 

2202 >>> con2 = FixBondLength(atom5, atom6) 

2203 >>> atoms.set_constraint([con1, con2]) 

2204 

2205 """ 

2206 

2207 def __init__(self, a1, a2, a3, a4, max_angle=2 * np.pi, min_angle=0., 

2208 fmax=0.1): 

2209 self.indices = [a1, a2, a3, a4] 

2210 self.min_angle = min_angle 

2211 self.max_angle = max_angle 

2212 self.fmax = fmax 

2213 

2214 def adjust_positions(self, atoms, new): 

2215 pass 

2216 

2217 def adjust_forces(self, atoms, forces): 

2218 angle = atoms.get_dihedral(self.indices[0], self.indices[1], 

2219 self.indices[2], self.indices[3]) 

2220 angle *= np.pi / 180. 

2221 if (angle < self.min_angle) or (angle > self.max_angle): 

2222 # Stop structure optimization 

2223 forces[:] *= 0 

2224 return 

2225 p = atoms.positions[self.indices] 

2226 f = forces[self.indices] 

2227 

2228 f0 = (f[1] + f[2]) / 2. 

2229 ff = f - f0 

2230 p0 = (p[2] + p[1]) / 2. 

2231 m0 = np.cross(p[1] - p0, ff[1]) / (p[1] - p0).dot(p[1] - p0) 

2232 fff = ff - np.cross(m0, p - p0) 

2233 d1 = np.cross(np.cross(p[1] - p0, p[0] - p[1]), p[1] - p0) / \ 

2234 (p[1] - p0).dot(p[1] - p0) 

2235 d2 = np.cross(np.cross(p[2] - p0, p[3] - p[2]), p[2] - p0) / \ 

2236 (p[2] - p0).dot(p[2] - p0) 

2237 omegap1 = (np.cross(d1, fff[0]) / d1.dot(d1)).dot(p[1] - p0) / \ 

2238 np.linalg.norm(p[1] - p0) 

2239 omegap2 = (np.cross(d2, fff[3]) / d2.dot(d2)).dot(p[2] - p0) / \ 

2240 np.linalg.norm(p[2] - p0) 

2241 omegap = omegap1 + omegap2 

2242 con_saved = atoms.constraints 

2243 try: 

2244 con = [con for con in con_saved 

2245 if not isinstance(con, MirrorTorque)] 

2246 atoms.set_constraint(con) 

2247 forces_copy = atoms.get_forces() 

2248 finally: 

2249 atoms.set_constraint(con_saved) 

2250 df1 = -1 / 2. * omegap * np.cross(p[1] - p0, d1) / \ 

2251 np.linalg.norm(p[1] - p0) 

2252 df2 = -1 / 2. * omegap * np.cross(p[2] - p0, d2) / \ 

2253 np.linalg.norm(p[2] - p0) 

2254 forces_copy[self.indices] += (df1, [0., 0., 0.], [0., 0., 0.], df2) 

2255 # Check if forces would be converged if the dihedral angle with 

2256 # mirrored torque would also be fixed 

2257 if (forces_copy**2).sum(axis=1).max() < self.fmax**2: 

2258 factor = 1. 

2259 else: 

2260 factor = 0. 

2261 df1 = -(1 + factor) / 2. * omegap * np.cross(p[1] - p0, d1) / \ 

2262 np.linalg.norm(p[1] - p0) 

2263 df2 = -(1 + factor) / 2. * omegap * np.cross(p[2] - p0, d2) / \ 

2264 np.linalg.norm(p[2] - p0) 

2265 forces[self.indices] += (df1, [0., 0., 0.], [0., 0., 0.], df2) 

2266 

2267 def index_shuffle(self, atoms, ind): 

2268 # See docstring of superclass 

2269 indices = [] 

2270 for new, old in slice2enlist(ind, len(atoms)): 

2271 if old in self.indices: 

2272 indices.append(new) 

2273 if len(indices) == 0: 

2274 raise IndexError('All indices in MirrorTorque not part of slice') 

2275 self.indices = np.asarray(indices, int) 

2276 

2277 def __repr__(self): 

2278 return 'MirrorTorque(%d, %d, %d, %d, %f, %f, %f)' % ( 

2279 self.indices[0], self.indices[1], self.indices[2], 

2280 self.indices[3], self.max_angle, self.min_angle, self.fmax) 

2281 

2282 def todict(self): 

2283 return {'name': 'MirrorTorque', 

2284 'kwargs': {'a1': self.indices[0], 'a2': self.indices[1], 

2285 'a3': self.indices[2], 'a4': self.indices[3], 

2286 'max_angle': self.max_angle, 

2287 'min_angle': self.min_angle, 'fmax': self.fmax}} 

2288 

2289 

2290class FixSymmetry(FixConstraint): 

2291 """ 

2292 Constraint to preserve spacegroup symmetry during optimisation. 

2293 

2294 Requires spglib package to be available. 

2295 """ 

2296 

2297 def __init__(self, atoms, symprec=0.01, adjust_positions=True, 

2298 adjust_cell=True, verbose=False): 

2299 self.atoms = atoms.copy() 

2300 self.symprec = symprec 

2301 self.verbose = verbose 

2302 refine_symmetry(atoms, symprec, self.verbose) # refine initial symmetry 

2303 sym = prep_symmetry(atoms, symprec, self.verbose) 

2304 self.rotations, self.translations, self.symm_map = sym 

2305 self.do_adjust_positions = adjust_positions 

2306 self.do_adjust_cell = adjust_cell 

2307 

2308 def adjust_cell(self, atoms, cell): 

2309 if not self.do_adjust_cell: 

2310 return 

2311 # stress should definitely be symmetrized as a rank 2 tensor 

2312 # UnitCellFilter uses deformation gradient as cell DOF with steps 

2313 # dF = stress.F^-T quantity that should be symmetrized is therefore dF . 

2314 # F^T assume prev F = I, so just symmetrize dF 

2315 cur_cell = atoms.get_cell() 

2316 cur_cell_inv = atoms.cell.reciprocal().T 

2317 

2318 # F defined such that cell = cur_cell . F^T 

2319 # assume prev F = I, so dF = F - I 

2320 delta_deform_grad = np.dot(cur_cell_inv, cell).T - np.eye(3) 

2321 

2322 # symmetrization doesn't work properly with large steps, since 

2323 # it depends on current cell, and cell is being changed by deformation 

2324 # gradient 

2325 max_delta_deform_grad = np.max(np.abs(delta_deform_grad)) 

2326 if max_delta_deform_grad > 0.25: 

2327 raise RuntimeError('FixSymmetry adjust_cell does not work properly' 

2328 ' with large deformation gradient step {} > 0.25' 

2329 .format(max_delta_deform_grad)) 

2330 elif max_delta_deform_grad > 0.15: 

2331 warn('FixSymmetry adjust_cell may be ill behaved with large ' 

2332 'deformation gradient step {}'.format(max_delta_deform_grad)) 

2333 

2334 symmetrized_delta_deform_grad = symmetrize_rank2(cur_cell, cur_cell_inv, 

2335 delta_deform_grad, 

2336 self.rotations) 

2337 cell[:] = np.dot(cur_cell, 

2338 (symmetrized_delta_deform_grad + np.eye(3)).T) 

2339 

2340 def adjust_positions(self, atoms, new): 

2341 if not self.do_adjust_positions: 

2342 return 

2343 # symmetrize changes in position as rank 1 tensors 

2344 step = new - atoms.positions 

2345 symmetrized_step = symmetrize_rank1(atoms.get_cell(), 

2346 atoms.cell.reciprocal().T, step, 

2347 self.rotations, self.translations, 

2348 self.symm_map) 

2349 new[:] = atoms.positions + symmetrized_step 

2350 

2351 def adjust_forces(self, atoms, forces): 

2352 # symmetrize forces as rank 1 tensors 

2353 # print('adjusting forces') 

2354 forces[:] = symmetrize_rank1(atoms.get_cell(), 

2355 atoms.cell.reciprocal().T, forces, 

2356 self.rotations, self.translations, 

2357 self.symm_map) 

2358 

2359 def adjust_stress(self, atoms, stress): 

2360 # symmetrize stress as rank 2 tensor 

2361 raw_stress = voigt_6_to_full_3x3_stress(stress) 

2362 symmetrized_stress = symmetrize_rank2(atoms.get_cell(), 

2363 atoms.cell.reciprocal().T, 

2364 raw_stress, self.rotations) 

2365 stress[:] = full_3x3_to_voigt_6_stress(symmetrized_stress) 

2366 

2367 def index_shuffle(self, atoms, ind): 

2368 if len(atoms) != len(ind) or len(set(ind)) != len(ind): 

2369 raise RuntimeError("FixSymmetry can only accomodate atom" 

2370 " permutions, and len(Atoms) == {} " 

2371 "!= len(ind) == {} or ind has duplicates" 

2372 .format(len(atoms), len(ind))) 

2373 

2374 ind_reversed = np.zeros((len(ind)), dtype=int) 

2375 ind_reversed[ind] = range(len(ind)) 

2376 new_symm_map = [] 

2377 for sm in self.symm_map: 

2378 new_sm = np.array([-1] * len(atoms)) 

2379 for at_i in range(len(ind)): 

2380 new_sm[ind_reversed[at_i]] = ind_reversed[sm[at_i]] 

2381 new_symm_map.append(new_sm) 

2382 

2383 self.symm_map = new_symm_map 

2384 

2385 def todict(self): 

2386 return { 

2387 'name': 'FixSymmetry', 

2388 'kwargs': { 

2389 'atoms': self.atoms, 

2390 'symprec': self.symprec, 

2391 'adjust_positions': self.do_adjust_positions, 

2392 'adjust_cell': self.do_adjust_cell, 

2393 'verbose': self.verbose, 

2394 }, 

2395 } 

2396 

2397 

2398class Filter(FilterOld): 

2399 @deprecated('Import Filter from ase.filters') 

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

2401 """ 

2402 .. deprecated:: 3.23.0 

2403 Import ``Filter`` from :mod:`ase.filters` 

2404 """ 

2405 super().__init__(*args, **kwargs) 

2406 

2407 

2408class StrainFilter(StrainFilterOld): 

2409 @deprecated('Import StrainFilter from ase.filters') 

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

2411 """ 

2412 .. deprecated:: 3.23.0 

2413 Import ``StrainFilter`` from :mod:`ase.filters` 

2414 """ 

2415 super().__init__(*args, **kwargs) 

2416 

2417 

2418class UnitCellFilter(UnitCellFilterOld): 

2419 @deprecated('Import UnitCellFilter from ase.filters') 

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

2421 """ 

2422 .. deprecated:: 3.23.0 

2423 Import ``UnitCellFilter`` from :mod:`ase.filters` 

2424 """ 

2425 super().__init__(*args, **kwargs) 

2426 

2427 

2428class ExpCellFilter(ExpCellFilterOld): 

2429 @deprecated('Import ExpCellFilter from ase.filters') 

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

2431 """ 

2432 .. deprecated:: 3.23.0 

2433 Import ``ExpCellFilter`` from :mod:`ase.filters` 

2434 or use :class:`~ase.filters.FrechetCellFilter` for better 

2435 convergence w.r.t. cell variables 

2436 """ 

2437 super().__init__(*args, **kwargs)