Coverage for /builds/debichem-team/python-ase/ase/constraints.py: 87.12%

1219 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +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 ( 

13 conditional_find_mic, 

14 find_mic, 

15 get_angles, 

16 get_angles_derivatives, 

17 get_dihedrals, 

18 get_dihedrals_derivatives, 

19 get_distances_derivatives, 

20 wrap_positions, 

21) 

22from ase.spacegroup.symmetrize import ( 

23 prep_symmetry, 

24 refine_symmetry, 

25 symmetrize_rank1, 

26 symmetrize_rank2, 

27) 

28from ase.stress import full_3x3_to_voigt_6_stress, voigt_6_to_full_3x3_stress 

29from ase.utils import deprecated 

30from ase.utils.parsemath import eval_expression 

31 

32__all__ = [ 

33 'FixCartesian', 'FixBondLength', 'FixedMode', 

34 'FixAtoms', 'FixScaled', 'FixCom', 'FixSubsetCom', 'FixedPlane', 

35 'FixConstraint', 'FixedLine', 'FixBondLengths', 'FixLinearTriatomic', 

36 'FixInternals', 'Hookean', 'ExternalForce', 'MirrorForce', 'MirrorTorque', 

37 'FixScaledParametricRelations', 'FixCartesianParametricRelations', 

38 'FixSymmetry'] 

39 

40 

41def dict2constraint(dct): 

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

43 raise ValueError 

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

45 

46 

47def slice2enlist(s, n): 

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

49 if isinstance(s, slice): 

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

51 return enumerate(s) 

52 

53 

54def constrained_indices(atoms, only_include=None): 

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

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

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

58 given constraint. 

59 """ 

60 indices = [] 

61 for constraint in atoms.constraints: 

62 if only_include is not None: 

63 if not isinstance(constraint, only_include): 

64 continue 

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

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

67 

68 

69class FixConstraint: 

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

71 

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

73 """Change the indices. 

74 

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

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

77 constraints. 

78 

79 ind -- List or tuple of indices. 

80 

81 """ 

82 raise NotImplementedError 

83 

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

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

86 of the underlying atoms object for the assignment of 

87 multiplied constraints to work. 

88 """ 

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

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

91 'remove your constraints.') 

92 raise NotImplementedError(msg) 

93 

94 def get_removed_dof(self, atoms: Atoms): 

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

96 raise NotImplementedError 

97 

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

99 """Adjust positions.""" 

100 

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

102 """Adjust momenta.""" 

103 # The default is in identical manner to forces. 

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

105 self.adjust_forces(atoms, momenta) 

106 

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

108 """Adjust forces.""" 

109 

110 def copy(self): 

111 """Copy constraint.""" 

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

113 

114 def todict(self): 

115 """Convert constraint to dictionary.""" 

116 

117 

118class IndexedConstraint(FixConstraint): 

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

120 """Constrain chosen atoms. 

121 

122 Parameters 

123 ---------- 

124 indices : sequence of int 

125 Indices for those atoms that should be constrained. 

126 mask : sequence of bool 

127 One boolean per atom indicating if the atom should be 

128 constrained or not. 

129 """ 

130 

131 if mask is not None: 

132 if indices is not None: 

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

134 indices = mask 

135 indices = np.atleast_1d(indices) 

136 if np.ndim(indices) > 1: 

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

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

139 

140 if indices.dtype == bool: 

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

142 elif len(indices) == 0: 

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

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

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

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

147 

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

149 raise ValueError( 

150 'The indices array contains duplicates. ' 

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

152 'forgot the mask= keyword.') 

153 

154 self.index = indices 

155 

156 def index_shuffle(self, atoms, ind): 

157 # See docstring of superclass 

158 index = [] 

159 

160 # Resolve negative indices: 

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

162 

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

164 if old in actual_indices: 

165 index.append(new) 

166 if len(index) == 0: 

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

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

169 # XXX make immutable 

170 

171 def get_indices(self): 

172 return self.index.copy() 

173 

174 def repeat(self, m, n): 

175 i0 = 0 

176 natoms = 0 

177 if isinstance(m, int): 

178 m = (m, m, m) 

179 index_new = [] 

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

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

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

183 i1 = i0 + n 

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

185 i0 = i1 

186 natoms += n 

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

188 # XXX make immutable 

189 return self 

190 

191 def delete_atoms(self, indices, natoms): 

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

193 

194 Required for removing atoms with existing constraint. 

195 """ 

196 

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

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

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

200 index = i[self.index] 

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

202 # XXX make immutable 

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

204 return None 

205 return self 

206 

207 

208class FixAtoms(IndexedConstraint): 

209 """Fix chosen atoms. 

210 

211 Examples 

212 -------- 

213 Fix all Copper atoms: 

214 

215 >>> from ase.build import bulk 

216 

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

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

219 >>> c = FixAtoms(mask=mask) 

220 >>> atoms.set_constraint(c) 

221 

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

223 

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

225 >>> atoms.set_constraint(c) 

226 """ 

227 

228 def get_removed_dof(self, atoms): 

229 return 3 * len(self.index) 

230 

231 def adjust_positions(self, atoms, new): 

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

233 

234 def adjust_forces(self, atoms, forces): 

235 forces[self.index] = 0.0 

236 

237 def __repr__(self): 

238 clsname = type(self).__name__ 

239 indices = ints2string(self.index) 

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

241 

242 def todict(self): 

243 return {'name': 'FixAtoms', 

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

245 

246 

247class FixCom(FixConstraint): 

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

249 

250 index = slice(None) # all atoms 

251 

252 def get_removed_dof(self, atoms): 

253 return 3 

254 

255 def adjust_positions(self, atoms, new): 

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

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

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

259 diff = old_cm - new_cm 

260 new += diff 

261 

262 def adjust_momenta(self, atoms, momenta): 

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

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

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

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

267 

268 def adjust_forces(self, atoms, forces): 

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

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

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

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

273 

274 def todict(self): 

275 return {'name': 'FixCom', 

276 'kwargs': {}} 

277 

278 

279class FixSubsetCom(FixCom, IndexedConstraint): 

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

281 

282 def __init__(self, indices): 

283 super().__init__(indices=indices) 

284 

285 def todict(self): 

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

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

288 

289 

290def ints2string(x, threshold=None): 

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

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

293 return str(x.tolist()) 

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

295 

296 

297class FixBondLengths(FixConstraint): 

298 maxiter = 500 

299 

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

301 bondlengths=None, iterations=None): 

302 """iterations: 

303 Ignored""" 

304 self.pairs = np.asarray(pairs) 

305 self.tolerance = tolerance 

306 self.bondlengths = bondlengths 

307 

308 def get_removed_dof(self, atoms): 

309 return len(self.pairs) 

310 

311 def adjust_positions(self, atoms, new): 

312 old = atoms.positions 

313 masses = atoms.get_masses() 

314 

315 if self.bondlengths is None: 

316 self.bondlengths = self.initialize_bond_lengths(atoms) 

317 

318 for i in range(self.maxiter): 

319 converged = True 

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

321 a = ab[0] 

322 b = ab[1] 

323 cd = self.bondlengths[j] 

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

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

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

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

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

329 if abs(x) > self.tolerance: 

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

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

332 converged = False 

333 if converged: 

334 break 

335 else: 

336 raise RuntimeError('Did not converge') 

337 

338 def adjust_momenta(self, atoms, p): 

339 old = atoms.positions 

340 masses = atoms.get_masses() 

341 

342 if self.bondlengths is None: 

343 self.bondlengths = self.initialize_bond_lengths(atoms) 

344 

345 for i in range(self.maxiter): 

346 converged = True 

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

348 a = ab[0] 

349 b = ab[1] 

350 cd = self.bondlengths[j] 

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

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

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

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

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

356 if abs(x) > self.tolerance: 

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

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

359 converged = False 

360 if converged: 

361 break 

362 else: 

363 raise RuntimeError('Did not converge') 

364 

365 def adjust_forces(self, atoms, forces): 

366 self.constraint_forces = -forces 

367 self.adjust_momenta(atoms, forces) 

368 self.constraint_forces += forces 

369 

370 def initialize_bond_lengths(self, atoms): 

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

372 

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

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

375 

376 return bondlengths 

377 

378 def get_indices(self): 

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

380 

381 def todict(self): 

382 return {'name': 'FixBondLengths', 

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

384 'tolerance': self.tolerance}} 

385 

386 def index_shuffle(self, atoms, ind): 

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

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

389 map[ind] = 1 

390 n = map.sum() 

391 map[:] = -1 

392 map[ind] = range(n) 

393 pairs = map[self.pairs] 

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

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

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

397 

398 

399def FixBondLength(a1, a2): 

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

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

402 

403 

404class FixLinearTriatomic(FixConstraint): 

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

406 

407 def __init__(self, triples): 

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

409 and linear vectorial constraints to the position of central 

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

411 type: 

412 

413 n--o--m 

414 

415 Parameters: 

416 

417 triples: list 

418 Indices of the atoms forming the linear molecules to constrain 

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

420 

421 When using these constraints in molecular dynamics or structure 

422 optimizations, atomic forces need to be redistributed within a 

423 triple. The function redistribute_forces_optimization implements 

424 the redistribution of forces for structure optimization, while 

425 the function redistribute_forces_md implements the redistribution 

426 for molecular dynamics. 

427 

428 References: 

429 

430 Ciccotti et al. Molecular Physics 47 (1982) 

431 :doi:`10.1080/00268978200100942` 

432 """ 

433 self.triples = np.asarray(triples) 

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

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

436 self.bondlengths = None 

437 

438 def get_removed_dof(self, atoms): 

439 return 4 * len(self.triples) 

440 

441 @property 

442 def n_ind(self): 

443 return self.triples[:, 0] 

444 

445 @property 

446 def m_ind(self): 

447 return self.triples[:, 2] 

448 

449 @property 

450 def o_ind(self): 

451 return self.triples[:, 1] 

452 

453 def initialize(self, atoms): 

454 masses = atoms.get_masses() 

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

456 

457 self.bondlengths = self.initialize_bond_lengths(atoms) 

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

459 

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

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

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

463 self.mass_n * self.mass_m) 

464 C2 = C1 / C2[:, None] 

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

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

467 C3[:, 1] *= -1 

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

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

470 C4 = C1 / C4[:, None] 

471 

472 self.C1 = C1 

473 self.C2 = C2 

474 self.C3 = C3 

475 self.C4 = C4 

476 

477 def adjust_positions(self, atoms, new): 

478 old = atoms.positions 

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

480 

481 if self.bondlengths is None: 

482 self.initialize(atoms) 

483 

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

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

486 d1 = new_n - new_m - r0 + d0 

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

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

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

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

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

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

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

494 if np.allclose(d0, r0): 

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

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

497 else: 

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

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

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

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

502 

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

504 

505 def adjust_momenta(self, atoms, p): 

506 old = atoms.positions 

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

508 

509 if self.bondlengths is None: 

510 self.initialize(atoms) 

511 

512 mass_nn = self.mass_n[:, None] 

513 mass_mm = self.mass_m[:, None] 

514 mass_oo = self.mass_o[:, None] 

515 

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

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

518 dv = p_n / mass_nn - p_m / mass_mm 

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

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

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

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

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

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

525 

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

527 

528 def adjust_forces(self, atoms, forces): 

529 

530 if self.bondlengths is None: 

531 self.initialize(atoms) 

532 

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

534 A[:, 0] *= -1 

535 A -= 1 

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

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

538 

539 self.constraint_forces = -forces 

540 old = atoms.positions 

541 

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

543 

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

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

546 df = fr_n - fr_m 

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

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

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

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

551 

552 self.constraint_forces += forces 

553 

554 def redistribute_forces_optimization(self, forces): 

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

556 optimizations. 

557 

558 The redistributed forces needs to be further adjusted using the 

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

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

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

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

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

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

565 

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

567 C4_1 * (C1_2 * forces_m - forces_o)) 

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

569 C4_2 * (C1_1 * forces_n - forces_o)) 

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

571 C4_1 * forces_n + C4_2 * forces_m) 

572 

573 return fr_n, fr_m, fr_o 

574 

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

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

577 dynamics. 

578 

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

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

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

582 Physics 47 (1982)).""" 

583 if self.bondlengths is None: 

584 self.initialize(atoms) 

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

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

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

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

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

590 mass_nn = self.mass_n[:, None] 

591 mass_mm = self.mass_m[:, None] 

592 mass_oo = self.mass_o[:, None] 

593 if rand: 

594 mr1 = (mass_mm / mass_nn) ** 0.5 

595 mr2 = (mass_oo / mass_nn) ** 0.5 

596 mr3 = (mass_nn / mass_mm) ** 0.5 

597 mr4 = (mass_oo / mass_mm) ** 0.5 

598 else: 

599 mr1 = 1.0 

600 mr2 = 1.0 

601 mr3 = 1.0 

602 mr4 = 1.0 

603 

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

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

606 mr2 * mass_mm * mass_nn * forces_o)) 

607 

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

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

610 mr4 * mass_mm * mass_nn * forces_o)) 

611 

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

613 

614 def get_slices(self, a): 

615 a_n = a[self.n_ind] 

616 a_m = a[self.m_ind] 

617 a_o = a[self.o_ind] 

618 

619 return a_n, a_m, a_o 

620 

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

622 a[self.n_ind] = a_n 

623 a[self.m_ind] = a_m 

624 a[self.o_ind] = a_o 

625 

626 def initialize_bond_lengths(self, atoms): 

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

628 

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

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

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

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

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

634 

635 return bondlengths 

636 

637 def get_indices(self): 

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

639 

640 def todict(self): 

641 return {'name': 'FixLinearTriatomic', 

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

643 

644 def index_shuffle(self, atoms, ind): 

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

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

647 map[ind] = 1 

648 n = map.sum() 

649 map[:] = -1 

650 map[ind] = range(n) 

651 triples = map[self.triples] 

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

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

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

655 

656 

657class FixedMode(FixConstraint): 

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

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

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

661 

662 def __init__(self, mode): 

663 mode = np.asarray(mode) 

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

665 

666 def get_removed_dof(self, atoms): 

667 return len(atoms) 

668 

669 def adjust_positions(self, atoms, newpositions): 

670 newpositions = newpositions.ravel() 

671 oldpositions = atoms.positions.ravel() 

672 step = newpositions - oldpositions 

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

674 

675 def adjust_forces(self, atoms, forces): 

676 forces = forces.ravel() 

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

678 

679 def index_shuffle(self, atoms, ind): 

680 eps = 1e-12 

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

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

683 excluded[ind] = False 

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

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

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

687 

688 def get_indices(self): 

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

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

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

692 # everything is being constrained. 

693 return [] 

694 

695 def todict(self): 

696 return {'name': 'FixedMode', 

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

698 

699 def __repr__(self): 

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

701 

702 

703def _normalize(direction): 

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

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

706 

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

708 return direction 

709 

710 

711class FixedPlane(IndexedConstraint): 

712 """ 

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

714 

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

716 """ 

717 

718 def __init__(self, indices, direction): 

719 """Constrain chosen atoms. 

720 

721 Parameters 

722 ---------- 

723 indices : int or list of int 

724 Index or indices for atoms that should be constrained 

725 direction : list of 3 int 

726 Direction of the normal vector 

727 

728 Examples 

729 -------- 

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

731 

732 >>> from ase.build import bulk 

733 >>> from ase.constraints import FixedPlane 

734 

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

736 >>> c = FixedPlane( 

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

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

739 ... ) 

740 >>> atoms.set_constraint(c) 

741 

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

743 

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

745 >>> atoms.set_constraint(c) 

746 """ 

747 super().__init__(indices=indices) 

748 self.dir = _normalize(direction) 

749 

750 def adjust_positions(self, atoms, newpositions): 

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

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

753 

754 def adjust_forces(self, atoms, forces): 

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

756 

757 def get_removed_dof(self, atoms): 

758 return len(self.index) 

759 

760 def todict(self): 

761 return { 

762 'name': 'FixedPlane', 

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

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

765 } 

766 

767 def __repr__(self): 

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

769 

770 

771def _projection(vectors, direction): 

772 dotprods = vectors @ direction 

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

774 return projection 

775 

776 

777class FixedLine(IndexedConstraint): 

778 """ 

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

780 

781 The line is defined by its vector *direction* 

782 """ 

783 

784 def __init__(self, indices, direction): 

785 """Constrain chosen atoms. 

786 

787 Parameters 

788 ---------- 

789 indices : int or list of int 

790 Index or indices for atoms that should be constrained 

791 direction : list of 3 int 

792 Direction of the vector defining the line 

793 

794 Examples 

795 -------- 

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

797 

798 >>> from ase.constraints import FixedLine 

799 >>> c = FixedLine( 

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

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

802 ... ) 

803 >>> atoms.set_constraint(c) 

804 

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

806 

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

808 >>> atoms.set_constraint(c) 

809 """ 

810 super().__init__(indices) 

811 self.dir = _normalize(direction) 

812 

813 def adjust_positions(self, atoms, newpositions): 

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

815 projection = _projection(step, self.dir) 

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

817 

818 def adjust_forces(self, atoms, forces): 

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

820 

821 def get_removed_dof(self, atoms): 

822 return 2 * len(self.index) 

823 

824 def __repr__(self): 

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

826 

827 def todict(self): 

828 return { 

829 'name': 'FixedLine', 

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

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

832 } 

833 

834 

835class FixCartesian(IndexedConstraint): 

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

837 

838 Parameters 

839 ---------- 

840 a : Sequence[int] 

841 Indices of atoms to be fixed. 

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

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

844 """ 

845 

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

847 super().__init__(indices=a) 

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

849 

850 def get_removed_dof(self, atoms: Atoms): 

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

852 

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

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

855 self.mask[None, :], 

856 atoms.positions[self.index], 

857 new[self.index], 

858 ) 

859 

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

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

862 

863 def todict(self): 

864 return {'name': 'FixCartesian', 

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

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

867 

868 def __repr__(self): 

869 name = type(self).__name__ 

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

871 

872 

873class FixScaled(IndexedConstraint): 

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

875 

876 Parameters 

877 ---------- 

878 a : Sequence[int] 

879 Indices of atoms to be fixed. 

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

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

882 """ 

883 

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

885 # XXX The unused cell keyword is there for compatibility 

886 # with old trajectory files. 

887 super().__init__(indices=a) 

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

889 

890 def get_removed_dof(self, atoms: Atoms): 

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

892 

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

894 cell = atoms.cell 

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

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

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

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

899 

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

901 # Forces are covariant to the coordinate transformation, 

902 # use the inverse transformations 

903 cell = atoms.cell 

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

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

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

907 

908 def todict(self): 

909 return {'name': 'FixScaled', 

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

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

912 

913 def __repr__(self): 

914 name = type(self).__name__ 

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

916 

917 

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

919# nested lists/tuples 

920class FixInternals(FixConstraint): 

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

922 

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

924 of bonds (bondcombos). 

925 

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

927 `dihedrals_deg`. 

928 Fixing planar angles is not supported at the moment. 

929 """ 

930 

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

932 angles_deg=None, dihedrals_deg=None, 

933 bondcombos=None, 

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

935 """ 

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

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

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

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

940 

941 Parameters 

942 ---------- 

943 bonds: nested python list, optional 

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

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

946 

947 angles_deg: nested python list, optional 

948 List with targetvalue and atom indices defining the fixedangles, 

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

950 

951 dihedrals_deg: nested python list, optional 

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

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

954 

955 bondcombos: nested python list, optional 

956 List with targetvalue, atom indices and linear coefficient defining 

957 the fixed linear combination of bonds, 

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

959 [index1, index2, coefficient_for_bond]]], ...] 

960 

961 mic: bool, optional, default: False 

962 Minimum image convention. 

963 

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

965 Convergence criterion. 

966 """ 

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

968 if angles: 

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

970 angles = np.asarray(angles) 

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

972 angles = angles.tolist() 

973 else: 

974 angles = angles_deg 

975 if dihedrals: 

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

977 dihedrals = np.asarray(dihedrals) 

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

979 dihedrals = dihedrals.tolist() 

980 else: 

981 dihedrals = dihedrals_deg 

982 

983 self.bonds = bonds or [] 

984 self.angles = angles or [] 

985 self.dihedrals = dihedrals or [] 

986 self.bondcombos = bondcombos or [] 

987 self.mic = mic 

988 self.epsilon = epsilon 

989 

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

991 + len(self.bondcombos)) 

992 

993 # Initialize these at run-time: 

994 self.constraints = [] 

995 self.initialized = False 

996 

997 def get_removed_dof(self, atoms): 

998 return self.n 

999 

1000 def initialize(self, atoms): 

1001 if self.initialized: 

1002 return 

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

1004 cell = None 

1005 pbc = None 

1006 if self.mic: 

1007 cell = atoms.cell 

1008 pbc = atoms.pbc 

1009 self.constraints = [] 

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

1011 (self.angles, self.FixAngle), 

1012 (self.dihedrals, self.FixDihedral), 

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

1014 for datum in data: 

1015 targetvalue = datum[0] 

1016 if targetvalue is None: # set to current value 

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

1018 self.mic) 

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

1020 self.constraints.append(constr) 

1021 self.initialized = True 

1022 

1023 @staticmethod 

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

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

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

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

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

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

1030 return c 

1031 

1032 def get_subconstraint(self, atoms, definition): 

1033 """Get pointer to a specific subconstraint. 

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

1035 self.initialize(atoms) 

1036 for subconstr in self.constraints: 

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

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

1039 subconstr.coefs)] 

1040 if defin == definition: 

1041 return subconstr 

1042 else: # identify primitive constraints by their indices 

1043 if subconstr.indices == [definition]: 

1044 return subconstr 

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

1046 

1047 def shuffle_definitions(self, shuffle_dic, internal_type): 

1048 dfns = [] # definitions 

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

1050 append = True 

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

1052 for old in dfn[1]: 

1053 if old in shuffle_dic: 

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

1055 else: 

1056 append = False 

1057 break 

1058 if append: 

1059 dfns.append(new_dfn) 

1060 return dfns 

1061 

1062 def shuffle_combos(self, shuffle_dic, internal_type): 

1063 dfns = [] # definitions 

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

1065 append = True 

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

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

1068 for i, indices in enumerate(all_indices): 

1069 for old in indices: 

1070 if old in shuffle_dic: 

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

1072 else: 

1073 append = False 

1074 break 

1075 if not append: 

1076 break 

1077 if append: 

1078 dfns.append(new_dfn) 

1079 return dfns 

1080 

1081 def index_shuffle(self, atoms, ind): 

1082 # See docstring of superclass 

1083 self.initialize(atoms) 

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

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

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

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

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

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

1090 self.initialized = False 

1091 self.initialize(atoms) 

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

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

1094 

1095 def get_indices(self): 

1096 cons = [] 

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

1098 cons.extend(dfn[1]) 

1099 for dfn in self.bondcombos: 

1100 for partial_dfn in dfn[1]: 

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

1102 return list(set(cons)) 

1103 

1104 def todict(self): 

1105 return {'name': 'FixInternals', 

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

1107 'angles_deg': self.angles, 

1108 'dihedrals_deg': self.dihedrals, 

1109 'bondcombos': self.bondcombos, 

1110 'mic': self.mic, 

1111 'epsilon': self.epsilon}} 

1112 

1113 def adjust_positions(self, atoms, newpos): 

1114 self.initialize(atoms) 

1115 for constraint in self.constraints: 

1116 constraint.setup_jacobian(atoms.positions) 

1117 for _ in range(50): 

1118 maxerr = 0.0 

1119 for constraint in self.constraints: 

1120 constraint.adjust_positions(atoms.positions, newpos) 

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

1122 if maxerr < self.epsilon: 

1123 return 

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

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

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

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

1128 ' Support for planar angles would require the' 

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

1130 ' See issue #868.') 

1131 raise ValueError(msg) 

1132 

1133 def adjust_forces(self, atoms, forces): 

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

1135 self.initialize(atoms) 

1136 positions = atoms.positions 

1137 N = len(forces) 

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

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

1140 

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

1142 

1143 tx[:, 0] = 1.0 

1144 ty[:, 1] = 1.0 

1145 tz[:, 2] = 1.0 

1146 ff = forces.ravel() 

1147 

1148 # Calculate the center of mass 

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

1150 

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

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

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

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

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

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

1157 

1158 # Normalizing transl., rotat. constraints 

1159 for r in list2_constraints: 

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

1161 

1162 # Add all angle, etc. constraint vectors 

1163 for constraint in self.constraints: 

1164 constraint.setup_jacobian(positions) 

1165 constraint.adjust_forces(positions, forces) 

1166 list_constraints.insert(0, constraint.jacobian) 

1167 # QR DECOMPOSITION - GRAM SCHMIDT 

1168 

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

1170 aa = np.column_stack(list_constraints) 

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

1172 # Projection 

1173 hh = [] 

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

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

1176 

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

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

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

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

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

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

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

1184 for vec in hh: 

1185 T += vec 

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

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

1188 

1189 def __repr__(self): 

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

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

1192 

1193 # Classes for internal use in FixInternals 

1194 class FixInternalsBase: 

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

1196 

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

1198 self.targetvalue = targetvalue # constant target value 

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

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

1201 self.masses = masses 

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

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

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

1205 self.cell = cell 

1206 self.pbc = pbc 

1207 

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

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

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

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

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

1213 for j in range(n): 

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

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

1216 return self.coefs @ jacobian 

1217 

1218 def finalize_positions(self, newpos): 

1219 jacobian = self.jacobian / self.masses 

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

1221 dnewpos = lamda * jacobian 

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

1223 

1224 def adjust_forces(self, positions, forces): 

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

1226 * self.jacobian) 

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

1228 

1229 class FixBondCombo(FixInternalsBase): 

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

1231 within FixInternals. 

1232 

1233 sum_i( coef_i * bond_length_i ) = constant 

1234 """ 

1235 

1236 def get_jacobian(self, pos): 

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

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

1239 pbc=self.pbc) 

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

1241 

1242 def setup_jacobian(self, pos): 

1243 self.jacobian = self.get_jacobian(pos) 

1244 

1245 def adjust_positions(self, oldpos, newpos): 

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

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

1248 cell=self.cell, 

1249 pbc=self.pbc) 

1250 value = self.coefs @ dists 

1251 self.sigma = value - self.targetvalue 

1252 self.finalize_positions(newpos) 

1253 

1254 @staticmethod 

1255 def get_value(atoms, indices, mic): 

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

1257 

1258 def __repr__(self): 

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

1260 '{self.coefs})') 

1261 

1262 class FixBondLengthAlt(FixBondCombo): 

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

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

1265 

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

1267 if targetvalue <= 0.: 

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

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

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

1271 

1272 @staticmethod 

1273 def get_value(atoms, indices, mic): 

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

1275 

1276 def __repr__(self): 

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

1278 

1279 class FixAngle(FixInternalsBase): 

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

1281 

1282 Convergence is potentially problematic for angles very close to 

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

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

1285 """ 

1286 

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

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

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

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

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

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

1293 

1294 def gather_vectors(self, pos): 

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

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

1297 return v0, v1 

1298 

1299 def get_jacobian(self, pos): 

1300 v0, v1 = self.gather_vectors(pos) 

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

1302 pbc=self.pbc) 

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

1304 

1305 def setup_jacobian(self, pos): 

1306 self.jacobian = self.get_jacobian(pos) 

1307 

1308 def adjust_positions(self, oldpos, newpos): 

1309 v0, v1 = self.gather_vectors(newpos) 

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

1311 self.sigma = value - self.targetvalue 

1312 self.finalize_positions(newpos) 

1313 

1314 @staticmethod 

1315 def get_value(atoms, indices, mic): 

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

1317 

1318 def __repr__(self): 

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

1320 

1321 class FixDihedral(FixInternalsBase): 

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

1323 

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

1325 becomes planar. Make sure to avoid this situation. 

1326 """ 

1327 

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

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

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

1331 

1332 def gather_vectors(self, pos): 

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

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

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

1336 return v0, v1, v2 

1337 

1338 def get_jacobian(self, pos): 

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

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

1341 pbc=self.pbc) 

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

1343 

1344 def setup_jacobian(self, pos): 

1345 self.jacobian = self.get_jacobian(pos) 

1346 

1347 def adjust_positions(self, oldpos, newpos): 

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

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

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

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

1352 self.finalize_positions(newpos) 

1353 

1354 @staticmethod 

1355 def get_value(atoms, indices, mic): 

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

1357 

1358 def __repr__(self): 

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

1360 

1361 

1362class FixParametricRelations(FixConstraint): 

1363 

1364 def __init__( 

1365 self, 

1366 indices, 

1367 Jacobian, 

1368 const_shift, 

1369 params=None, 

1370 eps=1e-12, 

1371 use_cell=False, 

1372 ): 

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

1374 space defined by the Jacobian 

1375 

1376 These constraints are based off the work in: 

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

1378 

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

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

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

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

1383 degrees of freedom onto the reduced parameter space. 

1384 

1385 Currently the constraint is set up to handle either atomic 

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

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

1388 to keep the mathematics behind the operations separate. 

1389 

1390 It would be possible to extend these constraints to allow 

1391 non-linear transformations if functionality to update the 

1392 Jacobian at each position update was included. This would 

1393 require passing an update function evaluate it every time 

1394 adjust_positions is callled. This is currently NOT supported, 

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

1396 

1397 Args: 

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

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

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

1401 The Jacobian describing 

1402 the parameter space transformation 

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

1404 A vector describing the constant term 

1405 in the transformation not accounted for in the Jacobian 

1406 params (list of str): 

1407 parameters used in the parametric representation 

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

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

1410 numbers and set the precision used 

1411 to generate the constraint expressions 

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

1413 

1414 """ 

1415 self.indices = np.array(indices) 

1416 self.Jacobian = np.array(Jacobian) 

1417 self.const_shift = np.array(const_shift) 

1418 

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

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

1421 

1422 self.eps = eps 

1423 self.use_cell = use_cell 

1424 

1425 if params is None: 

1426 params = [] 

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

1428 int_fmt_str = "{:0" + \ 

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

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

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

1432 else: 

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

1434 

1435 self.params = params 

1436 

1437 self.Jacobian_inv = np.linalg.inv( 

1438 self.Jacobian.T @ self.Jacobian) @ self.Jacobian.T 

1439 

1440 @classmethod 

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

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

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

1444 vector and constructs a FixParametricRelations constraint 

1445 

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

1447 elements must be ordered as: 

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

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

1450 component of the atomic position/lattice 

1451 vector. Currently only linear operations are allowed to be 

1452 included in the expressions so 

1453 only terms like: 

1454 - const * param_0 

1455 - sqrt[const] * param_1 

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

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

1458 the parameters passed in 

1459 params, are allowed. 

1460 

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

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

1463 expressions = [ 

1464 "1.0/3.0", "2.0/3.0", "z1", 

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

1466 "1.0/3.0", "2.0/3.0", "z2", 

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

1468 ] 

1469 

1470 For diamond are: 

1471 params = [] 

1472 expressions = [ 

1473 "0.0", "0.0", "0.0", 

1474 "0.25", "0.25", "0.25", 

1475 ], 

1476 

1477 and for stannite are 

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

1479 expressions = [ 

1480 "0.0", "0.0", "0.0", 

1481 "0.0", "0.5", "0.5", 

1482 "0.75", "0.25", "0.5", 

1483 "0.25", "0.75", "0.5", 

1484 "x4 + z4", "x4 + z4", "2*x4", 

1485 "x4 - z4", "x4 - z4", "-2*x4", 

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

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

1488 ] 

1489 

1490 Args: 

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

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

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

1494 parametric representation 

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

1496 parametric to the real space representation 

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

1498 numbers and set the precision used 

1499 to generate the constraint expressions 

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

1501 

1502 Returns: 

1503 cls( 

1504 indices, 

1505 Jacobian generated from expressions, 

1506 const_shift generated from expressions, 

1507 params, 

1508 eps-12, 

1509 use_cell, 

1510 ) 

1511 """ 

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

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

1514 

1515 for expr_ind, expression in enumerate(expressions): 

1516 expression = expression.strip() 

1517 

1518 # Convert subtraction to addition 

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

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

1521 expression = expression[1:] 

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

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

1524 

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

1526 # param_11 does not become 0.01 

1527 int_fmt_str = "{:0" + \ 

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

1529 

1530 param_dct = {} 

1531 param_map = {} 

1532 

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

1534 for param_ind, param in enumerate(params): 

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

1536 param_map[param] = param_str 

1537 param_dct[param_str] = 0.0 

1538 

1539 # Replace the parameters according to the map 

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

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

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

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

1544 

1545 # Partial linearity check 

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

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

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

1549 if n_params_in_sec > 1: 

1550 raise ValueError( 

1551 "FixParametricRelations expressions must be linear.") 

1552 

1553 const_shift[expr_ind] = float( 

1554 eval_expression(expression, param_dct)) 

1555 

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

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

1558 if param_str not in expression: 

1559 Jacobian[expr_ind, param_ind] = 0.0 

1560 continue 

1561 param_dct[param_str] = 1.0 

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

1563 test_1 -= const_shift[expr_ind] 

1564 Jacobian[expr_ind, param_ind] = test_1 

1565 

1566 param_dct[param_str] = 2.0 

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

1568 test_2 -= const_shift[expr_ind] 

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

1570 raise ValueError( 

1571 "FixParametricRelations expressions must be linear.") 

1572 param_dct[param_str] = 0.0 

1573 

1574 args = [ 

1575 indices, 

1576 Jacobian, 

1577 const_shift, 

1578 params, 

1579 eps, 

1580 use_cell, 

1581 ] 

1582 if cls is FixScaledParametricRelations: 

1583 args = args[:-1] 

1584 return cls(*args) 

1585 

1586 @property 

1587 def expressions(self): 

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

1589 and self.const_shift objects""" 

1590 expressions = [] 

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

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

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

1594 exp = "" 

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

1596 shift_val) > self.eps: 

1597 exp += fmt_str.format(shift_val) 

1598 

1599 param_exp = "" 

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

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

1602 if abs_jacob_val < self.eps: 

1603 continue 

1604 

1605 param = self.params[param_index] 

1606 if param_exp or exp: 

1607 if jacob_val > -1.0 * self.eps: 

1608 param_exp += " + " 

1609 else: 

1610 param_exp += " - " 

1611 elif (not exp) and (not param_exp) and ( 

1612 jacob_val < -1.0 * self.eps): 

1613 param_exp += "-" 

1614 

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

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

1617 else: 

1618 param_exp += (fmt_str + 

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

1620 

1621 exp += param_exp 

1622 

1623 expressions.append(exp) 

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

1625 

1626 def todict(self): 

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

1628 return { 

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

1630 "kwargs": { 

1631 "indices": self.indices, 

1632 "params": self.params, 

1633 "Jacobian": self.Jacobian, 

1634 "const_shift": self.const_shift, 

1635 "eps": self.eps, 

1636 "use_cell": self.use_cell, 

1637 } 

1638 } 

1639 

1640 def __repr__(self): 

1641 """The str representation of the constraint""" 

1642 if len(self.indices) > 1: 

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

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

1645 else: 

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

1647 

1648 if len(self.params) > 1: 

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

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

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

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

1653 else: 

1654 params_str = "[]" 

1655 

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

1657 type(self).__name__, 

1658 indices_str, 

1659 params_str, 

1660 self.eps 

1661 ) 

1662 

1663 

1664class FixScaledParametricRelations(FixParametricRelations): 

1665 

1666 def __init__( 

1667 self, 

1668 indices, 

1669 Jacobian, 

1670 const_shift, 

1671 params=None, 

1672 eps=1e-12, 

1673 ): 

1674 """The fractional coordinate version of FixParametricRelations 

1675 

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

1677 coordinates use_cell is false""" 

1678 super().__init__( 

1679 indices, 

1680 Jacobian, 

1681 const_shift, 

1682 params, 

1683 eps, 

1684 False, 

1685 ) 

1686 

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

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

1689 with the unit transformation""" 

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

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

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

1693 

1694 return cell.cartesian_positions(scaled) 

1695 

1696 def adjust_positions(self, atoms, positions): 

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

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

1699 atoms.cell, 

1700 positions[self.indices], 

1701 self.const_shift, 

1702 ) 

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

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

1705 

1706 def adjust_B(self, cell, positions): 

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

1708 keep track of this change""" 

1709 fractional = cell.scaled_positions(positions) 

1710 wrapped_fractional = (fractional % 1.0) % 1.0 

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

1712 return cell.cartesian_positions(wrapped_fractional) 

1713 

1714 def adjust_momenta(self, atoms, momenta): 

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

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

1717 atoms.cell, 

1718 momenta[self.indices], 

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

1720 ) 

1721 

1722 def adjust_forces(self, atoms, forces): 

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

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

1725 # inverse transformations 

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

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

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

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

1730 

1731 jacobian = cart2frac_jacob @ self.Jacobian 

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

1733 

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

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

1736 

1737 def todict(self): 

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

1739 dct = super().todict() 

1740 del dct["kwargs"]["use_cell"] 

1741 return dct 

1742 

1743 

1744class FixCartesianParametricRelations(FixParametricRelations): 

1745 

1746 def __init__( 

1747 self, 

1748 indices, 

1749 Jacobian, 

1750 const_shift, 

1751 params=None, 

1752 eps=1e-12, 

1753 use_cell=False, 

1754 ): 

1755 """The Cartesian coordinate version of FixParametricRelations""" 

1756 super().__init__( 

1757 indices, 

1758 Jacobian, 

1759 const_shift, 

1760 params, 

1761 eps, 

1762 use_cell, 

1763 ) 

1764 

1765 def adjust_contravariant(self, vecs, B): 

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

1767 the unit transformation""" 

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

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

1770 return vecs 

1771 

1772 def adjust_positions(self, atoms, positions): 

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

1774 if self.use_cell: 

1775 return 

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

1777 positions[self.indices], 

1778 self.const_shift, 

1779 ) 

1780 

1781 def adjust_momenta(self, atoms, momenta): 

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

1783 if self.use_cell: 

1784 return 

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

1786 momenta[self.indices], 

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

1788 ) 

1789 

1790 def adjust_forces(self, atoms, forces): 

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

1792 if self.use_cell: 

1793 return 

1794 

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

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

1797 forces_reduced).reshape(-1, 3) 

1798 

1799 def adjust_cell(self, atoms, cell): 

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

1801 if not self.use_cell: 

1802 return 

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

1804 cell[self.indices], 

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

1806 ) 

1807 

1808 def adjust_stress(self, atoms, stress): 

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

1810 if not self.use_cell: 

1811 return 

1812 

1813 stress_3x3 = voigt_6_to_full_3x3_stress(stress) 

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

1815 stress_3x3[self.indices] = ( 

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

1817 

1818 stress[:] = full_3x3_to_voigt_6_stress(stress_3x3) 

1819 

1820 

1821class Hookean(FixConstraint): 

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

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

1824 

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

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

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

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

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

1830 distance above a plane. 

1831 

1832 a1 : int 

1833 Index of atom 1 

1834 a2 : one of three options 

1835 1) index of atom 2 

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

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

1838 k : float 

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

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

1841 rt : float 

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

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

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

1845 

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

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

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

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

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

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

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

1853 

1854 References: 

1855 

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

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

1858 """ 

1859 

1860 if isinstance(a2, int): 

1861 self._type = 'two atoms' 

1862 self.indices = [a1, a2] 

1863 elif len(a2) == 3: 

1864 self._type = 'point' 

1865 self.index = a1 

1866 self.origin = np.array(a2) 

1867 elif len(a2) == 4: 

1868 self._type = 'plane' 

1869 self.index = a1 

1870 self.plane = a2 

1871 else: 

1872 raise RuntimeError('Unknown type for a2') 

1873 self.threshold = rt 

1874 self.spring = k 

1875 

1876 def get_removed_dof(self, atoms): 

1877 return 0 

1878 

1879 def todict(self): 

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

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

1882 'k': self.spring} 

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

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

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

1886 elif self._type == 'point': 

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

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

1889 elif self._type == 'plane': 

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

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

1892 else: 

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

1894 return dct 

1895 

1896 def adjust_positions(self, atoms, newpositions): 

1897 pass 

1898 

1899 def adjust_momenta(self, atoms, momenta): 

1900 pass 

1901 

1902 def adjust_forces(self, atoms, forces): 

1903 positions = atoms.positions 

1904 if self._type == 'plane': 

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

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

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

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

1909 if d < 0: 

1910 return 

1911 magnitude = self.spring * d 

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

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

1914 return 

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

1916 p1, p2 = positions[self.indices] 

1917 elif self._type == 'point': 

1918 p1 = positions[self.index] 

1919 p2 = self.origin 

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

1921 bondlength = np.linalg.norm(displace) 

1922 if bondlength > self.threshold: 

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

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

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

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

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

1928 else: 

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

1930 

1931 def adjust_potential_energy(self, atoms): 

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

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

1934 potential energy.)""" 

1935 positions = atoms.positions 

1936 if self._type == 'plane': 

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

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

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

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

1941 if d > 0: 

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

1943 else: 

1944 return 0. 

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

1946 p1, p2 = positions[self.indices] 

1947 elif self._type == 'point': 

1948 p1 = positions[self.index] 

1949 p2 = self.origin 

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

1951 bondlength = np.linalg.norm(displace) 

1952 if bondlength > self.threshold: 

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

1954 else: 

1955 return 0. 

1956 

1957 def get_indices(self): 

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

1959 return self.indices 

1960 elif self._type == 'point': 

1961 return self.index 

1962 elif self._type == 'plane': 

1963 return self.index 

1964 

1965 def index_shuffle(self, atoms, ind): 

1966 # See docstring of superclass 

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

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

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

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

1971 if old == a: 

1972 newa[i] = new 

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

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

1975 self.indices = newa 

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

1977 newa = -1 # Signal error 

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

1979 if old == self.index: 

1980 newa = new 

1981 break 

1982 if newa == -1: 

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

1984 self.index = newa 

1985 

1986 def __repr__(self): 

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

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

1989 elif self._type == 'point': 

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

1991 else: 

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

1993 

1994 

1995class ExternalForce(FixConstraint): 

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

1997 

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

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

2000 between atom1-2 and atom3-4: 

2001 

2002 >>> from ase.build import bulk 

2003 

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

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

2006 >>> fext = 1.0 

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

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

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

2010 

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

2012 

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

2014 self.indices = [a1, a2] 

2015 self.external_force = f_ext 

2016 

2017 def get_removed_dof(self, atoms): 

2018 return 0 

2019 

2020 def adjust_positions(self, atoms, new): 

2021 pass 

2022 

2023 def adjust_forces(self, atoms, forces): 

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

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

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

2027 

2028 def adjust_potential_energy(self, atoms): 

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

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

2031 

2032 def index_shuffle(self, atoms, ind): 

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

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

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

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

2037 if old == a: 

2038 newa[i] = new 

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

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

2041 self.indices = newa 

2042 

2043 def __repr__(self): 

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

2045 self.indices[1], 

2046 self.external_force) 

2047 

2048 def todict(self): 

2049 return {'name': 'ExternalForce', 

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

2051 'f_ext': self.external_force}} 

2052 

2053 

2054class MirrorForce(FixConstraint): 

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

2056 

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

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

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

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

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

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

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

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

2065 during the optimization with *MirrorForce*. 

2066 

2067 Parameters 

2068 ---------- 

2069 a1: int 

2070 First atom index. 

2071 a2: int 

2072 Second atom index. 

2073 max_dist: float 

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

2075 can be found. 

2076 min_dist: float 

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

2078 can be found. 

2079 fmax: float 

2080 Maximum force used for the optimization. 

2081 

2082 Notes 

2083 ----- 

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

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

2086 between atom1-2 and atom3-4: 

2087 

2088 >>> from ase.build import bulk 

2089 

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

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

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

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

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

2095 

2096 """ 

2097 

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

2099 self.indices = [a1, a2] 

2100 self.min_dist = min_dist 

2101 self.max_dist = max_dist 

2102 self.fmax = fmax 

2103 

2104 def adjust_positions(self, atoms, new): 

2105 pass 

2106 

2107 def adjust_forces(self, atoms, forces): 

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

2109 d = np.linalg.norm(dist) 

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

2111 # Stop structure optimization 

2112 forces[:] *= 0 

2113 return 

2114 dist /= d 

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

2116 f = df.dot(dist) 

2117 con_saved = atoms.constraints 

2118 try: 

2119 con = [con for con in con_saved 

2120 if not isinstance(con, MirrorForce)] 

2121 atoms.set_constraint(con) 

2122 forces_copy = atoms.get_forces() 

2123 finally: 

2124 atoms.set_constraint(con_saved) 

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

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

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

2128 # would also be fixed 

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

2130 factor = 1. 

2131 else: 

2132 factor = 0. 

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

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

2135 

2136 def index_shuffle(self, atoms, ind): 

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

2138 

2139 """ 

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

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

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

2143 if old == a: 

2144 newa[i] = new 

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

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

2147 self.indices = newa 

2148 

2149 def __repr__(self): 

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

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

2152 self.fmax) 

2153 

2154 def todict(self): 

2155 return {'name': 'MirrorForce', 

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

2157 'max_dist': self.max_dist, 

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

2159 

2160 

2161class MirrorTorque(FixConstraint): 

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

2163 angle defined by four atoms. 

2164 

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

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

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

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

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

2170 to find the transition state. Transition states in 

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

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

2173 during the optimization with *MirrorTorque*. 

2174 

2175 This constraint can be used to find 

2176 transition states of cis-trans-isomerization. 

2177 

2178 a1 a4 

2179 | | 

2180 a2 __ a3 

2181 

2182 Parameters 

2183 ---------- 

2184 a1: int 

2185 First atom index. 

2186 a2: int 

2187 Second atom index. 

2188 a3: int 

2189 Third atom index. 

2190 a4: int 

2191 Fourth atom index. 

2192 max_angle: float 

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

2194 can be found. 

2195 min_angle: float 

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

2197 can be found. 

2198 fmax: float 

2199 Maximum force used for the optimization. 

2200 

2201 Notes 

2202 ----- 

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

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

2205 between atom1-4 and atom5-6: 

2206 

2207 >>> from ase.build import bulk 

2208 

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

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

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

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

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

2214 

2215 """ 

2216 

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

2218 fmax=0.1): 

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

2220 self.min_angle = min_angle 

2221 self.max_angle = max_angle 

2222 self.fmax = fmax 

2223 

2224 def adjust_positions(self, atoms, new): 

2225 pass 

2226 

2227 def adjust_forces(self, atoms, forces): 

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

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

2230 angle *= np.pi / 180. 

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

2232 # Stop structure optimization 

2233 forces[:] *= 0 

2234 return 

2235 p = atoms.positions[self.indices] 

2236 f = forces[self.indices] 

2237 

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

2239 ff = f - f0 

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

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

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

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

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

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

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

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

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

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

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

2251 omegap = omegap1 + omegap2 

2252 con_saved = atoms.constraints 

2253 try: 

2254 con = [con for con in con_saved 

2255 if not isinstance(con, MirrorTorque)] 

2256 atoms.set_constraint(con) 

2257 forces_copy = atoms.get_forces() 

2258 finally: 

2259 atoms.set_constraint(con_saved) 

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

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

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

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

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

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

2266 # mirrored torque would also be fixed 

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

2268 factor = 1. 

2269 else: 

2270 factor = 0. 

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

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

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

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

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

2276 

2277 def index_shuffle(self, atoms, ind): 

2278 # See docstring of superclass 

2279 indices = [] 

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

2281 if old in self.indices: 

2282 indices.append(new) 

2283 if len(indices) == 0: 

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

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

2286 

2287 def __repr__(self): 

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

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

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

2291 

2292 def todict(self): 

2293 return {'name': 'MirrorTorque', 

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

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

2296 'max_angle': self.max_angle, 

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

2298 

2299 

2300class FixSymmetry(FixConstraint): 

2301 """ 

2302 Constraint to preserve spacegroup symmetry during optimisation. 

2303 

2304 Requires spglib package to be available. 

2305 """ 

2306 

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

2308 adjust_cell=True, verbose=False): 

2309 self.atoms = atoms.copy() 

2310 self.symprec = symprec 

2311 self.verbose = verbose 

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

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

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

2315 self.do_adjust_positions = adjust_positions 

2316 self.do_adjust_cell = adjust_cell 

2317 

2318 def adjust_cell(self, atoms, cell): 

2319 if not self.do_adjust_cell: 

2320 return 

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

2322 # UnitCellFilter uses deformation gradient as cell DOF with steps 

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

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

2325 cur_cell = atoms.get_cell() 

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

2327 

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

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

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

2331 

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

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

2334 # gradient 

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

2336 if max_delta_deform_grad > 0.25: 

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

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

2339 .format(max_delta_deform_grad)) 

2340 elif max_delta_deform_grad > 0.15: 

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

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

2343 

2344 symmetrized_delta_deform_grad = symmetrize_rank2(cur_cell, cur_cell_inv, 

2345 delta_deform_grad, 

2346 self.rotations) 

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

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

2349 

2350 def adjust_positions(self, atoms, new): 

2351 if not self.do_adjust_positions: 

2352 return 

2353 # symmetrize changes in position as rank 1 tensors 

2354 step = new - atoms.positions 

2355 symmetrized_step = symmetrize_rank1(atoms.get_cell(), 

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

2357 self.rotations, self.translations, 

2358 self.symm_map) 

2359 new[:] = atoms.positions + symmetrized_step 

2360 

2361 def adjust_forces(self, atoms, forces): 

2362 # symmetrize forces as rank 1 tensors 

2363 # print('adjusting forces') 

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

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

2366 self.rotations, self.translations, 

2367 self.symm_map) 

2368 

2369 def adjust_stress(self, atoms, stress): 

2370 # symmetrize stress as rank 2 tensor 

2371 raw_stress = voigt_6_to_full_3x3_stress(stress) 

2372 symmetrized_stress = symmetrize_rank2(atoms.get_cell(), 

2373 atoms.cell.reciprocal().T, 

2374 raw_stress, self.rotations) 

2375 stress[:] = full_3x3_to_voigt_6_stress(symmetrized_stress) 

2376 

2377 def index_shuffle(self, atoms, ind): 

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

2379 raise RuntimeError("FixSymmetry can only accomodate atom" 

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

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

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

2383 

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

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

2386 new_symm_map = [] 

2387 for sm in self.symm_map: 

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

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

2390 new_sm[ind_reversed[at_i]] = ind_reversed[sm[at_i]] 

2391 new_symm_map.append(new_sm) 

2392 

2393 self.symm_map = new_symm_map 

2394 

2395 def todict(self): 

2396 return { 

2397 'name': 'FixSymmetry', 

2398 'kwargs': { 

2399 'atoms': self.atoms, 

2400 'symprec': self.symprec, 

2401 'adjust_positions': self.do_adjust_positions, 

2402 'adjust_cell': self.do_adjust_cell, 

2403 'verbose': self.verbose, 

2404 }, 

2405 } 

2406 

2407 

2408class Filter(FilterOld): 

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

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

2411 """ 

2412 .. deprecated:: 3.23.0 

2413 Import ``Filter`` from :mod:`ase.filters` 

2414 """ 

2415 super().__init__(*args, **kwargs) 

2416 

2417 

2418class StrainFilter(StrainFilterOld): 

2419 @deprecated('Import StrainFilter from ase.filters') 

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

2421 """ 

2422 .. deprecated:: 3.23.0 

2423 Import ``StrainFilter`` from :mod:`ase.filters` 

2424 """ 

2425 super().__init__(*args, **kwargs) 

2426 

2427 

2428class UnitCellFilter(UnitCellFilterOld): 

2429 @deprecated('Import UnitCellFilter from ase.filters') 

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

2431 """ 

2432 .. deprecated:: 3.23.0 

2433 Import ``UnitCellFilter`` from :mod:`ase.filters` 

2434 """ 

2435 super().__init__(*args, **kwargs) 

2436 

2437 

2438class ExpCellFilter(ExpCellFilterOld): 

2439 @deprecated('Import ExpCellFilter from ase.filters') 

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

2441 """ 

2442 .. deprecated:: 3.23.0 

2443 Import ``ExpCellFilter`` from :mod:`ase.filters` 

2444 or use :class:`~ase.filters.FrechetCellFilter` for better 

2445 convergence w.r.t. cell variables 

2446 """ 

2447 super().__init__(*args, **kwargs)