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

1219 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3"""Constraints""" 

4from typing import Sequence 

5from warnings import warn 

6 

7import numpy as np 

8 

9from ase import Atoms 

10from ase.filters import ExpCellFilter as ExpCellFilterOld 

11from ase.filters import Filter as FilterOld 

12from ase.filters import StrainFilter as StrainFilterOld 

13from ase.filters import UnitCellFilter as UnitCellFilterOld 

14from ase.geometry import ( 

15 conditional_find_mic, 

16 find_mic, 

17 get_angles, 

18 get_angles_derivatives, 

19 get_dihedrals, 

20 get_dihedrals_derivatives, 

21 get_distances_derivatives, 

22 wrap_positions, 

23) 

24from ase.spacegroup.symmetrize import ( 

25 prep_symmetry, 

26 refine_symmetry, 

27 symmetrize_rank1, 

28 symmetrize_rank2, 

29) 

30from ase.stress import full_3x3_to_voigt_6_stress, voigt_6_to_full_3x3_stress 

31from ase.utils import deprecated 

32from ase.utils.parsemath import eval_expression 

33 

34__all__ = [ 

35 'FixCartesian', 'FixBondLength', 'FixedMode', 

36 'FixAtoms', 'FixScaled', 'FixCom', 'FixSubsetCom', 'FixedPlane', 

37 'FixConstraint', 'FixedLine', 'FixBondLengths', 'FixLinearTriatomic', 

38 'FixInternals', 'Hookean', 'ExternalForce', 'MirrorForce', 'MirrorTorque', 

39 'FixScaledParametricRelations', 'FixCartesianParametricRelations', 

40 'FixSymmetry'] 

41 

42 

43def dict2constraint(dct): 

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

45 raise ValueError 

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

47 

48 

49def slice2enlist(s, n): 

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

51 if isinstance(s, slice): 

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

53 return enumerate(s) 

54 

55 

56def constrained_indices(atoms, only_include=None): 

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

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

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

60 given constraint. 

61 """ 

62 indices = [] 

63 for constraint in atoms.constraints: 

64 if only_include is not None: 

65 if not isinstance(constraint, only_include): 

66 continue 

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

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

69 

70 

71class FixConstraint: 

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

73 

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

75 """Change the indices. 

76 

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

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

79 constraints. 

80 

81 ind -- List or tuple of indices. 

82 

83 """ 

84 raise NotImplementedError 

85 

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

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

88 of the underlying atoms object for the assignment of 

89 multiplied constraints to work. 

90 """ 

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

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

93 'remove your constraints.') 

94 raise NotImplementedError(msg) 

95 

96 def get_removed_dof(self, atoms: Atoms): 

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

98 raise NotImplementedError 

99 

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

101 """Adjust positions.""" 

102 

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

104 """Adjust momenta.""" 

105 # The default is in identical manner to forces. 

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

107 self.adjust_forces(atoms, momenta) 

108 

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

110 """Adjust forces.""" 

111 

112 def copy(self): 

113 """Copy constraint.""" 

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

115 

116 def todict(self): 

117 """Convert constraint to dictionary.""" 

118 

119 

120class IndexedConstraint(FixConstraint): 

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

122 """Constrain chosen atoms. 

123 

124 Parameters 

125 ---------- 

126 indices : sequence of int 

127 Indices for those atoms that should be constrained. 

128 mask : sequence of bool 

129 One boolean per atom indicating if the atom should be 

130 constrained or not. 

131 """ 

132 

133 if mask is not None: 

134 if indices is not None: 

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

136 indices = mask 

137 indices = np.atleast_1d(indices) 

138 if np.ndim(indices) > 1: 

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

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

141 

142 if indices.dtype == bool: 

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

144 elif len(indices) == 0: 

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

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

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

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

149 

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

151 raise ValueError( 

152 'The indices array contains duplicates. ' 

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

154 'forgot the mask= keyword.') 

155 

156 self.index = indices 

157 

158 def index_shuffle(self, atoms, ind): 

159 # See docstring of superclass 

160 index = [] 

161 

162 # Resolve negative indices: 

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

164 

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

166 if old in actual_indices: 

167 index.append(new) 

168 if len(index) == 0: 

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

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

171 # XXX make immutable 

172 

173 def get_indices(self): 

174 return self.index.copy() 

175 

176 def repeat(self, m, n): 

177 i0 = 0 

178 natoms = 0 

179 if isinstance(m, int): 

180 m = (m, m, m) 

181 index_new = [] 

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

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

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

185 i1 = i0 + n 

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

187 i0 = i1 

188 natoms += n 

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

190 # XXX make immutable 

191 return self 

192 

193 def delete_atoms(self, indices, natoms): 

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

195 

196 Required for removing atoms with existing constraint. 

197 """ 

198 

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

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

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

202 index = i[self.index] 

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

204 # XXX make immutable 

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

206 return None 

207 return self 

208 

209 

210class FixAtoms(IndexedConstraint): 

211 """Fix chosen atoms. 

212 

213 Examples 

214 -------- 

215 Fix all Copper atoms: 

216 

217 >>> from ase.build import bulk 

218 

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

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

221 >>> c = FixAtoms(mask=mask) 

222 >>> atoms.set_constraint(c) 

223 

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

225 

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

227 >>> atoms.set_constraint(c) 

228 """ 

229 

230 def get_removed_dof(self, atoms): 

231 return 3 * len(self.index) 

232 

233 def adjust_positions(self, atoms, new): 

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

235 

236 def adjust_forces(self, atoms, forces): 

237 forces[self.index] = 0.0 

238 

239 def __repr__(self): 

240 clsname = type(self).__name__ 

241 indices = ints2string(self.index) 

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

243 

244 def todict(self): 

245 return {'name': 'FixAtoms', 

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

247 

248 

249class FixCom(FixConstraint): 

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

251 

252 index = slice(None) # all atoms 

253 

254 def get_removed_dof(self, atoms): 

255 return 3 

256 

257 def adjust_positions(self, atoms, new): 

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

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

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

261 diff = old_cm - new_cm 

262 new += diff 

263 

264 def adjust_momenta(self, atoms, momenta): 

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

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

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

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

269 

270 def adjust_forces(self, atoms, forces): 

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

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

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

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

275 

276 def todict(self): 

277 return {'name': 'FixCom', 

278 'kwargs': {}} 

279 

280 

281class FixSubsetCom(FixCom, IndexedConstraint): 

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

283 

284 def __init__(self, indices): 

285 super().__init__(indices=indices) 

286 

287 def todict(self): 

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

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

290 

291 

292def ints2string(x, threshold=None): 

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

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

295 return str(x.tolist()) 

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

297 

298 

299class FixBondLengths(FixConstraint): 

300 maxiter = 500 

301 

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

303 bondlengths=None, iterations=None): 

304 """iterations: 

305 Ignored""" 

306 self.pairs = np.asarray(pairs) 

307 self.tolerance = tolerance 

308 self.bondlengths = bondlengths 

309 

310 def get_removed_dof(self, atoms): 

311 return len(self.pairs) 

312 

313 def adjust_positions(self, atoms, new): 

314 old = atoms.positions 

315 masses = atoms.get_masses() 

316 

317 if self.bondlengths is None: 

318 self.bondlengths = self.initialize_bond_lengths(atoms) 

319 

320 for i in range(self.maxiter): 

321 converged = True 

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

323 a = ab[0] 

324 b = ab[1] 

325 cd = self.bondlengths[j] 

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

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

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

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

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

331 if abs(x) > self.tolerance: 

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

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

334 converged = False 

335 if converged: 

336 break 

337 else: 

338 raise RuntimeError('Did not converge') 

339 

340 def adjust_momenta(self, atoms, p): 

341 old = atoms.positions 

342 masses = atoms.get_masses() 

343 

344 if self.bondlengths is None: 

345 self.bondlengths = self.initialize_bond_lengths(atoms) 

346 

347 for i in range(self.maxiter): 

348 converged = True 

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

350 a = ab[0] 

351 b = ab[1] 

352 cd = self.bondlengths[j] 

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

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

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

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

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

358 if abs(x) > self.tolerance: 

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

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

361 converged = False 

362 if converged: 

363 break 

364 else: 

365 raise RuntimeError('Did not converge') 

366 

367 def adjust_forces(self, atoms, forces): 

368 self.constraint_forces = -forces 

369 self.adjust_momenta(atoms, forces) 

370 self.constraint_forces += forces 

371 

372 def initialize_bond_lengths(self, atoms): 

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

374 

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

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

377 

378 return bondlengths 

379 

380 def get_indices(self): 

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

382 

383 def todict(self): 

384 return {'name': 'FixBondLengths', 

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

386 'tolerance': self.tolerance}} 

387 

388 def index_shuffle(self, atoms, ind): 

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

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

391 map[ind] = 1 

392 n = map.sum() 

393 map[:] = -1 

394 map[ind] = range(n) 

395 pairs = map[self.pairs] 

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

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

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

399 

400 

401def FixBondLength(a1, a2): 

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

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

404 

405 

406class FixLinearTriatomic(FixConstraint): 

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

408 

409 def __init__(self, triples): 

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

411 and linear vectorial constraints to the position of central 

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

413 type: 

414 

415 n--o--m 

416 

417 Parameters: 

418 

419 triples: list 

420 Indices of the atoms forming the linear molecules to constrain 

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

422 

423 When using these constraints in molecular dynamics or structure 

424 optimizations, atomic forces need to be redistributed within a 

425 triple. The function redistribute_forces_optimization implements 

426 the redistribution of forces for structure optimization, while 

427 the function redistribute_forces_md implements the redistribution 

428 for molecular dynamics. 

429 

430 References: 

431 

432 Ciccotti et al. Molecular Physics 47 (1982) 

433 :doi:`10.1080/00268978200100942` 

434 """ 

435 self.triples = np.asarray(triples) 

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

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

438 self.bondlengths = None 

439 

440 def get_removed_dof(self, atoms): 

441 return 4 * len(self.triples) 

442 

443 @property 

444 def n_ind(self): 

445 return self.triples[:, 0] 

446 

447 @property 

448 def m_ind(self): 

449 return self.triples[:, 2] 

450 

451 @property 

452 def o_ind(self): 

453 return self.triples[:, 1] 

454 

455 def initialize(self, atoms): 

456 masses = atoms.get_masses() 

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

458 

459 self.bondlengths = self.initialize_bond_lengths(atoms) 

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

461 

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

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

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

465 self.mass_n * self.mass_m) 

466 C2 = C1 / C2[:, None] 

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

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

469 C3[:, 1] *= -1 

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

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

472 C4 = C1 / C4[:, None] 

473 

474 self.C1 = C1 

475 self.C2 = C2 

476 self.C3 = C3 

477 self.C4 = C4 

478 

479 def adjust_positions(self, atoms, new): 

480 old = atoms.positions 

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

482 

483 if self.bondlengths is None: 

484 self.initialize(atoms) 

485 

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

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

488 d1 = new_n - new_m - r0 + d0 

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

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

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

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

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

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

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

496 if np.allclose(d0, r0): 

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

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

499 else: 

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

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

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

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

504 

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

506 

507 def adjust_momenta(self, atoms, p): 

508 old = atoms.positions 

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

510 

511 if self.bondlengths is None: 

512 self.initialize(atoms) 

513 

514 mass_nn = self.mass_n[:, None] 

515 mass_mm = self.mass_m[:, None] 

516 mass_oo = self.mass_o[:, None] 

517 

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

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

520 dv = p_n / mass_nn - p_m / mass_mm 

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

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

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

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

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

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

527 

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

529 

530 def adjust_forces(self, atoms, forces): 

531 

532 if self.bondlengths is None: 

533 self.initialize(atoms) 

534 

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

536 A[:, 0] *= -1 

537 A -= 1 

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

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

540 

541 self.constraint_forces = -forces 

542 old = atoms.positions 

543 

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

545 

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

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

548 df = fr_n - fr_m 

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

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

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

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

553 

554 self.constraint_forces += forces 

555 

556 def redistribute_forces_optimization(self, forces): 

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

558 optimizations. 

559 

560 The redistributed forces needs to be further adjusted using the 

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

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

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

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

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

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

567 

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

569 C4_1 * (C1_2 * forces_m - forces_o)) 

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

571 C4_2 * (C1_1 * forces_n - forces_o)) 

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

573 C4_1 * forces_n + C4_2 * forces_m) 

574 

575 return fr_n, fr_m, fr_o 

576 

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

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

579 dynamics. 

580 

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

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

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

584 Physics 47 (1982)).""" 

585 if self.bondlengths is None: 

586 self.initialize(atoms) 

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

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

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

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

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

592 mass_nn = self.mass_n[:, None] 

593 mass_mm = self.mass_m[:, None] 

594 mass_oo = self.mass_o[:, None] 

595 if rand: 

596 mr1 = (mass_mm / mass_nn) ** 0.5 

597 mr2 = (mass_oo / mass_nn) ** 0.5 

598 mr3 = (mass_nn / mass_mm) ** 0.5 

599 mr4 = (mass_oo / mass_mm) ** 0.5 

600 else: 

601 mr1 = 1.0 

602 mr2 = 1.0 

603 mr3 = 1.0 

604 mr4 = 1.0 

605 

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

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

608 mr2 * mass_mm * mass_nn * forces_o)) 

609 

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

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

612 mr4 * mass_mm * mass_nn * forces_o)) 

613 

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

615 

616 def get_slices(self, a): 

617 a_n = a[self.n_ind] 

618 a_m = a[self.m_ind] 

619 a_o = a[self.o_ind] 

620 

621 return a_n, a_m, a_o 

622 

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

624 a[self.n_ind] = a_n 

625 a[self.m_ind] = a_m 

626 a[self.o_ind] = a_o 

627 

628 def initialize_bond_lengths(self, atoms): 

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

630 

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

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

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

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

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

636 

637 return bondlengths 

638 

639 def get_indices(self): 

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

641 

642 def todict(self): 

643 return {'name': 'FixLinearTriatomic', 

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

645 

646 def index_shuffle(self, atoms, ind): 

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

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

649 map[ind] = 1 

650 n = map.sum() 

651 map[:] = -1 

652 map[ind] = range(n) 

653 triples = map[self.triples] 

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

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

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

657 

658 

659class FixedMode(FixConstraint): 

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

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

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

663 

664 def __init__(self, mode): 

665 mode = np.asarray(mode) 

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

667 

668 def get_removed_dof(self, atoms): 

669 return len(atoms) 

670 

671 def adjust_positions(self, atoms, newpositions): 

672 newpositions = newpositions.ravel() 

673 oldpositions = atoms.positions.ravel() 

674 step = newpositions - oldpositions 

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

676 

677 def adjust_forces(self, atoms, forces): 

678 forces = forces.ravel() 

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

680 

681 def index_shuffle(self, atoms, ind): 

682 eps = 1e-12 

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

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

685 excluded[ind] = False 

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

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

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

689 

690 def get_indices(self): 

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

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

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

694 # everything is being constrained. 

695 return [] 

696 

697 def todict(self): 

698 return {'name': 'FixedMode', 

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

700 

701 def __repr__(self): 

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

703 

704 

705def _normalize(direction): 

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

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

708 

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

710 return direction 

711 

712 

713class FixedPlane(IndexedConstraint): 

714 """ 

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

716 

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

718 """ 

719 

720 def __init__(self, indices, direction): 

721 """Constrain chosen atoms. 

722 

723 Parameters 

724 ---------- 

725 indices : int or list of int 

726 Index or indices for atoms that should be constrained 

727 direction : list of 3 int 

728 Direction of the normal vector 

729 

730 Examples 

731 -------- 

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

733 

734 >>> from ase.build import bulk 

735 >>> from ase.constraints import FixedPlane 

736 

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

738 >>> c = FixedPlane( 

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

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

741 ... ) 

742 >>> atoms.set_constraint(c) 

743 

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

745 

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

747 >>> atoms.set_constraint(c) 

748 """ 

749 super().__init__(indices=indices) 

750 self.dir = _normalize(direction) 

751 

752 def adjust_positions(self, atoms, newpositions): 

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

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

755 

756 def adjust_forces(self, atoms, forces): 

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

758 

759 def get_removed_dof(self, atoms): 

760 return len(self.index) 

761 

762 def todict(self): 

763 return { 

764 'name': 'FixedPlane', 

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

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

767 } 

768 

769 def __repr__(self): 

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

771 

772 

773def _projection(vectors, direction): 

774 dotprods = vectors @ direction 

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

776 return projection 

777 

778 

779class FixedLine(IndexedConstraint): 

780 """ 

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

782 

783 The line is defined by its vector *direction* 

784 """ 

785 

786 def __init__(self, indices, direction): 

787 """Constrain chosen atoms. 

788 

789 Parameters 

790 ---------- 

791 indices : int or list of int 

792 Index or indices for atoms that should be constrained 

793 direction : list of 3 int 

794 Direction of the vector defining the line 

795 

796 Examples 

797 -------- 

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

799 

800 >>> from ase.constraints import FixedLine 

801 >>> c = FixedLine( 

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

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

804 ... ) 

805 >>> atoms.set_constraint(c) 

806 

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

808 

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

810 >>> atoms.set_constraint(c) 

811 """ 

812 super().__init__(indices) 

813 self.dir = _normalize(direction) 

814 

815 def adjust_positions(self, atoms, newpositions): 

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

817 projection = _projection(step, self.dir) 

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

819 

820 def adjust_forces(self, atoms, forces): 

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

822 

823 def get_removed_dof(self, atoms): 

824 return 2 * len(self.index) 

825 

826 def __repr__(self): 

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

828 

829 def todict(self): 

830 return { 

831 'name': 'FixedLine', 

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

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

834 } 

835 

836 

837class FixCartesian(IndexedConstraint): 

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

839 

840 Parameters 

841 ---------- 

842 a : Sequence[int] 

843 Indices of atoms to be fixed. 

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

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

846 """ 

847 

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

849 super().__init__(indices=a) 

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

851 

852 def get_removed_dof(self, atoms: Atoms): 

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

854 

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

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

857 self.mask[None, :], 

858 atoms.positions[self.index], 

859 new[self.index], 

860 ) 

861 

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

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

864 

865 def todict(self): 

866 return {'name': 'FixCartesian', 

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

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

869 

870 def __repr__(self): 

871 name = type(self).__name__ 

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

873 

874 

875class FixScaled(IndexedConstraint): 

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

877 

878 Parameters 

879 ---------- 

880 a : Sequence[int] 

881 Indices of atoms to be fixed. 

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

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

884 """ 

885 

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

887 # XXX The unused cell keyword is there for compatibility 

888 # with old trajectory files. 

889 super().__init__(indices=a) 

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

891 

892 def get_removed_dof(self, atoms: Atoms): 

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

894 

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

896 cell = atoms.cell 

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

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

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

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

901 

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

903 # Forces are covariant to the coordinate transformation, 

904 # use the inverse transformations 

905 cell = atoms.cell 

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

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

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

909 

910 def todict(self): 

911 return {'name': 'FixScaled', 

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

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

914 

915 def __repr__(self): 

916 name = type(self).__name__ 

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

918 

919 

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

921# nested lists/tuples 

922class FixInternals(FixConstraint): 

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

924 

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

926 of bonds (bondcombos). 

927 

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

929 `dihedrals_deg`. 

930 Fixing planar angles is not supported at the moment. 

931 """ 

932 

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

934 angles_deg=None, dihedrals_deg=None, 

935 bondcombos=None, 

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

937 """ 

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

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

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

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

942 

943 Parameters 

944 ---------- 

945 bonds: nested python list, optional 

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

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

948 

949 angles_deg: nested python list, optional 

950 List with targetvalue and atom indices defining the fixedangles, 

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

952 

953 dihedrals_deg: nested python list, optional 

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

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

956 

957 bondcombos: nested python list, optional 

958 List with targetvalue, atom indices and linear coefficient defining 

959 the fixed linear combination of bonds, 

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

961 [index1, index2, coefficient_for_bond]]], ...] 

962 

963 mic: bool, optional, default: False 

964 Minimum image convention. 

965 

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

967 Convergence criterion. 

968 """ 

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

970 if angles: 

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

972 angles = np.asarray(angles) 

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

974 angles = angles.tolist() 

975 else: 

976 angles = angles_deg 

977 if dihedrals: 

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

979 dihedrals = np.asarray(dihedrals) 

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

981 dihedrals = dihedrals.tolist() 

982 else: 

983 dihedrals = dihedrals_deg 

984 

985 self.bonds = bonds or [] 

986 self.angles = angles or [] 

987 self.dihedrals = dihedrals or [] 

988 self.bondcombos = bondcombos or [] 

989 self.mic = mic 

990 self.epsilon = epsilon 

991 

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

993 + len(self.bondcombos)) 

994 

995 # Initialize these at run-time: 

996 self.constraints = [] 

997 self.initialized = False 

998 

999 def get_removed_dof(self, atoms): 

1000 return self.n 

1001 

1002 def initialize(self, atoms): 

1003 if self.initialized: 

1004 return 

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

1006 cell = None 

1007 pbc = None 

1008 if self.mic: 

1009 cell = atoms.cell 

1010 pbc = atoms.pbc 

1011 self.constraints = [] 

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

1013 (self.angles, self.FixAngle), 

1014 (self.dihedrals, self.FixDihedral), 

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

1016 for datum in data: 

1017 targetvalue = datum[0] 

1018 if targetvalue is None: # set to current value 

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

1020 self.mic) 

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

1022 self.constraints.append(constr) 

1023 self.initialized = True 

1024 

1025 @staticmethod 

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

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

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

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

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

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

1032 return c 

1033 

1034 def get_subconstraint(self, atoms, definition): 

1035 """Get pointer to a specific subconstraint. 

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

1037 self.initialize(atoms) 

1038 for subconstr in self.constraints: 

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

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

1041 subconstr.coefs)] 

1042 if defin == definition: 

1043 return subconstr 

1044 else: # identify primitive constraints by their indices 

1045 if subconstr.indices == [definition]: 

1046 return subconstr 

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

1048 

1049 def shuffle_definitions(self, shuffle_dic, internal_type): 

1050 dfns = [] # definitions 

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

1052 append = True 

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

1054 for old in dfn[1]: 

1055 if old in shuffle_dic: 

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

1057 else: 

1058 append = False 

1059 break 

1060 if append: 

1061 dfns.append(new_dfn) 

1062 return dfns 

1063 

1064 def shuffle_combos(self, shuffle_dic, internal_type): 

1065 dfns = [] # definitions 

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

1067 append = True 

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

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

1070 for i, indices in enumerate(all_indices): 

1071 for old in indices: 

1072 if old in shuffle_dic: 

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

1074 else: 

1075 append = False 

1076 break 

1077 if not append: 

1078 break 

1079 if append: 

1080 dfns.append(new_dfn) 

1081 return dfns 

1082 

1083 def index_shuffle(self, atoms, ind): 

1084 # See docstring of superclass 

1085 self.initialize(atoms) 

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

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

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

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

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

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

1092 self.initialized = False 

1093 self.initialize(atoms) 

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

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

1096 

1097 def get_indices(self): 

1098 cons = [] 

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

1100 cons.extend(dfn[1]) 

1101 for dfn in self.bondcombos: 

1102 for partial_dfn in dfn[1]: 

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

1104 return list(set(cons)) 

1105 

1106 def todict(self): 

1107 return {'name': 'FixInternals', 

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

1109 'angles_deg': self.angles, 

1110 'dihedrals_deg': self.dihedrals, 

1111 'bondcombos': self.bondcombos, 

1112 'mic': self.mic, 

1113 'epsilon': self.epsilon}} 

1114 

1115 def adjust_positions(self, atoms, newpos): 

1116 self.initialize(atoms) 

1117 for constraint in self.constraints: 

1118 constraint.setup_jacobian(atoms.positions) 

1119 for _ in range(50): 

1120 maxerr = 0.0 

1121 for constraint in self.constraints: 

1122 constraint.adjust_positions(atoms.positions, newpos) 

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

1124 if maxerr < self.epsilon: 

1125 return 

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

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

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

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

1130 ' Support for planar angles would require the' 

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

1132 ' See issue #868.') 

1133 raise ValueError(msg) 

1134 

1135 def adjust_forces(self, atoms, forces): 

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

1137 self.initialize(atoms) 

1138 positions = atoms.positions 

1139 N = len(forces) 

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

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

1142 

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

1144 

1145 tx[:, 0] = 1.0 

1146 ty[:, 1] = 1.0 

1147 tz[:, 2] = 1.0 

1148 ff = forces.ravel() 

1149 

1150 # Calculate the center of mass 

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

1152 

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

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

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

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

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

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

1159 

1160 # Normalizing transl., rotat. constraints 

1161 for r in list2_constraints: 

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

1163 

1164 # Add all angle, etc. constraint vectors 

1165 for constraint in self.constraints: 

1166 constraint.setup_jacobian(positions) 

1167 constraint.adjust_forces(positions, forces) 

1168 list_constraints.insert(0, constraint.jacobian) 

1169 # QR DECOMPOSITION - GRAM SCHMIDT 

1170 

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

1172 aa = np.column_stack(list_constraints) 

1173 (aa, _bb) = np.linalg.qr(aa) 

1174 # Projection 

1175 hh = [] 

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

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

1178 

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

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

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

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

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

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

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

1186 for vec in hh: 

1187 T += vec 

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

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

1190 

1191 def __repr__(self): 

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

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

1194 

1195 # Classes for internal use in FixInternals 

1196 class FixInternalsBase: 

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

1198 

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

1200 self.targetvalue = targetvalue # constant target value 

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

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

1203 self.masses = masses 

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

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

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

1207 self.cell = cell 

1208 self.pbc = pbc 

1209 

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

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

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

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

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

1215 for j in range(n): 

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

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

1218 return self.coefs @ jacobian 

1219 

1220 def finalize_positions(self, newpos): 

1221 jacobian = self.jacobian / self.masses 

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

1223 dnewpos = lamda * jacobian 

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

1225 

1226 def adjust_forces(self, positions, forces): 

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

1228 * self.jacobian) 

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

1230 

1231 class FixBondCombo(FixInternalsBase): 

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

1233 within FixInternals. 

1234 

1235 sum_i( coef_i * bond_length_i ) = constant 

1236 """ 

1237 

1238 def get_jacobian(self, pos): 

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

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

1241 pbc=self.pbc) 

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

1243 

1244 def setup_jacobian(self, pos): 

1245 self.jacobian = self.get_jacobian(pos) 

1246 

1247 def adjust_positions(self, oldpos, newpos): 

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

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

1250 cell=self.cell, 

1251 pbc=self.pbc) 

1252 value = self.coefs @ dists 

1253 self.sigma = value - self.targetvalue 

1254 self.finalize_positions(newpos) 

1255 

1256 @staticmethod 

1257 def get_value(atoms, indices, mic): 

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

1259 

1260 def __repr__(self): 

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

1262 '{self.coefs})') 

1263 

1264 class FixBondLengthAlt(FixBondCombo): 

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

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

1267 

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

1269 if targetvalue <= 0.: 

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

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

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

1273 

1274 @staticmethod 

1275 def get_value(atoms, indices, mic): 

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

1277 

1278 def __repr__(self): 

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

1280 

1281 class FixAngle(FixInternalsBase): 

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

1283 

1284 Convergence is potentially problematic for angles very close to 

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

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

1287 """ 

1288 

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

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

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

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

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

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

1295 

1296 def gather_vectors(self, pos): 

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

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

1299 return v0, v1 

1300 

1301 def get_jacobian(self, pos): 

1302 v0, v1 = self.gather_vectors(pos) 

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

1304 pbc=self.pbc) 

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

1306 

1307 def setup_jacobian(self, pos): 

1308 self.jacobian = self.get_jacobian(pos) 

1309 

1310 def adjust_positions(self, oldpos, newpos): 

1311 v0, v1 = self.gather_vectors(newpos) 

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

1313 self.sigma = value - self.targetvalue 

1314 self.finalize_positions(newpos) 

1315 

1316 @staticmethod 

1317 def get_value(atoms, indices, mic): 

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

1319 

1320 def __repr__(self): 

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

1322 

1323 class FixDihedral(FixInternalsBase): 

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

1325 

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

1327 becomes planar. Make sure to avoid this situation. 

1328 """ 

1329 

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

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

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

1333 

1334 def gather_vectors(self, pos): 

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

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

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

1338 return v0, v1, v2 

1339 

1340 def get_jacobian(self, pos): 

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

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

1343 pbc=self.pbc) 

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

1345 

1346 def setup_jacobian(self, pos): 

1347 self.jacobian = self.get_jacobian(pos) 

1348 

1349 def adjust_positions(self, oldpos, newpos): 

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

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

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

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

1354 self.finalize_positions(newpos) 

1355 

1356 @staticmethod 

1357 def get_value(atoms, indices, mic): 

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

1359 

1360 def __repr__(self): 

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

1362 

1363 

1364class FixParametricRelations(FixConstraint): 

1365 

1366 def __init__( 

1367 self, 

1368 indices, 

1369 Jacobian, 

1370 const_shift, 

1371 params=None, 

1372 eps=1e-12, 

1373 use_cell=False, 

1374 ): 

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

1376 space defined by the Jacobian 

1377 

1378 These constraints are based off the work in: 

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

1380 

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

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

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

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

1385 degrees of freedom onto the reduced parameter space. 

1386 

1387 Currently the constraint is set up to handle either atomic 

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

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

1390 to keep the mathematics behind the operations separate. 

1391 

1392 It would be possible to extend these constraints to allow 

1393 non-linear transformations if functionality to update the 

1394 Jacobian at each position update was included. This would 

1395 require passing an update function evaluate it every time 

1396 adjust_positions is callled. This is currently NOT supported, 

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

1398 

1399 Args: 

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

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

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

1403 The Jacobian describing 

1404 the parameter space transformation 

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

1406 A vector describing the constant term 

1407 in the transformation not accounted for in the Jacobian 

1408 params (list of str): 

1409 parameters used in the parametric representation 

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

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

1412 numbers and set the precision used 

1413 to generate the constraint expressions 

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

1415 

1416 """ 

1417 self.indices = np.array(indices) 

1418 self.Jacobian = np.array(Jacobian) 

1419 self.const_shift = np.array(const_shift) 

1420 

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

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

1423 

1424 self.eps = eps 

1425 self.use_cell = use_cell 

1426 

1427 if params is None: 

1428 params = [] 

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

1430 int_fmt_str = "{:0" + \ 

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

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

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

1434 else: 

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

1436 

1437 self.params = params 

1438 

1439 self.Jacobian_inv = np.linalg.inv( 

1440 self.Jacobian.T @ self.Jacobian) @ self.Jacobian.T 

1441 

1442 @classmethod 

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

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

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

1446 vector and constructs a FixParametricRelations constraint 

1447 

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

1449 elements must be ordered as: 

1450 [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], 

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

1452 component of the atomic position/lattice 

1453 vector. Currently only linear operations are allowed to be 

1454 included in the expressions so 

1455 only terms like: 

1456 - const * param_0 

1457 - sqrt[const] * param_1 

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

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

1460 the parameters passed in 

1461 params, are allowed. 

1462 

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

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

1465 expressions = [ 

1466 "1.0/3.0", "2.0/3.0", "z1", 

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

1468 "1.0/3.0", "2.0/3.0", "z2", 

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

1470 ] 

1471 

1472 For diamond are: 

1473 params = [] 

1474 expressions = [ 

1475 "0.0", "0.0", "0.0", 

1476 "0.25", "0.25", "0.25", 

1477 ], 

1478 

1479 and for stannite are 

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

1481 expressions = [ 

1482 "0.0", "0.0", "0.0", 

1483 "0.0", "0.5", "0.5", 

1484 "0.75", "0.25", "0.5", 

1485 "0.25", "0.75", "0.5", 

1486 "x4 + z4", "x4 + z4", "2*x4", 

1487 "x4 - z4", "x4 - z4", "-2*x4", 

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

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

1490 ] 

1491 

1492 Args: 

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

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

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

1496 parametric representation 

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

1498 parametric to the real space representation 

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

1500 numbers and set the precision used 

1501 to generate the constraint expressions 

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

1503 

1504 Returns: 

1505 cls( 

1506 indices, 

1507 Jacobian generated from expressions, 

1508 const_shift generated from expressions, 

1509 params, 

1510 eps-12, 

1511 use_cell, 

1512 ) 

1513 """ 

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

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

1516 

1517 for expr_ind, expression in enumerate(expressions): 

1518 expression = expression.strip() 

1519 

1520 # Convert subtraction to addition 

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

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

1523 expression = expression[1:] 

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

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

1526 

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

1528 # param_11 does not become 0.01 

1529 int_fmt_str = "{:0" + \ 

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

1531 

1532 param_dct = {} 

1533 param_map = {} 

1534 

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

1536 for param_ind, param in enumerate(params): 

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

1538 param_map[param] = param_str 

1539 param_dct[param_str] = 0.0 

1540 

1541 # Replace the parameters according to the map 

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

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

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

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

1546 

1547 # Partial linearity check 

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

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

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

1551 if n_params_in_sec > 1: 

1552 raise ValueError( 

1553 "FixParametricRelations expressions must be linear.") 

1554 

1555 const_shift[expr_ind] = float( 

1556 eval_expression(expression, param_dct)) 

1557 

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

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

1560 if param_str not in expression: 

1561 Jacobian[expr_ind, param_ind] = 0.0 

1562 continue 

1563 param_dct[param_str] = 1.0 

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

1565 test_1 -= const_shift[expr_ind] 

1566 Jacobian[expr_ind, param_ind] = test_1 

1567 

1568 param_dct[param_str] = 2.0 

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

1570 test_2 -= const_shift[expr_ind] 

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

1572 raise ValueError( 

1573 "FixParametricRelations expressions must be linear.") 

1574 param_dct[param_str] = 0.0 

1575 

1576 args = [ 

1577 indices, 

1578 Jacobian, 

1579 const_shift, 

1580 params, 

1581 eps, 

1582 use_cell, 

1583 ] 

1584 if cls is FixScaledParametricRelations: 

1585 args = args[:-1] 

1586 return cls(*args) 

1587 

1588 @property 

1589 def expressions(self): 

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

1591 and self.const_shift objects""" 

1592 expressions = [] 

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

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

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

1596 exp = "" 

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

1598 shift_val) > self.eps: 

1599 exp += fmt_str.format(shift_val) 

1600 

1601 param_exp = "" 

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

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

1604 if abs_jacob_val < self.eps: 

1605 continue 

1606 

1607 param = self.params[param_index] 

1608 if param_exp or exp: 

1609 if jacob_val > -1.0 * self.eps: 

1610 param_exp += " + " 

1611 else: 

1612 param_exp += " - " 

1613 elif (not exp) and (not param_exp) and ( 

1614 jacob_val < -1.0 * self.eps): 

1615 param_exp += "-" 

1616 

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

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

1619 else: 

1620 param_exp += (fmt_str + 

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

1622 

1623 exp += param_exp 

1624 

1625 expressions.append(exp) 

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

1627 

1628 def todict(self): 

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

1630 return { 

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

1632 "kwargs": { 

1633 "indices": self.indices, 

1634 "params": self.params, 

1635 "Jacobian": self.Jacobian, 

1636 "const_shift": self.const_shift, 

1637 "eps": self.eps, 

1638 "use_cell": self.use_cell, 

1639 } 

1640 } 

1641 

1642 def __repr__(self): 

1643 """The str representation of the constraint""" 

1644 if len(self.indices) > 1: 

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

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

1647 else: 

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

1649 

1650 if len(self.params) > 1: 

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

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

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

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

1655 else: 

1656 params_str = "[]" 

1657 

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

1659 type(self).__name__, 

1660 indices_str, 

1661 params_str, 

1662 self.eps 

1663 ) 

1664 

1665 

1666class FixScaledParametricRelations(FixParametricRelations): 

1667 

1668 def __init__( 

1669 self, 

1670 indices, 

1671 Jacobian, 

1672 const_shift, 

1673 params=None, 

1674 eps=1e-12, 

1675 ): 

1676 """The fractional coordinate version of FixParametricRelations 

1677 

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

1679 coordinates use_cell is false""" 

1680 super().__init__( 

1681 indices, 

1682 Jacobian, 

1683 const_shift, 

1684 params, 

1685 eps, 

1686 False, 

1687 ) 

1688 

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

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

1691 with the unit transformation""" 

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

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

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

1695 

1696 return cell.cartesian_positions(scaled) 

1697 

1698 def adjust_positions(self, atoms, positions): 

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

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

1701 atoms.cell, 

1702 positions[self.indices], 

1703 self.const_shift, 

1704 ) 

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

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

1707 

1708 def adjust_B(self, cell, positions): 

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

1710 keep track of this change""" 

1711 fractional = cell.scaled_positions(positions) 

1712 wrapped_fractional = (fractional % 1.0) % 1.0 

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

1714 return cell.cartesian_positions(wrapped_fractional) 

1715 

1716 def adjust_momenta(self, atoms, momenta): 

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

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

1719 atoms.cell, 

1720 momenta[self.indices], 

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

1722 ) 

1723 

1724 def adjust_forces(self, atoms, forces): 

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

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

1727 # inverse transformations 

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

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

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

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

1732 

1733 jacobian = cart2frac_jacob @ self.Jacobian 

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

1735 

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

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

1738 

1739 def todict(self): 

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

1741 dct = super().todict() 

1742 del dct["kwargs"]["use_cell"] 

1743 return dct 

1744 

1745 

1746class FixCartesianParametricRelations(FixParametricRelations): 

1747 

1748 def __init__( 

1749 self, 

1750 indices, 

1751 Jacobian, 

1752 const_shift, 

1753 params=None, 

1754 eps=1e-12, 

1755 use_cell=False, 

1756 ): 

1757 """The Cartesian coordinate version of FixParametricRelations""" 

1758 super().__init__( 

1759 indices, 

1760 Jacobian, 

1761 const_shift, 

1762 params, 

1763 eps, 

1764 use_cell, 

1765 ) 

1766 

1767 def adjust_contravariant(self, vecs, B): 

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

1769 the unit transformation""" 

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

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

1772 return vecs 

1773 

1774 def adjust_positions(self, atoms, positions): 

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

1776 if self.use_cell: 

1777 return 

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

1779 positions[self.indices], 

1780 self.const_shift, 

1781 ) 

1782 

1783 def adjust_momenta(self, atoms, momenta): 

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

1785 if self.use_cell: 

1786 return 

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

1788 momenta[self.indices], 

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

1790 ) 

1791 

1792 def adjust_forces(self, atoms, forces): 

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

1794 if self.use_cell: 

1795 return 

1796 

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

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

1799 forces_reduced).reshape(-1, 3) 

1800 

1801 def adjust_cell(self, atoms, cell): 

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

1803 if not self.use_cell: 

1804 return 

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

1806 cell[self.indices], 

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

1808 ) 

1809 

1810 def adjust_stress(self, atoms, stress): 

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

1812 if not self.use_cell: 

1813 return 

1814 

1815 stress_3x3 = voigt_6_to_full_3x3_stress(stress) 

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

1817 stress_3x3[self.indices] = ( 

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

1819 

1820 stress[:] = full_3x3_to_voigt_6_stress(stress_3x3) 

1821 

1822 

1823class Hookean(FixConstraint): 

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

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

1826 

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

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

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

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

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

1832 distance above a plane. 

1833 

1834 a1 : int 

1835 Index of atom 1 

1836 a2 : one of three options 

1837 1) index of atom 2 

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

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

1840 k : float 

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

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

1843 rt : float 

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

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

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

1847 

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

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

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

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

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

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

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

1855 

1856 References: 

1857 

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

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

1860 """ 

1861 

1862 if isinstance(a2, int): 

1863 self._type = 'two atoms' 

1864 self.indices = [a1, a2] 

1865 elif len(a2) == 3: 

1866 self._type = 'point' 

1867 self.index = a1 

1868 self.origin = np.array(a2) 

1869 elif len(a2) == 4: 

1870 self._type = 'plane' 

1871 self.index = a1 

1872 self.plane = a2 

1873 else: 

1874 raise RuntimeError('Unknown type for a2') 

1875 self.threshold = rt 

1876 self.spring = k 

1877 

1878 def get_removed_dof(self, atoms): 

1879 return 0 

1880 

1881 def todict(self): 

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

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

1884 'k': self.spring} 

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

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

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

1888 elif self._type == 'point': 

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

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

1891 elif self._type == 'plane': 

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

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

1894 else: 

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

1896 return dct 

1897 

1898 def adjust_positions(self, atoms, newpositions): 

1899 pass 

1900 

1901 def adjust_momenta(self, atoms, momenta): 

1902 pass 

1903 

1904 def adjust_forces(self, atoms, forces): 

1905 positions = atoms.positions 

1906 if self._type == 'plane': 

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

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

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

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

1911 if d < 0: 

1912 return 

1913 magnitude = self.spring * d 

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

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

1916 return 

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

1918 p1, p2 = positions[self.indices] 

1919 elif self._type == 'point': 

1920 p1 = positions[self.index] 

1921 p2 = self.origin 

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

1923 bondlength = np.linalg.norm(displace) 

1924 if bondlength > self.threshold: 

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

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

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

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

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

1930 else: 

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

1932 

1933 def adjust_potential_energy(self, atoms): 

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

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

1936 potential energy.)""" 

1937 positions = atoms.positions 

1938 if self._type == 'plane': 

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

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

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

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

1943 if d > 0: 

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

1945 else: 

1946 return 0. 

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

1948 p1, p2 = positions[self.indices] 

1949 elif self._type == 'point': 

1950 p1 = positions[self.index] 

1951 p2 = self.origin 

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

1953 bondlength = np.linalg.norm(displace) 

1954 if bondlength > self.threshold: 

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

1956 else: 

1957 return 0. 

1958 

1959 def get_indices(self): 

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

1961 return self.indices 

1962 elif self._type == 'point': 

1963 return self.index 

1964 elif self._type == 'plane': 

1965 return self.index 

1966 

1967 def index_shuffle(self, atoms, ind): 

1968 # See docstring of superclass 

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

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

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

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

1973 if old == a: 

1974 newa[i] = new 

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

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

1977 self.indices = newa 

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

1979 newa = -1 # Signal error 

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

1981 if old == self.index: 

1982 newa = new 

1983 break 

1984 if newa == -1: 

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

1986 self.index = newa 

1987 

1988 def __repr__(self): 

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

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

1991 elif self._type == 'point': 

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

1993 else: 

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

1995 

1996 

1997class ExternalForce(FixConstraint): 

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

1999 

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

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

2002 between atom1-2 and atom3-4: 

2003 

2004 >>> from ase.build import bulk 

2005 

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

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

2008 >>> fext = 1.0 

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

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

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

2012 

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

2014 

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

2016 self.indices = [a1, a2] 

2017 self.external_force = f_ext 

2018 

2019 def get_removed_dof(self, atoms): 

2020 return 0 

2021 

2022 def adjust_positions(self, atoms, new): 

2023 pass 

2024 

2025 def adjust_forces(self, atoms, forces): 

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

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

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

2029 

2030 def adjust_potential_energy(self, atoms): 

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

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

2033 

2034 def index_shuffle(self, atoms, ind): 

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

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

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

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

2039 if old == a: 

2040 newa[i] = new 

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

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

2043 self.indices = newa 

2044 

2045 def __repr__(self): 

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

2047 self.indices[1], 

2048 self.external_force) 

2049 

2050 def todict(self): 

2051 return {'name': 'ExternalForce', 

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

2053 'f_ext': self.external_force}} 

2054 

2055 

2056class MirrorForce(FixConstraint): 

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

2058 

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

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

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

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

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

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

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

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

2067 during the optimization with *MirrorForce*. 

2068 

2069 Parameters 

2070 ---------- 

2071 a1: int 

2072 First atom index. 

2073 a2: int 

2074 Second atom index. 

2075 max_dist: float 

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

2077 can be found. 

2078 min_dist: float 

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

2080 can be found. 

2081 fmax: float 

2082 Maximum force used for the optimization. 

2083 

2084 Notes 

2085 ----- 

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

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

2088 between atom1-2 and atom3-4: 

2089 

2090 >>> from ase.build import bulk 

2091 

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

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

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

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

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

2097 

2098 """ 

2099 

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

2101 self.indices = [a1, a2] 

2102 self.min_dist = min_dist 

2103 self.max_dist = max_dist 

2104 self.fmax = fmax 

2105 

2106 def adjust_positions(self, atoms, new): 

2107 pass 

2108 

2109 def adjust_forces(self, atoms, forces): 

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

2111 d = np.linalg.norm(dist) 

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

2113 # Stop structure optimization 

2114 forces[:] *= 0 

2115 return 

2116 dist /= d 

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

2118 f = df.dot(dist) 

2119 con_saved = atoms.constraints 

2120 try: 

2121 con = [con for con in con_saved 

2122 if not isinstance(con, MirrorForce)] 

2123 atoms.set_constraint(con) 

2124 forces_copy = atoms.get_forces() 

2125 finally: 

2126 atoms.set_constraint(con_saved) 

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

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

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

2130 # would also be fixed 

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

2132 factor = 1. 

2133 else: 

2134 factor = 0. 

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

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

2137 

2138 def index_shuffle(self, atoms, ind): 

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

2140 

2141 """ 

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

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

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

2145 if old == a: 

2146 newa[i] = new 

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

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

2149 self.indices = newa 

2150 

2151 def __repr__(self): 

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

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

2154 self.fmax) 

2155 

2156 def todict(self): 

2157 return {'name': 'MirrorForce', 

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

2159 'max_dist': self.max_dist, 

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

2161 

2162 

2163class MirrorTorque(FixConstraint): 

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

2165 angle defined by four atoms. 

2166 

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

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

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

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

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

2172 to find the transition state. Transition states in 

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

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

2175 during the optimization with *MirrorTorque*. 

2176 

2177 This constraint can be used to find 

2178 transition states of cis-trans-isomerization. 

2179 

2180 a1 a4 

2181 | | 

2182 a2 __ a3 

2183 

2184 Parameters 

2185 ---------- 

2186 a1: int 

2187 First atom index. 

2188 a2: int 

2189 Second atom index. 

2190 a3: int 

2191 Third atom index. 

2192 a4: int 

2193 Fourth atom index. 

2194 max_angle: float 

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

2196 can be found. 

2197 min_angle: float 

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

2199 can be found. 

2200 fmax: float 

2201 Maximum force used for the optimization. 

2202 

2203 Notes 

2204 ----- 

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

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

2207 between atom1-4 and atom5-6: 

2208 

2209 >>> from ase.build import bulk 

2210 

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

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

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

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

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

2216 

2217 """ 

2218 

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

2220 fmax=0.1): 

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

2222 self.min_angle = min_angle 

2223 self.max_angle = max_angle 

2224 self.fmax = fmax 

2225 

2226 def adjust_positions(self, atoms, new): 

2227 pass 

2228 

2229 def adjust_forces(self, atoms, forces): 

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

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

2232 angle *= np.pi / 180. 

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

2234 # Stop structure optimization 

2235 forces[:] *= 0 

2236 return 

2237 p = atoms.positions[self.indices] 

2238 f = forces[self.indices] 

2239 

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

2241 ff = f - f0 

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

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

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

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

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

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

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

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

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

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

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

2253 omegap = omegap1 + omegap2 

2254 con_saved = atoms.constraints 

2255 try: 

2256 con = [con for con in con_saved 

2257 if not isinstance(con, MirrorTorque)] 

2258 atoms.set_constraint(con) 

2259 forces_copy = atoms.get_forces() 

2260 finally: 

2261 atoms.set_constraint(con_saved) 

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

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

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

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

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

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

2268 # mirrored torque would also be fixed 

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

2270 factor = 1. 

2271 else: 

2272 factor = 0. 

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

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

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

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

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

2278 

2279 def index_shuffle(self, atoms, ind): 

2280 # See docstring of superclass 

2281 indices = [] 

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

2283 if old in self.indices: 

2284 indices.append(new) 

2285 if len(indices) == 0: 

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

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

2288 

2289 def __repr__(self): 

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

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

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

2293 

2294 def todict(self): 

2295 return {'name': 'MirrorTorque', 

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

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

2298 'max_angle': self.max_angle, 

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

2300 

2301 

2302class FixSymmetry(FixConstraint): 

2303 """ 

2304 Constraint to preserve spacegroup symmetry during optimisation. 

2305 

2306 Requires spglib package to be available. 

2307 """ 

2308 

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

2310 adjust_cell=True, verbose=False): 

2311 self.atoms = atoms.copy() 

2312 self.symprec = symprec 

2313 self.verbose = verbose 

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

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

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

2317 self.do_adjust_positions = adjust_positions 

2318 self.do_adjust_cell = adjust_cell 

2319 

2320 def adjust_cell(self, atoms, cell): 

2321 if not self.do_adjust_cell: 

2322 return 

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

2324 # UnitCellFilter uses deformation gradient as cell DOF with steps 

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

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

2327 cur_cell = atoms.get_cell() 

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

2329 

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

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

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

2333 

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

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

2336 # gradient 

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

2338 if max_delta_deform_grad > 0.25: 

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

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

2341 .format(max_delta_deform_grad)) 

2342 elif max_delta_deform_grad > 0.15: 

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

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

2345 

2346 symmetrized_delta_deform_grad = symmetrize_rank2(cur_cell, cur_cell_inv, 

2347 delta_deform_grad, 

2348 self.rotations) 

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

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

2351 

2352 def adjust_positions(self, atoms, new): 

2353 if not self.do_adjust_positions: 

2354 return 

2355 # symmetrize changes in position as rank 1 tensors 

2356 step = new - atoms.positions 

2357 symmetrized_step = symmetrize_rank1(atoms.get_cell(), 

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

2359 self.rotations, self.translations, 

2360 self.symm_map) 

2361 new[:] = atoms.positions + symmetrized_step 

2362 

2363 def adjust_forces(self, atoms, forces): 

2364 # symmetrize forces as rank 1 tensors 

2365 # print('adjusting forces') 

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

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

2368 self.rotations, self.translations, 

2369 self.symm_map) 

2370 

2371 def adjust_stress(self, atoms, stress): 

2372 # symmetrize stress as rank 2 tensor 

2373 raw_stress = voigt_6_to_full_3x3_stress(stress) 

2374 symmetrized_stress = symmetrize_rank2(atoms.get_cell(), 

2375 atoms.cell.reciprocal().T, 

2376 raw_stress, self.rotations) 

2377 stress[:] = full_3x3_to_voigt_6_stress(symmetrized_stress) 

2378 

2379 def index_shuffle(self, atoms, ind): 

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

2381 raise RuntimeError("FixSymmetry can only accomodate atom" 

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

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

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

2385 

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

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

2388 new_symm_map = [] 

2389 for sm in self.symm_map: 

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

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

2392 new_sm[ind_reversed[at_i]] = ind_reversed[sm[at_i]] 

2393 new_symm_map.append(new_sm) 

2394 

2395 self.symm_map = new_symm_map 

2396 

2397 def todict(self): 

2398 return { 

2399 'name': 'FixSymmetry', 

2400 'kwargs': { 

2401 'atoms': self.atoms, 

2402 'symprec': self.symprec, 

2403 'adjust_positions': self.do_adjust_positions, 

2404 'adjust_cell': self.do_adjust_cell, 

2405 'verbose': self.verbose, 

2406 }, 

2407 } 

2408 

2409 

2410class Filter(FilterOld): 

2411 @deprecated('Import Filter from ase.filters') 

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

2413 """ 

2414 .. deprecated:: 3.23.0 

2415 Import ``Filter`` from :mod:`ase.filters` 

2416 """ 

2417 super().__init__(*args, **kwargs) 

2418 

2419 

2420class StrainFilter(StrainFilterOld): 

2421 @deprecated('Import StrainFilter from ase.filters') 

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

2423 """ 

2424 .. deprecated:: 3.23.0 

2425 Import ``StrainFilter`` from :mod:`ase.filters` 

2426 """ 

2427 super().__init__(*args, **kwargs) 

2428 

2429 

2430class UnitCellFilter(UnitCellFilterOld): 

2431 @deprecated('Import UnitCellFilter from ase.filters') 

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

2433 """ 

2434 .. deprecated:: 3.23.0 

2435 Import ``UnitCellFilter`` from :mod:`ase.filters` 

2436 """ 

2437 super().__init__(*args, **kwargs) 

2438 

2439 

2440class ExpCellFilter(ExpCellFilterOld): 

2441 @deprecated('Import ExpCellFilter from ase.filters') 

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

2443 """ 

2444 .. deprecated:: 3.23.0 

2445 Import ``ExpCellFilter`` from :mod:`ase.filters` 

2446 or use :class:`~ase.filters.FrechetCellFilter` for better 

2447 convergence w.r.t. cell variables 

2448 """ 

2449 super().__init__(*args, **kwargs)