Coverage for /builds/kinetik161/ase/ase/constraints.py: 86.76%

1156 statements  

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

1"""Constraints""" 

2from typing import Sequence 

3from warnings import warn 

4 

5import numpy as np 

6 

7from ase.filters import ExpCellFilter as ExpCellFilterOld 

8from ase.filters import Filter as FilterOld 

9from ase.filters import StrainFilter as StrainFilterOld 

10from ase.filters import UnitCellFilter as UnitCellFilterOld 

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

12 get_angles_derivatives, get_dihedrals, 

13 get_dihedrals_derivatives, get_distances_derivatives, 

14 wrap_positions) 

15from ase.stress import full_3x3_to_voigt_6_stress, voigt_6_to_full_3x3_stress 

16from ase.utils import deprecated 

17from ase.utils.parsemath import eval_expression 

18 

19__all__ = [ 

20 'FixCartesian', 'FixBondLength', 'FixedMode', 

21 'FixAtoms', 'FixScaled', 'FixCom', 'FixedPlane', 

22 'FixConstraint', 'FixedLine', 'FixBondLengths', 'FixLinearTriatomic', 

23 'FixInternals', 'Hookean', 'ExternalForce', 'MirrorForce', 'MirrorTorque', 

24 "FixScaledParametricRelations", "FixCartesianParametricRelations"] 

25 

26 

27def dict2constraint(dct): 

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

29 raise ValueError 

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

31 

32 

33def slice2enlist(s, n): 

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

35 if isinstance(s, slice): 

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

37 return enumerate(s) 

38 

39 

40def constrained_indices(atoms, only_include=None): 

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

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

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

44 given constraint. 

45 """ 

46 indices = [] 

47 for constraint in atoms.constraints: 

48 if only_include is not None: 

49 if not isinstance(constraint, only_include): 

50 continue 

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

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

53 

54 

55class FixConstraint: 

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

57 

58 def index_shuffle(self, atoms, ind): 

59 """Change the indices. 

60 

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

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

63 constraints. 

64 

65 ind -- List or tuple of indices. 

66 

67 """ 

68 raise NotImplementedError 

69 

70 def repeat(self, m, n): 

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

72 of the underlying atoms object for the assignment of 

73 multiplied constraints to work. 

74 """ 

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

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

77 'remove your constraints.') 

78 raise NotImplementedError(msg) 

79 

80 def adjust_momenta(self, atoms, momenta): 

81 """Adjusts momenta in identical manner to forces.""" 

82 self.adjust_forces(atoms, momenta) 

83 

84 def copy(self): 

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

86 

87 

88class IndexedConstraint(FixConstraint): 

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

90 """Constrain chosen atoms. 

91 

92 Parameters 

93 ---------- 

94 indices : sequence of int 

95 Indices for those atoms that should be constrained. 

96 mask : sequence of bool 

97 One boolean per atom indicating if the atom should be 

98 constrained or not. 

99 """ 

100 

101 if mask is not None: 

102 if indices is not None: 

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

104 indices = mask 

105 indices = np.atleast_1d(indices) 

106 if np.ndim(indices) > 1: 

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

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

109 

110 if indices.dtype == bool: 

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

112 elif len(indices) == 0: 

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

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

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

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

117 

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

119 raise ValueError( 

120 'The indices array contains duplicates. ' 

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

122 'forgot the mask= keyword.') 

123 

124 self.index = indices 

125 

126 def index_shuffle(self, atoms, ind): 

127 # See docstring of superclass 

128 index = [] 

129 

130 # Resolve negative indices: 

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

132 

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

134 if old in actual_indices: 

135 index.append(new) 

136 if len(index) == 0: 

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

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

139 # XXX make immutable 

140 

141 def get_indices(self): 

142 return self.index.copy() 

143 

144 def repeat(self, m, n): 

145 i0 = 0 

146 natoms = 0 

147 if isinstance(m, int): 

148 m = (m, m, m) 

149 index_new = [] 

150 for m2 in range(m[2]): 

151 for m1 in range(m[1]): 

152 for m0 in range(m[0]): 

153 i1 = i0 + n 

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

155 i0 = i1 

156 natoms += n 

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

158 # XXX make immutable 

159 return self 

160 

161 def delete_atoms(self, indices, natoms): 

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

163 

164 Required for removing atoms with existing constraint. 

165 """ 

166 

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

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

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

170 index = i[self.index] 

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

172 # XXX make immutable 

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

174 return None 

175 return self 

176 

177 

178class FixAtoms(IndexedConstraint): 

179 """Fix chosen atoms. 

180 

181 Examples 

182 -------- 

183 Fix all Copper atoms: 

184 

185 >>> from ase.build import bulk 

186 

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

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

189 >>> c = FixAtoms(mask=mask) 

190 >>> atoms.set_constraint(c) 

191 

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

193 

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

195 >>> atoms.set_constraint(c) 

196 """ 

197 

198 def get_removed_dof(self, atoms): 

199 return 3 * len(self.index) 

200 

201 def adjust_positions(self, atoms, new): 

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

203 

204 def adjust_forces(self, atoms, forces): 

205 forces[self.index] = 0.0 

206 

207 def __repr__(self): 

208 clsname = type(self).__name__ 

209 indices = ints2string(self.index) 

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

211 

212 def todict(self): 

213 return {'name': 'FixAtoms', 

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

215 

216 

217class FixCom(FixConstraint): 

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

219 

220 def get_removed_dof(self, atoms): 

221 return 3 

222 

223 def adjust_positions(self, atoms, new): 

224 masses = atoms.get_masses() 

225 old_cm = atoms.get_center_of_mass() 

226 new_cm = np.dot(masses, new) / masses.sum() 

227 diff = old_cm - new_cm 

228 new += diff 

229 

230 def adjust_momenta(self, atoms, momenta): 

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

232 masses = atoms.get_masses() 

233 velocity_com = momenta.sum(axis=0) / masses.sum() 

234 momenta -= masses[:, None] * velocity_com 

235 

236 def adjust_forces(self, atoms, forces): 

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

238 masses = atoms.get_masses() 

239 forces -= masses[:, None] * (masses @ forces) / sum(masses**2) 

240 

241 def todict(self): 

242 return {'name': 'FixCom', 

243 'kwargs': {}} 

244 

245 

246def ints2string(x, threshold=None): 

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

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

249 return str(x.tolist()) 

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

251 

252 

253class FixBondLengths(FixConstraint): 

254 maxiter = 500 

255 

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

257 bondlengths=None, iterations=None): 

258 """iterations: 

259 Ignored""" 

260 self.pairs = np.asarray(pairs) 

261 self.tolerance = tolerance 

262 self.bondlengths = bondlengths 

263 

264 def get_removed_dof(self, atoms): 

265 return len(self.pairs) 

266 

267 def adjust_positions(self, atoms, new): 

268 old = atoms.positions 

269 masses = atoms.get_masses() 

270 

271 if self.bondlengths is None: 

272 self.bondlengths = self.initialize_bond_lengths(atoms) 

273 

274 for i in range(self.maxiter): 

275 converged = True 

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

277 a = ab[0] 

278 b = ab[1] 

279 cd = self.bondlengths[j] 

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

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

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

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

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

285 if abs(x) > self.tolerance: 

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

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

288 converged = False 

289 if converged: 

290 break 

291 else: 

292 raise RuntimeError('Did not converge') 

293 

294 def adjust_momenta(self, atoms, p): 

295 old = atoms.positions 

296 masses = atoms.get_masses() 

297 

298 if self.bondlengths is None: 

299 self.bondlengths = self.initialize_bond_lengths(atoms) 

300 

301 for i in range(self.maxiter): 

302 converged = True 

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

304 a = ab[0] 

305 b = ab[1] 

306 cd = self.bondlengths[j] 

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

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

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

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

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

312 if abs(x) > self.tolerance: 

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

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

315 converged = False 

316 if converged: 

317 break 

318 else: 

319 raise RuntimeError('Did not converge') 

320 

321 def adjust_forces(self, atoms, forces): 

322 self.constraint_forces = -forces 

323 self.adjust_momenta(atoms, forces) 

324 self.constraint_forces += forces 

325 

326 def initialize_bond_lengths(self, atoms): 

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

328 

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

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

331 

332 return bondlengths 

333 

334 def get_indices(self): 

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

336 

337 def todict(self): 

338 return {'name': 'FixBondLengths', 

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

340 'tolerance': self.tolerance}} 

341 

342 def index_shuffle(self, atoms, ind): 

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

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

345 map[ind] = 1 

346 n = map.sum() 

347 map[:] = -1 

348 map[ind] = range(n) 

349 pairs = map[self.pairs] 

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

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

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

353 

354 

355def FixBondLength(a1, a2): 

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

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

358 

359 

360class FixLinearTriatomic(FixConstraint): 

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

362 

363 def __init__(self, triples): 

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

365 and linear vectorial constraints to the position of central 

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

367 type: 

368 

369 n--o--m 

370 

371 Parameters: 

372 

373 triples: list 

374 Indices of the atoms forming the linear molecules to constrain 

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

376 

377 When using these constraints in molecular dynamics or structure 

378 optimizations, atomic forces need to be redistributed within a 

379 triple. The function redistribute_forces_optimization implements 

380 the redistribution of forces for structure optimization, while 

381 the function redistribute_forces_md implements the redistribution 

382 for molecular dynamics. 

383 

384 References: 

385 

386 Ciccotti et al. Molecular Physics 47 (1982) 

387 :doi:`10.1080/00268978200100942` 

388 """ 

389 self.triples = np.asarray(triples) 

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

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

392 self.bondlengths = None 

393 

394 def get_removed_dof(self, atoms): 

395 return 4 * len(self.triples) 

396 

397 @property 

398 def n_ind(self): 

399 return self.triples[:, 0] 

400 

401 @property 

402 def m_ind(self): 

403 return self.triples[:, 2] 

404 

405 @property 

406 def o_ind(self): 

407 return self.triples[:, 1] 

408 

409 def initialize(self, atoms): 

410 masses = atoms.get_masses() 

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

412 

413 self.bondlengths = self.initialize_bond_lengths(atoms) 

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

415 

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

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

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

419 self.mass_n * self.mass_m) 

420 C2 = C1 / C2[:, None] 

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

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

423 C3[:, 1] *= -1 

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

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

426 C4 = C1 / C4[:, None] 

427 

428 self.C1 = C1 

429 self.C2 = C2 

430 self.C3 = C3 

431 self.C4 = C4 

432 

433 def adjust_positions(self, atoms, new): 

434 old = atoms.positions 

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

436 

437 if self.bondlengths is None: 

438 self.initialize(atoms) 

439 

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

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

442 d1 = new_n - new_m - r0 + d0 

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

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

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

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

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

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

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

450 if np.allclose(d0, r0): 

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

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

453 else: 

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

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

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

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

458 

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

460 

461 def adjust_momenta(self, atoms, p): 

462 old = atoms.positions 

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

464 

465 if self.bondlengths is None: 

466 self.initialize(atoms) 

467 

468 mass_nn = self.mass_n[:, None] 

469 mass_mm = self.mass_m[:, None] 

470 mass_oo = self.mass_o[:, None] 

471 

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

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

474 dv = p_n / mass_nn - p_m / mass_mm 

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

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

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

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

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

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

481 

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

483 

484 def adjust_forces(self, atoms, forces): 

485 

486 if self.bondlengths is None: 

487 self.initialize(atoms) 

488 

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

490 A[:, 0] *= -1 

491 A -= 1 

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

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

494 

495 self.constraint_forces = -forces 

496 old = atoms.positions 

497 

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

499 

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

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

502 df = fr_n - fr_m 

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

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

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

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

507 

508 self.constraint_forces += forces 

509 

510 def redistribute_forces_optimization(self, forces): 

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

512 optimizations. 

513 

514 The redistributed forces needs to be further adjusted using the 

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

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

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

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

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

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

521 

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

523 C4_1 * (C1_2 * forces_m - forces_o)) 

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

525 C4_2 * (C1_1 * forces_n - forces_o)) 

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

527 C4_1 * forces_n + C4_2 * forces_m) 

528 

529 return fr_n, fr_m, fr_o 

530 

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

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

533 dynamics. 

534 

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

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

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

538 Physics 47 (1982)).""" 

539 if self.bondlengths is None: 

540 self.initialize(atoms) 

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

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

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

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

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

546 mass_nn = self.mass_n[:, None] 

547 mass_mm = self.mass_m[:, None] 

548 mass_oo = self.mass_o[:, None] 

549 if rand: 

550 mr1 = (mass_mm / mass_nn) ** 0.5 

551 mr2 = (mass_oo / mass_nn) ** 0.5 

552 mr3 = (mass_nn / mass_mm) ** 0.5 

553 mr4 = (mass_oo / mass_mm) ** 0.5 

554 else: 

555 mr1 = 1.0 

556 mr2 = 1.0 

557 mr3 = 1.0 

558 mr4 = 1.0 

559 

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

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

562 mr2 * mass_mm * mass_nn * forces_o)) 

563 

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

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

566 mr4 * mass_mm * mass_nn * forces_o)) 

567 

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

569 

570 def get_slices(self, a): 

571 a_n = a[self.n_ind] 

572 a_m = a[self.m_ind] 

573 a_o = a[self.o_ind] 

574 

575 return a_n, a_m, a_o 

576 

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

578 a[self.n_ind] = a_n 

579 a[self.m_ind] = a_m 

580 a[self.o_ind] = a_o 

581 

582 def initialize_bond_lengths(self, atoms): 

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

584 

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

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

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

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

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

590 

591 return bondlengths 

592 

593 def get_indices(self): 

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

595 

596 def todict(self): 

597 return {'name': 'FixLinearTriatomic', 

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

599 

600 def index_shuffle(self, atoms, ind): 

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

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

603 map[ind] = 1 

604 n = map.sum() 

605 map[:] = -1 

606 map[ind] = range(n) 

607 triples = map[self.triples] 

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

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

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

611 

612 

613class FixedMode(FixConstraint): 

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

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

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

617 

618 def __init__(self, mode): 

619 mode = np.asarray(mode) 

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

621 

622 def get_removed_dof(self, atoms): 

623 return len(atoms) 

624 

625 def adjust_positions(self, atoms, newpositions): 

626 newpositions = newpositions.ravel() 

627 oldpositions = atoms.positions.ravel() 

628 step = newpositions - oldpositions 

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

630 

631 def adjust_forces(self, atoms, forces): 

632 forces = forces.ravel() 

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

634 

635 def index_shuffle(self, atoms, ind): 

636 eps = 1e-12 

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

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

639 excluded[ind] = False 

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

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

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

643 

644 def get_indices(self): 

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

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

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

648 # everything is being constrained. 

649 return [] 

650 

651 def todict(self): 

652 return {'name': 'FixedMode', 

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

654 

655 def __repr__(self): 

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

657 

658 

659def _normalize(direction): 

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

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

662 

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

664 return direction 

665 

666 

667class FixedPlane(IndexedConstraint): 

668 """ 

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

670 

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

672 """ 

673 

674 def __init__(self, indices, direction): 

675 """Constrain chosen atoms. 

676 

677 Parameters 

678 ---------- 

679 indices : int or list of int 

680 Index or indices for atoms that should be constrained 

681 direction : list of 3 int 

682 Direction of the normal vector 

683 

684 Examples 

685 -------- 

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

687 

688 >>> from ase.build import bulk 

689 >>> from ase.constraints import FixedPlane 

690 

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

692 >>> c = FixedPlane( 

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

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

695 ... ) 

696 >>> atoms.set_constraint(c) 

697 

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

699 

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

701 >>> atoms.set_constraint(c) 

702 """ 

703 super().__init__(indices=indices) 

704 self.dir = _normalize(direction) 

705 

706 def adjust_positions(self, atoms, newpositions): 

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

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

709 

710 def adjust_forces(self, atoms, forces): 

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

712 

713 def get_removed_dof(self, atoms): 

714 return len(self.index) 

715 

716 def todict(self): 

717 return { 

718 'name': 'FixedPlane', 

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

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

721 } 

722 

723 def __repr__(self): 

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

725 

726 

727def _projection(vectors, direction): 

728 dotprods = vectors @ direction 

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

730 return projection 

731 

732 

733class FixedLine(IndexedConstraint): 

734 """ 

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

736 

737 The line is defined by its vector *direction* 

738 """ 

739 

740 def __init__(self, indices, direction): 

741 """Constrain chosen atoms. 

742 

743 Parameters 

744 ---------- 

745 indices : int or list of int 

746 Index or indices for atoms that should be constrained 

747 direction : list of 3 int 

748 Direction of the vector defining the line 

749 

750 Examples 

751 -------- 

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

753 

754 >>> from ase.constraints import FixedLine 

755 >>> c = FixedLine( 

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

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

758 ... ) 

759 >>> atoms.set_constraint(c) 

760 

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

762 

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

764 >>> atoms.set_constraint(c) 

765 """ 

766 super().__init__(indices) 

767 self.dir = _normalize(direction) 

768 

769 def adjust_positions(self, atoms, newpositions): 

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

771 projection = _projection(step, self.dir) 

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

773 

774 def adjust_forces(self, atoms, forces): 

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

776 

777 def get_removed_dof(self, atoms): 

778 return 2 * len(self.index) 

779 

780 def __repr__(self): 

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

782 

783 def todict(self): 

784 return { 

785 'name': 'FixedLine', 

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

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

788 } 

789 

790 

791class FixCartesian(IndexedConstraint): 

792 'Fix an atom index *a* in the directions of the cartesian coordinates.' 

793 

794 def __init__(self, a, mask=(1, 1, 1)): 

795 super().__init__(indices=a) 

796 self.mask = ~np.asarray(mask, bool) 

797 

798 def get_removed_dof(self, atoms): 

799 return (3 - self.mask.sum()) * len(self.index) 

800 

801 def adjust_positions(self, atoms, new): 

802 step = new[self.index] - atoms.positions[self.index] 

803 step *= self.mask[None, :] 

804 new[self.index] = atoms.positions[self.index] + step 

805 

806 def adjust_forces(self, atoms, forces): 

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

808 

809 def __repr__(self): 

810 return 'FixCartesian(indices={}, mask={})'.format( 

811 self.index.tolist(), list(~self.mask)) 

812 

813 def todict(self): 

814 return {'name': 'FixCartesian', 

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

816 'mask': (~self.mask).tolist()}} 

817 

818 

819class FixScaled(IndexedConstraint): 

820 'Fix an atom index *a* in the directions of the unit vectors.' 

821 

822 def __init__(self, a, mask=(1, 1, 1), cell=None): 

823 # XXX The unused cell keyword is there for compatibility 

824 # with old trajectory files. 

825 super().__init__(a) 

826 self.mask = np.array(mask, bool) 

827 

828 def get_removed_dof(self, atoms): 

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

830 

831 def adjust_positions(self, atoms, new): 

832 cell = atoms.cell 

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

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

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

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

837 

838 def adjust_forces(self, atoms, forces): 

839 # Forces are covariant to the coordinate transformation, 

840 # use the inverse transformations 

841 cell = atoms.cell 

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

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

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

845 

846 def todict(self): 

847 return {'name': 'FixScaled', 

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

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

850 

851 def __repr__(self): 

852 return f'FixScaled({self.index.tolist()}, {self.mask})' 

853 

854 

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

856# nested lists/tuples 

857class FixInternals(FixConstraint): 

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

859 

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

861 of bonds (bondcombos). 

862 

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

864 `dihedrals_deg`. 

865 Fixing planar angles is not supported at the moment. 

866 """ 

867 

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

869 angles_deg=None, dihedrals_deg=None, 

870 bondcombos=None, 

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

872 """ 

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

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

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

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

877 

878 Parameters 

879 ---------- 

880 bonds: nested python list, optional 

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

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

883 

884 angles_deg: nested python list, optional 

885 List with targetvalue and atom indices defining the fixedangles, 

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

887 

888 dihedrals_deg: nested python list, optional 

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

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

891 

892 bondcombos: nested python list, optional 

893 List with targetvalue, atom indices and linear coefficient defining 

894 the fixed linear combination of bonds, 

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

896 [index1, index2, coefficient_for_bond]]], ...] 

897 

898 mic: bool, optional, default: False 

899 Minimum image convention. 

900 

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

902 Convergence criterion. 

903 """ 

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

905 if angles: 

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

907 angles = np.asarray(angles) 

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

909 angles = angles.tolist() 

910 else: 

911 angles = angles_deg 

912 if dihedrals: 

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

914 dihedrals = np.asarray(dihedrals) 

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

916 dihedrals = dihedrals.tolist() 

917 else: 

918 dihedrals = dihedrals_deg 

919 

920 self.bonds = bonds or [] 

921 self.angles = angles or [] 

922 self.dihedrals = dihedrals or [] 

923 self.bondcombos = bondcombos or [] 

924 self.mic = mic 

925 self.epsilon = epsilon 

926 

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

928 + len(self.bondcombos)) 

929 

930 # Initialize these at run-time: 

931 self.constraints = [] 

932 self.initialized = False 

933 

934 def get_removed_dof(self, atoms): 

935 return self.n 

936 

937 def initialize(self, atoms): 

938 if self.initialized: 

939 return 

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

941 cell = None 

942 pbc = None 

943 if self.mic: 

944 cell = atoms.cell 

945 pbc = atoms.pbc 

946 self.constraints = [] 

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

948 (self.angles, self.FixAngle), 

949 (self.dihedrals, self.FixDihedral), 

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

951 for datum in data: 

952 targetvalue = datum[0] 

953 if targetvalue is None: # set to current value 

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

955 self.mic) 

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

957 self.constraints.append(constr) 

958 self.initialized = True 

959 

960 @staticmethod 

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

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

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

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

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

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

967 return c 

968 

969 def get_subconstraint(self, atoms, definition): 

970 """Get pointer to a specific subconstraint. 

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

972 self.initialize(atoms) 

973 for subconstr in self.constraints: 

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

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

976 subconstr.coefs)] 

977 if defin == definition: 

978 return subconstr 

979 else: # identify primitive constraints by their indices 

980 if subconstr.indices == [definition]: 

981 return subconstr 

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

983 

984 def shuffle_definitions(self, shuffle_dic, internal_type): 

985 dfns = [] # definitions 

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

987 append = True 

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

989 for old in dfn[1]: 

990 if old in shuffle_dic: 

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

992 else: 

993 append = False 

994 break 

995 if append: 

996 dfns.append(new_dfn) 

997 return dfns 

998 

999 def shuffle_combos(self, shuffle_dic, internal_type): 

1000 dfns = [] # definitions 

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

1002 append = True 

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

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

1005 for i, indices in enumerate(all_indices): 

1006 for old in indices: 

1007 if old in shuffle_dic: 

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

1009 else: 

1010 append = False 

1011 break 

1012 if not append: 

1013 break 

1014 if append: 

1015 dfns.append(new_dfn) 

1016 return dfns 

1017 

1018 def index_shuffle(self, atoms, ind): 

1019 # See docstring of superclass 

1020 self.initialize(atoms) 

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

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

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

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

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

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

1027 self.initialized = False 

1028 self.initialize(atoms) 

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

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

1031 

1032 def get_indices(self): 

1033 cons = [] 

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

1035 cons.extend(dfn[1]) 

1036 for dfn in self.bondcombos: 

1037 for partial_dfn in dfn[1]: 

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

1039 return list(set(cons)) 

1040 

1041 def todict(self): 

1042 return {'name': 'FixInternals', 

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

1044 'angles_deg': self.angles, 

1045 'dihedrals_deg': self.dihedrals, 

1046 'bondcombos': self.bondcombos, 

1047 'mic': self.mic, 

1048 'epsilon': self.epsilon}} 

1049 

1050 def adjust_positions(self, atoms, newpos): 

1051 self.initialize(atoms) 

1052 for constraint in self.constraints: 

1053 constraint.setup_jacobian(atoms.positions) 

1054 for j in range(50): 

1055 maxerr = 0.0 

1056 for constraint in self.constraints: 

1057 constraint.adjust_positions(atoms.positions, newpos) 

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

1059 if maxerr < self.epsilon: 

1060 return 

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

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

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

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

1065 ' Support for planar angles would require the' 

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

1067 ' See issue #868.') 

1068 raise ValueError(msg) 

1069 

1070 def adjust_forces(self, atoms, forces): 

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

1072 self.initialize(atoms) 

1073 positions = atoms.positions 

1074 N = len(forces) 

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

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

1077 

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

1079 

1080 tx[:, 0] = 1.0 

1081 ty[:, 1] = 1.0 

1082 tz[:, 2] = 1.0 

1083 ff = forces.ravel() 

1084 

1085 # Calculate the center of mass 

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

1087 

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

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

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

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

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

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

1094 

1095 # Normalizing transl., rotat. constraints 

1096 for r in list2_constraints: 

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

1098 

1099 # Add all angle, etc. constraint vectors 

1100 for constraint in self.constraints: 

1101 constraint.setup_jacobian(positions) 

1102 constraint.adjust_forces(positions, forces) 

1103 list_constraints.insert(0, constraint.jacobian) 

1104 # QR DECOMPOSITION - GRAM SCHMIDT 

1105 

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

1107 aa = np.column_stack(list_constraints) 

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

1109 # Projection 

1110 hh = [] 

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

1112 hh.append(aa[:, i] * np.row_stack(aa[:, i])) 

1113 

1114 txx = aa[:, self.n] * np.row_stack(aa[:, self.n]) 

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

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

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

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

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

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

1121 for vec in hh: 

1122 T += vec 

1123 ff = np.dot(T, np.row_stack(ff)) 

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

1125 

1126 def __repr__(self): 

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

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

1129 

1130 # Classes for internal use in FixInternals 

1131 class FixInternalsBase: 

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

1133 

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

1135 self.targetvalue = targetvalue # constant target value 

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

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

1138 self.masses = masses 

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

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

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

1142 self.cell = cell 

1143 self.pbc = pbc 

1144 

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

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

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

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

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

1150 for j in range(n): 

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

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

1153 return self.coefs @ jacobian 

1154 

1155 def finalize_positions(self, newpos): 

1156 jacobian = self.jacobian / self.masses 

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

1158 dnewpos = lamda * jacobian 

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

1160 

1161 def adjust_forces(self, positions, forces): 

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

1163 * self.jacobian) 

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

1165 

1166 class FixBondCombo(FixInternalsBase): 

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

1168 within FixInternals. 

1169 

1170 sum_i( coef_i * bond_length_i ) = constant 

1171 """ 

1172 

1173 def get_jacobian(self, pos): 

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

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

1176 pbc=self.pbc) 

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

1178 

1179 def setup_jacobian(self, pos): 

1180 self.jacobian = self.get_jacobian(pos) 

1181 

1182 def adjust_positions(self, oldpos, newpos): 

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

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

1185 cell=self.cell, 

1186 pbc=self.pbc) 

1187 value = self.coefs @ dists 

1188 self.sigma = value - self.targetvalue 

1189 self.finalize_positions(newpos) 

1190 

1191 @staticmethod 

1192 def get_value(atoms, indices, mic): 

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

1194 

1195 def __repr__(self): 

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

1197 '{self.coefs})') 

1198 

1199 class FixBondLengthAlt(FixBondCombo): 

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

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

1202 

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

1204 if targetvalue <= 0.: 

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

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

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

1208 

1209 @staticmethod 

1210 def get_value(atoms, indices, mic): 

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

1212 

1213 def __repr__(self): 

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

1215 

1216 class FixAngle(FixInternalsBase): 

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

1218 

1219 Convergence is potentially problematic for angles very close to 

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

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

1222 """ 

1223 

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

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

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

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

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

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

1230 

1231 def gather_vectors(self, pos): 

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

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

1234 return v0, v1 

1235 

1236 def get_jacobian(self, pos): 

1237 v0, v1 = self.gather_vectors(pos) 

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

1239 pbc=self.pbc) 

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

1241 

1242 def setup_jacobian(self, pos): 

1243 self.jacobian = self.get_jacobian(pos) 

1244 

1245 def adjust_positions(self, oldpos, newpos): 

1246 v0, v1 = self.gather_vectors(newpos) 

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

1248 self.sigma = value - self.targetvalue 

1249 self.finalize_positions(newpos) 

1250 

1251 @staticmethod 

1252 def get_value(atoms, indices, mic): 

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

1254 

1255 def __repr__(self): 

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

1257 

1258 class FixDihedral(FixInternalsBase): 

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

1260 

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

1262 becomes planar. Make sure to avoid this situation. 

1263 """ 

1264 

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

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

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

1268 

1269 def gather_vectors(self, pos): 

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

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

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

1273 return v0, v1, v2 

1274 

1275 def get_jacobian(self, pos): 

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

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

1278 pbc=self.pbc) 

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

1280 

1281 def setup_jacobian(self, pos): 

1282 self.jacobian = self.get_jacobian(pos) 

1283 

1284 def adjust_positions(self, oldpos, newpos): 

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

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

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

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

1289 self.finalize_positions(newpos) 

1290 

1291 @staticmethod 

1292 def get_value(atoms, indices, mic): 

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

1294 

1295 def __repr__(self): 

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

1297 

1298 

1299class FixParametricRelations(FixConstraint): 

1300 

1301 def __init__( 

1302 self, 

1303 indices, 

1304 Jacobian, 

1305 const_shift, 

1306 params=None, 

1307 eps=1e-12, 

1308 use_cell=False, 

1309 ): 

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

1311 space defined by the Jacobian 

1312 

1313 These constraints are based off the work in: 

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

1315 

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

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

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

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

1320 degrees of freedom onto the reduced parameter space. 

1321 

1322 Currently the constraint is set up to handle either atomic 

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

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

1325 to keep the mathematics behind the operations separate. 

1326 

1327 It would be possible to extend these constraints to allow 

1328 non-linear transformations if functionality to update the 

1329 Jacobian at each position update was included. This would 

1330 require passing an update function evaluate it every time 

1331 adjust_positions is callled. This is currently NOT supported, 

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

1333 

1334 Args: 

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

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

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

1338 The Jacobian describing 

1339 the parameter space transformation 

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

1341 A vector describing the constant term 

1342 in the transformation not accounted for in the Jacobian 

1343 params (list of str): 

1344 parameters used in the parametric representation 

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

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

1347 numbers and set the precision used 

1348 to generate the constraint expressions 

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

1350 

1351 """ 

1352 self.indices = np.array(indices) 

1353 self.Jacobian = np.array(Jacobian) 

1354 self.const_shift = np.array(const_shift) 

1355 

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

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

1358 

1359 self.eps = eps 

1360 self.use_cell = use_cell 

1361 

1362 if params is None: 

1363 params = [] 

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

1365 int_fmt_str = "{:0" + \ 

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

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

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

1369 else: 

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

1371 

1372 self.params = params 

1373 

1374 self.Jacobian_inv = np.linalg.inv( 

1375 self.Jacobian.T @ self.Jacobian) @ self.Jacobian.T 

1376 

1377 @classmethod 

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

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

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

1381 vector and constructs a FixParametricRelations constraint 

1382 

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

1384 elements must be ordered as: 

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

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

1387 component of the atomic position/lattice 

1388 vector. Currently only linear operations are allowed to be 

1389 included in the expressions so 

1390 only terms like: 

1391 - const * param_0 

1392 - sqrt[const] * param_1 

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

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

1395 the parameters passed in 

1396 params, are allowed. 

1397 

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

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

1400 expressions = [ 

1401 "1.0/3.0", "2.0/3.0", "z1", 

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

1403 "1.0/3.0", "2.0/3.0", "z2", 

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

1405 ] 

1406 

1407 For diamond are: 

1408 params = [] 

1409 expressions = [ 

1410 "0.0", "0.0", "0.0", 

1411 "0.25", "0.25", "0.25", 

1412 ], 

1413 

1414 and for stannite are 

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

1416 expressions = [ 

1417 "0.0", "0.0", "0.0", 

1418 "0.0", "0.5", "0.5", 

1419 "0.75", "0.25", "0.5", 

1420 "0.25", "0.75", "0.5", 

1421 "x4 + z4", "x4 + z4", "2*x4", 

1422 "x4 - z4", "x4 - z4", "-2*x4", 

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

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

1425 ] 

1426 

1427 Args: 

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

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

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

1431 parametric representation 

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

1433 parametric to the real space representation 

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

1435 numbers and set the precision used 

1436 to generate the constraint expressions 

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

1438 

1439 Returns: 

1440 cls( 

1441 indices, 

1442 Jacobian generated from expressions, 

1443 const_shift generated from expressions, 

1444 params, 

1445 eps-12, 

1446 use_cell, 

1447 ) 

1448 """ 

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

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

1451 

1452 for expr_ind, expression in enumerate(expressions): 

1453 expression = expression.strip() 

1454 

1455 # Convert subtraction to addition 

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

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

1458 expression = expression[1:] 

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

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

1461 

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

1463 # param_11 does not become 0.01 

1464 int_fmt_str = "{:0" + \ 

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

1466 

1467 param_dct = {} 

1468 param_map = {} 

1469 

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

1471 for param_ind, param in enumerate(params): 

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

1473 param_map[param] = param_str 

1474 param_dct[param_str] = 0.0 

1475 

1476 # Replace the parameters according to the map 

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

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

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

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

1481 

1482 # Partial linearity check 

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

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

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

1486 if n_params_in_sec > 1: 

1487 raise ValueError( 

1488 "FixParametricRelations expressions must be linear.") 

1489 

1490 const_shift[expr_ind] = float( 

1491 eval_expression(expression, param_dct)) 

1492 

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

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

1495 if param_str not in expression: 

1496 Jacobian[expr_ind, param_ind] = 0.0 

1497 continue 

1498 param_dct[param_str] = 1.0 

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

1500 test_1 -= const_shift[expr_ind] 

1501 Jacobian[expr_ind, param_ind] = test_1 

1502 

1503 param_dct[param_str] = 2.0 

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

1505 test_2 -= const_shift[expr_ind] 

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

1507 raise ValueError( 

1508 "FixParametricRelations expressions must be linear.") 

1509 param_dct[param_str] = 0.0 

1510 

1511 args = [ 

1512 indices, 

1513 Jacobian, 

1514 const_shift, 

1515 params, 

1516 eps, 

1517 use_cell, 

1518 ] 

1519 if cls is FixScaledParametricRelations: 

1520 args = args[:-1] 

1521 return cls(*args) 

1522 

1523 @property 

1524 def expressions(self): 

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

1526 and self.const_shift objects""" 

1527 expressions = [] 

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

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

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

1531 exp = "" 

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

1533 shift_val) > self.eps: 

1534 exp += fmt_str.format(shift_val) 

1535 

1536 param_exp = "" 

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

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

1539 if abs_jacob_val < self.eps: 

1540 continue 

1541 

1542 param = self.params[param_index] 

1543 if param_exp or exp: 

1544 if jacob_val > -1.0 * self.eps: 

1545 param_exp += " + " 

1546 else: 

1547 param_exp += " - " 

1548 elif (not exp) and (not param_exp) and ( 

1549 jacob_val < -1.0 * self.eps): 

1550 param_exp += "-" 

1551 

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

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

1554 else: 

1555 param_exp += (fmt_str + 

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

1557 

1558 exp += param_exp 

1559 

1560 expressions.append(exp) 

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

1562 

1563 def todict(self): 

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

1565 return { 

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

1567 "kwargs": { 

1568 "indices": self.indices, 

1569 "params": self.params, 

1570 "Jacobian": self.Jacobian, 

1571 "const_shift": self.const_shift, 

1572 "eps": self.eps, 

1573 "use_cell": self.use_cell, 

1574 } 

1575 } 

1576 

1577 def __repr__(self): 

1578 """The str representation of the constraint""" 

1579 if len(self.indices) > 1: 

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

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

1582 else: 

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

1584 

1585 if len(self.params) > 1: 

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

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

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

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

1590 else: 

1591 params_str = "[]" 

1592 

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

1594 type(self).__name__, 

1595 indices_str, 

1596 params_str, 

1597 self.eps 

1598 ) 

1599 

1600 

1601class FixScaledParametricRelations(FixParametricRelations): 

1602 

1603 def __init__( 

1604 self, 

1605 indices, 

1606 Jacobian, 

1607 const_shift, 

1608 params=None, 

1609 eps=1e-12, 

1610 ): 

1611 """The fractional coordinate version of FixParametricRelations 

1612 

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

1614 coordinates use_cell is false""" 

1615 super().__init__( 

1616 indices, 

1617 Jacobian, 

1618 const_shift, 

1619 params, 

1620 eps, 

1621 False, 

1622 ) 

1623 

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

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

1626 with the unit transformation""" 

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

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

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

1630 

1631 return cell.cartesian_positions(scaled) 

1632 

1633 def adjust_positions(self, atoms, positions): 

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

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

1636 atoms.cell, 

1637 positions[self.indices], 

1638 self.const_shift, 

1639 ) 

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

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

1642 

1643 def adjust_B(self, cell, positions): 

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

1645 keep track of this change""" 

1646 fractional = cell.scaled_positions(positions) 

1647 wrapped_fractional = (fractional % 1.0) % 1.0 

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

1649 return cell.cartesian_positions(wrapped_fractional) 

1650 

1651 def adjust_momenta(self, atoms, momenta): 

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

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

1654 atoms.cell, 

1655 momenta[self.indices], 

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

1657 ) 

1658 

1659 def adjust_forces(self, atoms, forces): 

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

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

1662 # inverse transformations 

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

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

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

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

1667 

1668 jacobian = cart2frac_jacob @ self.Jacobian 

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

1670 

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

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

1673 

1674 def todict(self): 

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

1676 dct = super().todict() 

1677 del dct["kwargs"]["use_cell"] 

1678 return dct 

1679 

1680 

1681class FixCartesianParametricRelations(FixParametricRelations): 

1682 

1683 def __init__( 

1684 self, 

1685 indices, 

1686 Jacobian, 

1687 const_shift, 

1688 params=None, 

1689 eps=1e-12, 

1690 use_cell=False, 

1691 ): 

1692 """The Cartesian coordinate version of FixParametricRelations""" 

1693 super().__init__( 

1694 indices, 

1695 Jacobian, 

1696 const_shift, 

1697 params, 

1698 eps, 

1699 use_cell, 

1700 ) 

1701 

1702 def adjust_contravariant(self, vecs, B): 

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

1704 the unit transformation""" 

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

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

1707 return vecs 

1708 

1709 def adjust_positions(self, atoms, positions): 

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

1711 if self.use_cell: 

1712 return 

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

1714 positions[self.indices], 

1715 self.const_shift, 

1716 ) 

1717 

1718 def adjust_momenta(self, atoms, momenta): 

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

1720 if self.use_cell: 

1721 return 

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

1723 momenta[self.indices], 

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

1725 ) 

1726 

1727 def adjust_forces(self, atoms, forces): 

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

1729 if self.use_cell: 

1730 return 

1731 

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

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

1734 forces_reduced).reshape(-1, 3) 

1735 

1736 def adjust_cell(self, atoms, cell): 

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

1738 if not self.use_cell: 

1739 return 

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

1741 cell[self.indices], 

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

1743 ) 

1744 

1745 def adjust_stress(self, atoms, stress): 

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

1747 if not self.use_cell: 

1748 return 

1749 

1750 stress_3x3 = voigt_6_to_full_3x3_stress(stress) 

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

1752 stress_3x3[self.indices] = ( 

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

1754 

1755 stress[:] = full_3x3_to_voigt_6_stress(stress_3x3) 

1756 

1757 

1758class Hookean(FixConstraint): 

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

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

1761 

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

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

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

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

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

1767 distance above a plane. 

1768 

1769 a1 : int 

1770 Index of atom 1 

1771 a2 : one of three options 

1772 1) index of atom 2 

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

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

1775 k : float 

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

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

1778 rt : float 

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

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

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

1782 

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

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

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

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

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

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

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

1790 

1791 References: 

1792 

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

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

1795 """ 

1796 

1797 if isinstance(a2, int): 

1798 self._type = 'two atoms' 

1799 self.indices = [a1, a2] 

1800 elif len(a2) == 3: 

1801 self._type = 'point' 

1802 self.index = a1 

1803 self.origin = np.array(a2) 

1804 elif len(a2) == 4: 

1805 self._type = 'plane' 

1806 self.index = a1 

1807 self.plane = a2 

1808 else: 

1809 raise RuntimeError('Unknown type for a2') 

1810 self.threshold = rt 

1811 self.spring = k 

1812 

1813 def get_removed_dof(self, atoms): 

1814 return 0 

1815 

1816 def todict(self): 

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

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

1819 'k': self.spring} 

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

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

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

1823 elif self._type == 'point': 

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

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

1826 elif self._type == 'plane': 

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

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

1829 else: 

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

1831 return dct 

1832 

1833 def adjust_positions(self, atoms, newpositions): 

1834 pass 

1835 

1836 def adjust_momenta(self, atoms, momenta): 

1837 pass 

1838 

1839 def adjust_forces(self, atoms, forces): 

1840 positions = atoms.positions 

1841 if self._type == 'plane': 

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

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

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

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

1846 if d < 0: 

1847 return 

1848 magnitude = self.spring * d 

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

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

1851 return 

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

1853 p1, p2 = positions[self.indices] 

1854 elif self._type == 'point': 

1855 p1 = positions[self.index] 

1856 p2 = self.origin 

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

1858 bondlength = np.linalg.norm(displace) 

1859 if bondlength > self.threshold: 

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

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

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

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

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

1865 else: 

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

1867 

1868 def adjust_potential_energy(self, atoms): 

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

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

1871 potential energy.)""" 

1872 positions = atoms.positions 

1873 if self._type == 'plane': 

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

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

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

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

1878 if d > 0: 

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

1880 else: 

1881 return 0. 

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

1883 p1, p2 = positions[self.indices] 

1884 elif self._type == 'point': 

1885 p1 = positions[self.index] 

1886 p2 = self.origin 

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

1888 bondlength = np.linalg.norm(displace) 

1889 if bondlength > self.threshold: 

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

1891 else: 

1892 return 0. 

1893 

1894 def get_indices(self): 

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

1896 return self.indices 

1897 elif self._type == 'point': 

1898 return self.index 

1899 elif self._type == 'plane': 

1900 return self.index 

1901 

1902 def index_shuffle(self, atoms, ind): 

1903 # See docstring of superclass 

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

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

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

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

1908 if old == a: 

1909 newa[i] = new 

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

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

1912 self.indices = newa 

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

1914 newa = -1 # Signal error 

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

1916 if old == self.index: 

1917 newa = new 

1918 break 

1919 if newa == -1: 

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

1921 self.index = newa 

1922 

1923 def __repr__(self): 

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

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

1926 elif self._type == 'point': 

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

1928 else: 

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

1930 

1931 

1932class ExternalForce(FixConstraint): 

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

1934 

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

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

1937 between atom1-2 and atom3-4: 

1938 

1939 >>> from ase.build import bulk 

1940 

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

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

1943 >>> fext = 1.0 

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

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

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

1947 

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

1949 

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

1951 self.indices = [a1, a2] 

1952 self.external_force = f_ext 

1953 

1954 def get_removed_dof(self, atoms): 

1955 return 0 

1956 

1957 def adjust_positions(self, atoms, new): 

1958 pass 

1959 

1960 def adjust_forces(self, atoms, forces): 

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

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

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

1964 

1965 def adjust_potential_energy(self, atoms): 

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

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

1968 

1969 def index_shuffle(self, atoms, ind): 

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

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

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

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

1974 if old == a: 

1975 newa[i] = new 

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

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

1978 self.indices = newa 

1979 

1980 def __repr__(self): 

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

1982 self.indices[1], 

1983 self.external_force) 

1984 

1985 def todict(self): 

1986 return {'name': 'ExternalForce', 

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

1988 'f_ext': self.external_force}} 

1989 

1990 

1991class MirrorForce(FixConstraint): 

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

1993 

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

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

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

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

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

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

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

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

2002 during the optimization with *MirrorForce*. 

2003 

2004 Parameters 

2005 ---------- 

2006 a1: int 

2007 First atom index. 

2008 a2: int 

2009 Second atom index. 

2010 max_dist: float 

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

2012 can be found. 

2013 min_dist: float 

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

2015 can be found. 

2016 fmax: float 

2017 Maximum force used for the optimization. 

2018 

2019 Notes 

2020 ----- 

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

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

2023 between atom1-2 and atom3-4: 

2024 

2025 >>> from ase.build import bulk 

2026 

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

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

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

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

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

2032 

2033 """ 

2034 

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

2036 self.indices = [a1, a2] 

2037 self.min_dist = min_dist 

2038 self.max_dist = max_dist 

2039 self.fmax = fmax 

2040 

2041 def adjust_positions(self, atoms, new): 

2042 pass 

2043 

2044 def adjust_forces(self, atoms, forces): 

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

2046 d = np.linalg.norm(dist) 

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

2048 # Stop structure optimization 

2049 forces[:] *= 0 

2050 return 

2051 dist /= d 

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

2053 f = df.dot(dist) 

2054 con_saved = atoms.constraints 

2055 try: 

2056 con = [con for con in con_saved 

2057 if not isinstance(con, MirrorForce)] 

2058 atoms.set_constraint(con) 

2059 forces_copy = atoms.get_forces() 

2060 finally: 

2061 atoms.set_constraint(con_saved) 

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

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

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

2065 # would also be fixed 

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

2067 factor = 1. 

2068 else: 

2069 factor = 0. 

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

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

2072 

2073 def index_shuffle(self, atoms, ind): 

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

2075 

2076 """ 

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

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

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

2080 if old == a: 

2081 newa[i] = new 

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

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

2084 self.indices = newa 

2085 

2086 def __repr__(self): 

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

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

2089 self.fmax) 

2090 

2091 def todict(self): 

2092 return {'name': 'MirrorForce', 

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

2094 'max_dist': self.max_dist, 

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

2096 

2097 

2098class MirrorTorque(FixConstraint): 

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

2100 angle defined by four atoms. 

2101 

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

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

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

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

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

2107 to find the transition state. Transition states in 

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

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

2110 during the optimization with *MirrorTorque*. 

2111 

2112 This constraint can be used to find 

2113 transition states of cis-trans-isomerization. 

2114 

2115 a1 a4 

2116 | | 

2117 a2 __ a3 

2118 

2119 Parameters 

2120 ---------- 

2121 a1: int 

2122 First atom index. 

2123 a2: int 

2124 Second atom index. 

2125 a3: int 

2126 Third atom index. 

2127 a4: int 

2128 Fourth atom index. 

2129 max_angle: float 

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

2131 can be found. 

2132 min_angle: float 

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

2134 can be found. 

2135 fmax: float 

2136 Maximum force used for the optimization. 

2137 

2138 Notes 

2139 ----- 

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

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

2142 between atom1-4 and atom5-6: 

2143 

2144 >>> from ase.build import bulk 

2145 

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

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

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

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

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

2151 

2152 """ 

2153 

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

2155 fmax=0.1): 

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

2157 self.min_angle = min_angle 

2158 self.max_angle = max_angle 

2159 self.fmax = fmax 

2160 

2161 def adjust_positions(self, atoms, new): 

2162 pass 

2163 

2164 def adjust_forces(self, atoms, forces): 

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

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

2167 angle *= np.pi / 180. 

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

2169 # Stop structure optimization 

2170 forces[:] *= 0 

2171 return 

2172 p = atoms.positions[self.indices] 

2173 f = forces[self.indices] 

2174 

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

2176 ff = f - f0 

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

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

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

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

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

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

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

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

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

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

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

2188 omegap = omegap1 + omegap2 

2189 con_saved = atoms.constraints 

2190 try: 

2191 con = [con for con in con_saved 

2192 if not isinstance(con, MirrorTorque)] 

2193 atoms.set_constraint(con) 

2194 forces_copy = atoms.get_forces() 

2195 finally: 

2196 atoms.set_constraint(con_saved) 

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

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

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

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

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

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

2203 # mirrored torque would also be fixed 

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

2205 factor = 1. 

2206 else: 

2207 factor = 0. 

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

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

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

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

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

2213 

2214 def index_shuffle(self, atoms, ind): 

2215 # See docstring of superclass 

2216 indices = [] 

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

2218 if old in self.indices: 

2219 indices.append(new) 

2220 if len(indices) == 0: 

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

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

2223 

2224 def __repr__(self): 

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

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

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

2228 

2229 def todict(self): 

2230 return {'name': 'MirrorTorque', 

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

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

2233 'max_angle': self.max_angle, 

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

2235 

2236 

2237class Filter(FilterOld): 

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

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

2240 """ 

2241 .. deprecated:: 3.23.0 

2242 Import ``Filter`` from :mod:`ase.filters` 

2243 """ 

2244 super().__init__(*args, **kwargs) 

2245 

2246 

2247class StrainFilter(StrainFilterOld): 

2248 @deprecated('Import StrainFilter from ase.filters') 

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

2250 """ 

2251 .. deprecated:: 3.23.0 

2252 Import ``StrainFilter`` from :mod:`ase.filters` 

2253 """ 

2254 super().__init__(*args, **kwargs) 

2255 

2256 

2257class UnitCellFilter(UnitCellFilterOld): 

2258 @deprecated('Import UnitCellFilter from ase.filters') 

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

2260 """ 

2261 .. deprecated:: 3.23.0 

2262 Import ``UnitCellFilter`` from :mod:`ase.filters` 

2263 """ 

2264 super().__init__(*args, **kwargs) 

2265 

2266 

2267class ExpCellFilter(ExpCellFilterOld): 

2268 @deprecated('Import ExpCellFilter from ase.filters') 

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

2270 """ 

2271 .. deprecated:: 3.23.0 

2272 Import ``ExpCellFilter`` from :mod:`ase.filters` 

2273 or use :class:`~ase.filters.FrechetCellFilter` for better 

2274 convergence w.r.t. cell variables 

2275 """ 

2276 super().__init__(*args, **kwargs)