Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Implementation of the Precon abstract base class and subclasses 

3""" 

4 

5import sys 

6import time 

7import copy 

8import warnings 

9from abc import ABC, abstractmethod 

10 

11import numpy as np 

12from scipy import sparse 

13from scipy.sparse.linalg import spsolve 

14from scipy.interpolate import CubicSpline 

15 

16 

17from ase.constraints import Filter, FixAtoms 

18from ase.utils import longsum 

19from ase.geometry import find_mic 

20import ase.utils.ff as ff 

21import ase.units as units 

22from ase.optimize.precon.neighbors import (get_neighbours, 

23 estimate_nearest_neighbour_distance) 

24from ase.neighborlist import neighbor_list 

25 

26try: 

27 from pyamg import smoothed_aggregation_solver 

28 have_pyamg = True 

29 

30 def create_pyamg_solver(P, max_levels=15): 

31 return smoothed_aggregation_solver( 

32 P, B=None, 

33 strength=('symmetric', {'theta': 0.0}), 

34 smooth=( 

35 'jacobi', {'filter': True, 'weighting': 'local'}), 

36 improve_candidates=([('block_gauss_seidel', 

37 {'sweep': 'symmetric', 'iterations': 4})] + 

38 [None] * (max_levels - 1)), 

39 aggregate='standard', 

40 presmoother=('block_gauss_seidel', 

41 {'sweep': 'symmetric', 'iterations': 1}), 

42 postsmoother=('block_gauss_seidel', 

43 {'sweep': 'symmetric', 'iterations': 1}), 

44 max_levels=max_levels, 

45 max_coarse=300, 

46 coarse_solver='pinv') 

47 

48except ImportError: 

49 have_pyamg = False 

50 

51THz = 1e12 * 1. / units.s 

52 

53 

54class Precon(ABC): 

55 

56 @abstractmethod 

57 def make_precon(self, atoms, reinitialize=None): 

58 """ 

59 Create a preconditioner matrix based on the passed set of atoms. 

60 

61 Creates a general-purpose preconditioner for use with optimization 

62 algorithms, based on examining distances between pairs of atoms in the 

63 lattice. The matrix will be stored in the attribute self.P and 

64 returned. 

65 

66 Args: 

67 atoms: the Atoms object used to create the preconditioner. 

68  

69 reinitialize: if True, parameters of the preconditioner 

70 will be recalculated before the preconditioner matrix is 

71 created. If False, they will be calculated only when they 

72 do not currently have a value (ie, the first time this 

73 function is called). 

74 

75 Returns: 

76 P: A sparse scipy csr_matrix. BE AWARE that using 

77 numpy.dot() with sparse matrices will result in 

78 errors/incorrect results - use the .dot method directly 

79 on the matrix instead. 

80 """ 

81 ... 

82 

83 @abstractmethod 

84 def Pdot(self, x): 

85 """ 

86 Return the result of applying P to a vector x 

87 """ 

88 ... 

89 

90 def dot(self, x, y): 

91 """ 

92 Return the preconditioned dot product <P x, y> 

93 

94 Uses 128-bit floating point math for vector dot products 

95 """ 

96 return longsum(self.Pdot(x) * y) 

97 

98 def norm(self, x): 

99 """ 

100 Return the P-norm of x, where |x|_P = sqrt(<Px, x>) 

101 """ 

102 return np.sqrt(self.dot(x, x)) 

103 

104 def vdot(self, x, y): 

105 return self.dot(x.reshape(-1), 

106 y.reshape(-1)) 

107 

108 @abstractmethod 

109 def solve(self, x): 

110 """ 

111 Solve the (sparse) linear system P x = y and return y 

112 """ 

113 ... 

114 

115 def apply(self, forces, atoms): 

116 """ 

117 Convenience wrapper that combines make_precon() and solve() 

118 

119 Parameters 

120 ---------- 

121 forces: array 

122 (len(atoms)*3) array of input forces 

123 atoms: ase.atoms.Atoms 

124 

125 Returns 

126 ------- 

127 precon_forces: array 

128 (len(atoms), 3) array of preconditioned forces 

129 residual: float 

130 inf-norm of original forces, i.e. maximum absolute force 

131 """ 

132 self.make_precon(atoms) 

133 residual = np.linalg.norm(forces, np.inf) 

134 precon_forces = self.solve(forces) 

135 return precon_forces, residual 

136 

137 @abstractmethod 

138 def copy(self): 

139 ... 

140 

141 @abstractmethod 

142 def asarray(self): 

143 """ 

144 Array representation of preconditioner, as a dense matrix 

145 """ 

146 ... 

147 

148 

149class Logfile: 

150 def __init__(self, logfile=None): 

151 if isinstance(logfile, str): 

152 if logfile == "-": 

153 logfile = sys.stdout 

154 else: 

155 logfile = open(logfile, "a") 

156 self.logfile = logfile 

157 

158 def write(self, *args): 

159 if self.logfile is None: 

160 return 

161 self.logfile.write(*args) 

162 

163 

164class SparsePrecon(Precon): 

165 def __init__(self, r_cut=None, r_NN=None, 

166 mu=None, mu_c=None, 

167 dim=3, c_stab=0.1, force_stab=False, 

168 reinitialize=False, array_convention='C', 

169 solver="auto", solve_tol=1e-8, 

170 apply_positions=True, apply_cell=True, 

171 estimate_mu_eigmode=False, logfile=None, rng=None, 

172 neighbour_list=neighbor_list): 

173 """Initialise a preconditioner object based on passed parameters. 

174 

175 Parameters: 

176 r_cut: float. This is a cut-off radius. The preconditioner matrix 

177 will be created by considering pairs of atoms that are within a 

178 distance r_cut of each other. For a regular lattice, this is 

179 usually taken somewhere between the first- and second-nearest 

180 neighbour distance. If r_cut is not provided, default is 

181 2 * r_NN (see below) 

182 r_NN: nearest neighbour distance. If not provided, this is 

183 calculated 

184 from input structure. 

185 mu: float 

186 energy scale for position degreees of freedom. If `None`, mu 

187 is precomputed using finite difference derivatives. 

188 mu_c: float 

189 energy scale for cell degreees of freedom. Also precomputed 

190 if None. 

191 estimate_mu_eigmode: 

192 If True, estimates mu based on the lowest eigenmodes of 

193 unstabilised preconditioner. If False it uses the sine based 

194 approach. 

195 dim: int; dimensions of the problem 

196 c_stab: float. The diagonal of the preconditioner matrix will have 

197 a stabilisation constant added, which will be the value of 

198 c_stab times mu. 

199 force_stab: 

200 If True, always add the stabilisation to diagnonal, regardless 

201 of the presence of fixed atoms. 

202 reinitialize: if True, the value of mu will be recalculated when 

203 self.make_precon is called. This can be overridden in specific 

204 cases with reinitialize argument in self.make_precon. If it 

205 is set to True here, the value passed for mu will be 

206 irrelevant unless reinitialize is set to False the first time 

207 make_precon is called. 

208 array_convention: Either 'C' or 'F' for Fortran; this will change 

209 the preconditioner to reflect the ordering of the indices in 

210 the vector it will operate on. The C convention assumes the 

211 vector will be arranged atom-by-atom (ie [x1, y1, z1, x2, ...]) 

212 while the F convention assumes it will be arranged component 

213 by component (ie [x1, x2, ..., y1, y2, ...]). 

214 solver: One of "auto", "direct" or "pyamg", specifying whether to 

215 use a direct sparse solver or PyAMG to solve P x = y. 

216 Default is "auto" which uses PyAMG if available, falling 

217 back to sparse solver if not. solve_tol: tolerance used for 

218 PyAMG sparse linear solver, if available. 

219 apply_positions: bool 

220 if True, apply preconditioner to position DoF 

221 apply_cell: bool 

222 if True, apply preconditioner to cell DoF 

223 logfile: file object or str 

224 If *logfile* is a string, a file with that name will be opened. 

225 Use '-' for stdout. 

226 rng: None or np.random.RandomState instance 

227 Random number generator to use for initialising pyamg solver 

228 neighbor_list: function (optional). Optionally replace the built-in 

229 ASE neighbour list with an alternative with the same call 

230 signature, e.g. `matscipy.neighbours.neighbour_list`. 

231 

232 Raises: 

233 ValueError for problem with arguments 

234 

235 """ 

236 self.r_NN = r_NN 

237 self.r_cut = r_cut 

238 self.mu = mu 

239 self.mu_c = mu_c 

240 self.estimate_mu_eigmode = estimate_mu_eigmode 

241 self.c_stab = c_stab 

242 self.force_stab = force_stab 

243 self.array_convention = array_convention 

244 self.reinitialize = reinitialize 

245 self.P = None 

246 self.old_positions = None 

247 

248 use_pyamg = False 

249 if solver == "auto": 

250 use_pyamg = have_pyamg 

251 elif solver == "direct": 

252 use_pyamg = False 

253 elif solver == "pyamg": 

254 if not have_pyamg: 

255 raise RuntimeError("solver='pyamg', PyAMG can't be imported!") 

256 use_pyamg = True 

257 else: 

258 raise ValueError('unknown solver - ' 

259 'should be "auto", "direct" or "pyamg"') 

260 

261 self.use_pyamg = use_pyamg 

262 self.solve_tol = solve_tol 

263 self.apply_positions = apply_positions 

264 self.apply_cell = apply_cell 

265 

266 if dim < 1: 

267 raise ValueError('Dimension must be at least 1') 

268 self.dim = dim 

269 self.logfile = Logfile(logfile) 

270 

271 if rng is None: 

272 rng = np.random.RandomState() 

273 self.rng = rng 

274 

275 self.neighbor_list = neighbor_list 

276 

277 def copy(self): 

278 return copy.deepcopy(self) 

279 

280 def Pdot(self, x): 

281 return self.P.dot(x) 

282 

283 def solve(self, x): 

284 start_time = time.time() 

285 if self.use_pyamg and have_pyamg: 

286 y = self.ml.solve(x, x0=self.rng.rand(self.P.shape[0]), 

287 tol=self.solve_tol, 

288 accel='cg', 

289 maxiter=300, 

290 cycle='W') 

291 else: 

292 y = spsolve(self.P, x) 

293 self.logfile.write('--- Precon applied in %s seconds ---\n' % 

294 (time.time() - start_time)) 

295 return y 

296 

297 def estimate_mu(self, atoms, H=None): 

298 r"""Estimate optimal preconditioner coefficient \mu 

299 

300 \mu is estimated from a numerical solution of 

301 

302 [dE(p+v) - dE(p)] \cdot v = \mu < P1 v, v > 

303 

304 with perturbation 

305 

306 v(x,y,z) = H P_lowest_nonzero_eigvec(x, y, z) 

307 

308 or 

309 

310 v(x,y,z) = H (sin(x / Lx), sin(y / Ly), sin(z / Lz)) 

311 

312 After the optimal \mu is found, self.mu will be set to its value. 

313 

314 If `atoms` is an instance of Filter an additional \mu_c 

315 will be computed for the cell degrees of freedom . 

316 

317 Args: 

318 atoms: Atoms object for initial system 

319 

320 H: 3x3 array or None 

321 Magnitude of deformation to apply. 

322 Default is 1e-2*rNN*np.eye(3) 

323 

324 Returns: 

325 mu : float 

326 mu_c : float or None 

327 """ 

328 logfile = self.logfile 

329 

330 if self.dim != 3: 

331 raise ValueError('Automatic calculation of mu only possible for ' 

332 'three-dimensional preconditioners. Try setting ' 

333 'mu manually instead.') 

334 

335 if self.r_NN is None: 

336 self.r_NN = estimate_nearest_neighbour_distance(atoms, 

337 self.neighbor_list) 

338 

339 # deformation matrix, default is diagonal 

340 if H is None: 

341 H = 1e-2 * self.r_NN * np.eye(3) 

342 

343 # compute perturbation 

344 p = atoms.get_positions() 

345 

346 if self.estimate_mu_eigmode: 

347 self.mu = 1.0 

348 self.mu_c = 1.0 

349 c_stab = self.c_stab 

350 self.c_stab = 0.0 

351 

352 if isinstance(atoms, Filter): 

353 n = len(atoms.atoms) 

354 else: 

355 n = len(atoms) 

356 self._make_sparse_precon(atoms, initial_assembly=True) 

357 self.P = self.P[:3 * n, :3 * n] 

358 eigvals, eigvecs = sparse.linalg.eigsh(self.P, k=4, which='SM') 

359 

360 logfile.write('estimate_mu(): lowest 4 eigvals = %f %f %f %f\n' % 

361 (eigvals[0], eigvals[1], eigvals[2], eigvals[3])) 

362 # check eigenvalues 

363 if any(eigvals[0:3] > 1e-6): 

364 raise ValueError('First 3 eigenvalues of preconditioner matrix' 

365 'do not correspond to translational modes.') 

366 elif eigvals[3] < 1e-6: 

367 raise ValueError('Fourth smallest eigenvalue of ' 

368 'preconditioner matrix ' 

369 'is too small, increase r_cut.') 

370 

371 x = np.zeros(n) 

372 for i in range(n): 

373 x[i] = eigvecs[:, 3][3 * i] 

374 x = x / np.linalg.norm(x) 

375 if x[0] < 0: 

376 x = -x 

377 

378 v = np.zeros(3 * len(atoms)) 

379 for i in range(n): 

380 v[3 * i] = x[i] 

381 v[3 * i + 1] = x[i] 

382 v[3 * i + 2] = x[i] 

383 v = v / np.linalg.norm(v) 

384 v = v.reshape((-1, 3)) 

385 

386 self.c_stab = c_stab 

387 else: 

388 Lx, Ly, Lz = [p[:, i].max() - p[:, i].min() for i in range(3)] 

389 logfile.write('estimate_mu(): Lx=%.1f Ly=%.1f Lz=%.1f\n' % 

390 (Lx, Ly, Lz)) 

391 

392 x, y, z = p.T 

393 # sine_vr = [np.sin(x/Lx), np.sin(y/Ly), np.sin(z/Lz)], but we need 

394 # to take into account the possibility that one of Lx/Ly/Lz is 

395 # zero. 

396 sine_vr = [x, y, z] 

397 

398 for i, L in enumerate([Lx, Ly, Lz]): 

399 if L == 0: 

400 warnings.warn( 

401 'Cell length L[%d] == 0. Setting H[%d,%d] = 0.' % 

402 (i, i, i)) 

403 H[i, i] = 0.0 

404 else: 

405 sine_vr[i] = np.sin(sine_vr[i] / L) 

406 

407 v = np.dot(H, sine_vr).T 

408 

409 natoms = len(atoms) 

410 if isinstance(atoms, Filter): 

411 natoms = len(atoms.atoms) 

412 eps = H / self.r_NN 

413 v[natoms:, :] = eps 

414 

415 v1 = v.reshape(-1) 

416 

417 # compute LHS 

418 dE_p = -atoms.get_forces().reshape(-1) 

419 atoms_v = atoms.copy() 

420 atoms_v.calc = atoms.calc 

421 if isinstance(atoms, Filter): 

422 atoms_v = atoms.__class__(atoms_v) 

423 if hasattr(atoms, 'constant_volume'): 

424 atoms_v.constant_volume = atoms.constant_volume 

425 atoms_v.set_positions(p + v) 

426 dE_p_plus_v = -atoms_v.get_forces().reshape(-1) 

427 

428 # compute left hand side 

429 LHS = (dE_p_plus_v - dE_p) * v1 

430 

431 # assemble P with \mu = 1 

432 self.mu = 1.0 

433 self.mu_c = 1.0 

434 

435 self._make_sparse_precon(atoms, initial_assembly=True) 

436 

437 # compute right hand side 

438 RHS = self.P.dot(v1) * v1 

439 

440 # use partial sums to compute separate mu for positions and cell DoFs 

441 self.mu = longsum(LHS[:3 * natoms]) / longsum(RHS[:3 * natoms]) 

442 if self.mu < 1.0: 

443 logfile.write('estimate_mu(): mu (%.3f) < 1.0, ' 

444 'capping at mu=1.0' % self.mu) 

445 self.mu = 1.0 

446 

447 if isinstance(atoms, Filter): 

448 self.mu_c = longsum(LHS[3 * natoms:]) / longsum(RHS[3 * natoms:]) 

449 if self.mu_c < 1.0: 

450 logfile.write('estimate_mu(): mu_c (%.3f) < 1.0, ' 

451 'capping at mu_c=1.0\n' % self.mu_c) 

452 self.mu_c = 1.0 

453 

454 logfile.write('estimate_mu(): mu=%r, mu_c=%r\n' % 

455 (self.mu, self.mu_c)) 

456 

457 self.P = None # force a rebuild with new mu (there may be fixed atoms) 

458 return (self.mu, self.mu_c) 

459 

460 def asarray(self): 

461 return np.array(self.P.todense()) 

462 

463 def one_dim_to_ndim(self, csc_P, N): 

464 """ 

465 Expand an N x N precon matrix to self.dim*N x self.dim * N 

466 

467 Args: 

468 csc_P (sparse matrix): N x N sparse matrix, in CSC format 

469 """ 

470 start_time = time.time() 

471 if self.dim == 1: 

472 P = csc_P 

473 elif self.array_convention == 'F': 

474 csc_P = csc_P.tocsr() 

475 P = csc_P 

476 for i in range(self.dim - 1): 

477 P = sparse.block_diag((P, csc_P)).tocsr() 

478 else: 

479 # convert back to triplet and read the arrays 

480 csc_P = csc_P.tocoo() 

481 i = csc_P.row * self.dim 

482 j = csc_P.col * self.dim 

483 z = csc_P.data 

484 

485 # N-dimensionalise, interlaced coordinates 

486 I = np.hstack([i + d for d in range(self.dim)]) 

487 J = np.hstack([j + d for d in range(self.dim)]) 

488 Z = np.hstack([z for d in range(self.dim)]) 

489 P = sparse.csc_matrix((Z, (I, J)), 

490 shape=(self.dim * N, self.dim * N)) 

491 P = P.tocsr() 

492 self.logfile.write('--- N-dim precon created in %s s ---\n' % 

493 (time.time() - start_time)) 

494 return P 

495 

496 def create_solver(self): 

497 if self.use_pyamg and have_pyamg: 

498 start_time = time.time() 

499 self.ml = create_pyamg_solver(self.P) 

500 self.logfile.write('--- multi grid solver created in %s ---\n' % 

501 (time.time() - start_time)) 

502 

503 

504class SparseCoeffPrecon(SparsePrecon): 

505 def _make_sparse_precon(self, atoms, initial_assembly=False, 

506 force_stab=False): 

507 """Create a sparse preconditioner matrix based on the passed atoms. 

508 

509 Creates a general-purpose preconditioner for use with optimization 

510 algorithms, based on examining distances between pairs of atoms in the 

511 lattice. The matrix will be stored in the attribute self.P and 

512 returned. Note that this function will use self.mu, whatever it is. 

513 

514 Args: 

515 atoms: the Atoms object used to create the preconditioner. 

516 

517 Returns: 

518 A scipy.sparse.csr_matrix object, representing a d*N by d*N matrix 

519 (where N is the number of atoms, and d is the value of self.dim). 

520 BE AWARE that using numpy.dot() with this object will result in 

521 errors/incorrect results - use the .dot method directly on the 

522 sparse matrix instead. 

523 

524 """ 

525 logfile = self.logfile 

526 logfile.write('creating sparse precon: initial_assembly=%r, ' 

527 'force_stab=%r, apply_positions=%r, apply_cell=%r\n' % 

528 (initial_assembly, force_stab, self.apply_positions, 

529 self.apply_cell)) 

530 

531 N = len(atoms) 

532 diag_i = np.arange(N, dtype=int) 

533 start_time = time.time() 

534 if self.apply_positions: 

535 # compute neighbour list 

536 i, j, rij, fixed_atoms = get_neighbours(atoms, self.r_cut, 

537 neighbor_list=self.neighbor_list) 

538 logfile.write('--- neighbour list created in %s s --- \n' % 

539 (time.time() - start_time)) 

540 

541 # compute entries in triplet format: without the constraints 

542 start_time = time.time() 

543 coeff = self.get_coeff(rij) 

544 diag_coeff = np.bincount(i, -coeff, minlength=N).astype(np.float64) 

545 if force_stab or len(fixed_atoms) == 0: 

546 logfile.write('adding stabilisation to precon') 

547 diag_coeff += self.mu * self.c_stab 

548 else: 

549 diag_coeff = np.ones(N) 

550 

551 # precon is mu_c * identity for cell DoF 

552 if isinstance(atoms, Filter): 

553 if self.apply_cell: 

554 diag_coeff[-3:] = self.mu_c 

555 else: 

556 diag_coeff[-3:] = 1.0 

557 logfile.write('--- computed triplet format in %s s ---\n' % 

558 (time.time() - start_time)) 

559 

560 if self.apply_positions and not initial_assembly: 

561 # apply the constraints 

562 start_time = time.time() 

563 mask = np.ones(N) 

564 mask[fixed_atoms] = 0.0 

565 coeff *= mask[i] * mask[j] 

566 diag_coeff[fixed_atoms] = 1.0 

567 logfile.write('--- applied fixed_atoms in %s s ---\n' % 

568 (time.time() - start_time)) 

569 

570 if self.apply_positions: 

571 # remove zeros 

572 start_time = time.time() 

573 inz = np.nonzero(coeff) 

574 i = np.hstack((i[inz], diag_i)) 

575 j = np.hstack((j[inz], diag_i)) 

576 coeff = np.hstack((coeff[inz], diag_coeff)) 

577 logfile.write('--- remove zeros in %s s ---\n' % 

578 (time.time() - start_time)) 

579 else: 

580 i = diag_i 

581 j = diag_i 

582 coeff = diag_coeff 

583 

584 # create an N x N precon matrix in compressed sparse column (CSC) format 

585 start_time = time.time() 

586 csc_P = sparse.csc_matrix((coeff, (i, j)), shape=(N, N)) 

587 logfile.write('--- created CSC matrix in %s s ---\n' % 

588 (time.time() - start_time)) 

589 

590 self.P = self.one_dim_to_ndim(csc_P, N) 

591 self.create_solver() 

592 

593 def make_precon(self, atoms, reinitialize=None): 

594 if self.r_NN is None: 

595 self.r_NN = estimate_nearest_neighbour_distance(atoms, 

596 self.neighbor_list) 

597 

598 if self.r_cut is None: 

599 # This is the first time this function has been called, and no 

600 # cutoff radius has been specified, so calculate it automatically. 

601 self.r_cut = 2.0 * self.r_NN 

602 elif self.r_cut < self.r_NN: 

603 warning = ('WARNING: r_cut (%.2f) < r_NN (%.2f), ' 

604 'increasing to 1.1*r_NN = %.2f' % (self.r_cut, 

605 self.r_NN, 

606 1.1 * self.r_NN)) 

607 warnings.warn(warning) 

608 self.r_cut = 1.1 * self.r_NN 

609 

610 if reinitialize is None: 

611 # The caller has not specified whether or not to recalculate mu, 

612 # so the Precon's setting is used. 

613 reinitialize = self.reinitialize 

614 

615 if self.mu is None: 

616 # Regardless of what the caller has specified, if we don't 

617 # currently have a value of mu, then we need one. 

618 reinitialize = True 

619 

620 if reinitialize: 

621 self.estimate_mu(atoms) 

622 

623 if self.P is not None: 

624 real_atoms = atoms 

625 if isinstance(atoms, Filter): 

626 real_atoms = atoms.atoms 

627 if self.old_positions is None: 

628 self.old_positions = real_atoms.positions 

629 displacement, _ = find_mic(real_atoms.positions - 

630 self.old_positions, 

631 real_atoms.cell, real_atoms.pbc) 

632 self.old_positions = real_atoms.get_positions() 

633 max_abs_displacement = abs(displacement).max() 

634 self.logfile.write('max(abs(displacements)) = %.2f A (%.2f r_NN)' % 

635 (max_abs_displacement, 

636 max_abs_displacement / self.r_NN)) 

637 if max_abs_displacement < 0.5 * self.r_NN: 

638 return 

639 

640 start_time = time.time() 

641 self._make_sparse_precon(atoms, force_stab=self.force_stab) 

642 self.logfile.write('--- Precon created in %s seconds --- \n' % 

643 (time.time() - start_time)) 

644 

645 @abstractmethod 

646 def get_coeff(self, r): 

647 ... 

648 

649 

650class Pfrommer(Precon): 

651 """ 

652 Use initial guess for inverse Hessian from Pfrommer et al. as a 

653 simple preconditioner 

654 

655 J. Comput. Phys. vol 131 p233-240 (1997) 

656 """ 

657 

658 def __init__(self, bulk_modulus=500 * units.GPa, phonon_frequency=50 * THz, 

659 apply_positions=True, apply_cell=True): 

660 """ 

661 Default bulk modulus is 500 GPa and default phonon frequency is 50 THz 

662 """ 

663 self.bulk_modulus = bulk_modulus 

664 self.phonon_frequency = phonon_frequency 

665 self.apply_positions = apply_positions 

666 self.apply_cell = apply_cell 

667 self.H0 = None 

668 

669 def make_precon(self, atoms, reinitialize=None): 

670 if self.H0 is not None: 

671 # only build H0 on first call 

672 return 

673 

674 variable_cell = False 

675 if isinstance(atoms, Filter): 

676 variable_cell = True 

677 atoms = atoms.atoms 

678 

679 # position DoF 

680 omega = self.phonon_frequency 

681 mass = atoms.get_masses().mean() 

682 block = np.eye(3) / (mass * omega**2) 

683 blocks = [block] * len(atoms) 

684 

685 # cell DoF 

686 if variable_cell: 

687 coeff = 1.0 

688 if self.apply_cell: 

689 coeff = 1.0 / (3 * self.bulk_modulus) 

690 blocks.append(np.diag([coeff] * 9)) 

691 

692 self.H0 = sparse.block_diag(blocks, format='csr') 

693 return 

694 

695 def Pdot(self, x): 

696 return self.H0.solve(x) 

697 

698 def solve(self, x): 

699 y = self.H0.dot(x) 

700 return y 

701 

702 def copy(self): 

703 return Pfrommer(self.bulk_modulus, 

704 self.phonon_frequency, 

705 self.apply_positions, 

706 self.apply_cell) 

707 

708 def asarray(self): 

709 return np.array(self.H0.todense()) 

710 

711 

712class IdentityPrecon(Precon): 

713 """ 

714 Dummy preconditioner which does not modify forces 

715 """ 

716 

717 def make_precon(self, atoms, reinitialize=None): 

718 self.atoms = atoms 

719 

720 def Pdot(self, x): 

721 return x 

722 

723 def solve(self, x): 

724 return x 

725 

726 def copy(self): 

727 return IdentityPrecon() 

728 

729 def asarray(self): 

730 return np.eye(3 * len(self.atoms)) 

731 

732 

733class C1(SparseCoeffPrecon): 

734 """Creates matrix by inserting a constant whenever r_ij is less than r_cut. 

735 """ 

736 

737 def __init__(self, r_cut=None, mu=None, mu_c=None, dim=3, c_stab=0.1, 

738 force_stab=False, 

739 reinitialize=False, array_convention='C', 

740 solver="auto", solve_tol=1e-9, 

741 apply_positions=True, apply_cell=True, logfile=None): 

742 super().__init__(r_cut=r_cut, mu=mu, mu_c=mu_c, 

743 dim=dim, c_stab=c_stab, 

744 force_stab=force_stab, 

745 reinitialize=reinitialize, 

746 array_convention=array_convention, 

747 solver=solver, solve_tol=solve_tol, 

748 apply_positions=apply_positions, 

749 apply_cell=apply_cell, 

750 logfile=logfile) 

751 

752 def get_coeff(self, r): 

753 return -self.mu * np.ones_like(r) 

754 

755 

756class Exp(SparseCoeffPrecon): 

757 """Creates matrix with values decreasing exponentially with distance. 

758 """ 

759 

760 def __init__(self, A=3.0, r_cut=None, r_NN=None, mu=None, mu_c=None, 

761 dim=3, c_stab=0.1, 

762 force_stab=False, reinitialize=False, array_convention='C', 

763 solver="auto", solve_tol=1e-9, 

764 apply_positions=True, apply_cell=True, 

765 estimate_mu_eigmode=False, logfile=None): 

766 """ 

767 Initialise an Exp preconditioner with given parameters. 

768 

769 Args: 

770 r_cut, mu, c_stab, dim, sparse, reinitialize, array_convention: see 

771 precon.__init__() 

772 A: coefficient in exp(-A*r/r_NN). Default is A=3.0. 

773 """ 

774 super().__init__(r_cut=r_cut, r_NN=r_NN, 

775 mu=mu, mu_c=mu_c, dim=dim, c_stab=c_stab, 

776 force_stab=force_stab, 

777 reinitialize=reinitialize, 

778 array_convention=array_convention, 

779 solver=solver, 

780 solve_tol=solve_tol, 

781 apply_positions=apply_positions, 

782 apply_cell=apply_cell, 

783 estimate_mu_eigmode=estimate_mu_eigmode, 

784 logfile=logfile) 

785 

786 self.A = A 

787 

788 def get_coeff(self, r): 

789 return -self.mu * np.exp(-self.A * (r / self.r_NN - 1)) 

790 

791 

792def ij_to_x(i, j): 

793 x = [3 * i, 3 * i + 1, 3 * i + 2, 

794 3 * j, 3 * j + 1, 3 * j + 2] 

795 return x 

796 

797 

798def ijk_to_x(i, j, k): 

799 x = [3 * i, 3 * i + 1, 3 * i + 2, 

800 3 * j, 3 * j + 1, 3 * j + 2, 

801 3 * k, 3 * k + 1, 3 * k + 2] 

802 return x 

803 

804 

805def ijkl_to_x(i, j, k, l): 

806 x = [3 * i, 3 * i + 1, 3 * i + 2, 

807 3 * j, 3 * j + 1, 3 * j + 2, 

808 3 * k, 3 * k + 1, 3 * k + 2, 

809 3 * l, 3 * l + 1, 3 * l + 2] 

810 return x 

811 

812 

813def apply_fixed(atoms, P): 

814 fixed_atoms = [] 

815 for constraint in atoms.constraints: 

816 if isinstance(constraint, FixAtoms): 

817 fixed_atoms.extend(list(constraint.index)) 

818 else: 

819 raise TypeError( 

820 'only FixAtoms constraints are supported by Precon class') 

821 if len(fixed_atoms) != 0: 

822 P = P.tolil() 

823 for i in fixed_atoms: 

824 P[i, :] = 0.0 

825 P[:, i] = 0.0 

826 P[i, i] = 1.0 

827 return P 

828 

829 

830class FF(SparsePrecon): 

831 """Creates matrix using morse/bond/angle/dihedral force field parameters. 

832 """ 

833 

834 def __init__(self, dim=3, c_stab=0.1, force_stab=False, 

835 array_convention='C', solver="auto", solve_tol=1e-9, 

836 apply_positions=True, apply_cell=True, 

837 hessian='spectral', morses=None, bonds=None, angles=None, 

838 dihedrals=None, logfile=None): 

839 """Initialise an FF preconditioner with given parameters. 

840 

841 Args: 

842 dim, c_stab, force_stab, array_convention, use_pyamg, solve_tol: 

843 see SparsePrecon.__init__() 

844 morses: Morse instance 

845 bonds: Bond instance 

846 angles: Angle instance 

847 dihedrals: Dihedral instance 

848 """ 

849 

850 if (morses is None and bonds is None and angles is None and 

851 dihedrals is None): 

852 raise ImportError( 

853 'At least one of morses, bonds, angles or dihedrals must be ' 

854 'defined!') 

855 

856 super().__init__(dim=dim, c_stab=c_stab, 

857 force_stab=force_stab, 

858 array_convention=array_convention, 

859 solver=solver, 

860 solve_tol=solve_tol, 

861 apply_positions=apply_positions, 

862 apply_cell=apply_cell, logfile=logfile) 

863 

864 self.hessian = hessian 

865 self.morses = morses 

866 self.bonds = bonds 

867 self.angles = angles 

868 self.dihedrals = dihedrals 

869 

870 def make_precon(self, atoms, reinitialize=None): 

871 start_time = time.time() 

872 self._make_sparse_precon(atoms, force_stab=self.force_stab) 

873 self.logfile.write('--- Precon created in %s seconds ---\n' 

874 % (time.time() - start_time)) 

875 

876 def add_morse(self, morse, atoms, row, col, data, conn=None): 

877 if self.hessian == 'reduced': 

878 i, j, Hx = ff.get_morse_potential_reduced_hessian( 

879 atoms, morse) 

880 elif self.hessian == 'spectral': 

881 i, j, Hx = ff.get_morse_potential_hessian( 

882 atoms, morse, spectral=True) 

883 else: 

884 raise NotImplementedError('Not implemented hessian') 

885 x = ij_to_x(i, j) 

886 row.extend(np.repeat(x, 6)) 

887 col.extend(np.tile(x, 6)) 

888 data.extend(Hx.flatten()) 

889 if conn is not None: 

890 conn[i, j] = True 

891 conn[j, i] = True 

892 

893 def add_bond(self, bond, atoms, row, col, data, conn=None): 

894 if self.hessian == 'reduced': 

895 i, j, Hx = ff.get_bond_potential_reduced_hessian( 

896 atoms, bond, self.morses) 

897 elif self.hessian == 'spectral': 

898 i, j, Hx = ff.get_bond_potential_hessian( 

899 atoms, bond, self.morses, spectral=True) 

900 else: 

901 raise NotImplementedError('Not implemented hessian') 

902 x = ij_to_x(i, j) 

903 row.extend(np.repeat(x, 6)) 

904 col.extend(np.tile(x, 6)) 

905 data.extend(Hx.flatten()) 

906 if conn is not None: 

907 conn[i, j] = True 

908 conn[j, i] = True 

909 

910 def add_angle(self, angle, atoms, row, col, data, conn=None): 

911 if self.hessian == 'reduced': 

912 i, j, k, Hx = ff.get_angle_potential_reduced_hessian( 

913 atoms, angle, self.morses) 

914 elif self.hessian == 'spectral': 

915 i, j, k, Hx = ff.get_angle_potential_hessian( 

916 atoms, angle, self.morses, spectral=True) 

917 else: 

918 raise NotImplementedError('Not implemented hessian') 

919 x = ijk_to_x(i, j, k) 

920 row.extend(np.repeat(x, 9)) 

921 col.extend(np.tile(x, 9)) 

922 data.extend(Hx.flatten()) 

923 if conn is not None: 

924 conn[i, j] = conn[i, k] = conn[j, k] = True 

925 conn[j, i] = conn[k, i] = conn[k, j] = True 

926 

927 def add_dihedral(self, dihedral, atoms, row, col, data, conn=None): 

928 if self.hessian == 'reduced': 

929 i, j, k, l, Hx = \ 

930 ff.get_dihedral_potential_reduced_hessian( 

931 atoms, dihedral, self.morses) 

932 elif self.hessian == 'spectral': 

933 i, j, k, l, Hx = ff.get_dihedral_potential_hessian( 

934 atoms, dihedral, self.morses, spectral=True) 

935 else: 

936 raise NotImplementedError('Not implemented hessian') 

937 x = ijkl_to_x(i, j, k, l) 

938 row.extend(np.repeat(x, 12)) 

939 col.extend(np.tile(x, 12)) 

940 data.extend(Hx.flatten()) 

941 if conn is not None: 

942 conn[i, j] = conn[i, k] = conn[i, l] = conn[ 

943 j, k] = conn[j, l] = conn[k, l] = True 

944 conn[j, i] = conn[k, i] = conn[l, i] = conn[ 

945 k, j] = conn[l, j] = conn[l, k] = True 

946 

947 def _make_sparse_precon(self, atoms, initial_assembly=False, 

948 force_stab=False): 

949 N = len(atoms) 

950 

951 row = [] 

952 col = [] 

953 data = [] 

954 

955 if self.morses is not None: 

956 for morse in self.morses: 

957 self.add_morse(morse, atoms, row, col, data) 

958 

959 if self.bonds is not None: 

960 for bond in self.bonds: 

961 self.add_bond(bond, atoms, row, col, data) 

962 

963 if self.angles is not None: 

964 for angle in self.angles: 

965 self.add_angle(angle, atoms, row, col, data) 

966 

967 if self.dihedrals is not None: 

968 for dihedral in self.dihedrals: 

969 self.add_dihedral(dihedral, atoms, row, col, data) 

970 

971 # add the diagonal 

972 row.extend(range(self.dim * N)) 

973 col.extend(range(self.dim * N)) 

974 data.extend([self.c_stab] * self.dim * N) 

975 

976 # create the matrix 

977 start_time = time.time() 

978 self.P = sparse.csc_matrix( 

979 (data, (row, col)), shape=(self.dim * N, self.dim * N)) 

980 self.logfile.write('--- created CSC matrix in %s s ---\n' % 

981 (time.time() - start_time)) 

982 

983 self.P = apply_fixed(atoms, self.P) 

984 self.P = self.P.tocsr() 

985 self.logfile.write('--- N-dim precon created in %s s ---\n' % 

986 (time.time() - start_time)) 

987 self.create_solver() 

988 

989 

990class Exp_FF(Exp, FF): 

991 """Creates matrix with values decreasing exponentially with distance. 

992 """ 

993 

994 def __init__(self, A=3.0, r_cut=None, r_NN=None, mu=None, mu_c=None, 

995 dim=3, c_stab=0.1, 

996 force_stab=False, reinitialize=False, array_convention='C', 

997 solver="auto", solve_tol=1e-9, 

998 apply_positions=True, apply_cell=True, 

999 estimate_mu_eigmode=False, 

1000 hessian='spectral', morses=None, bonds=None, angles=None, 

1001 dihedrals=None, logfile=None): 

1002 """Initialise an Exp+FF preconditioner with given parameters. 

1003 

1004 Args: 

1005 r_cut, mu, c_stab, dim, reinitialize, array_convention: see 

1006 precon.__init__() 

1007 A: coefficient in exp(-A*r/r_NN). Default is A=3.0. 

1008 """ 

1009 if (morses is None and bonds is None and angles is None and 

1010 dihedrals is None): 

1011 raise ImportError( 

1012 'At least one of morses, bonds, angles or dihedrals must ' 

1013 'be defined!') 

1014 

1015 SparsePrecon.__init__(self, r_cut=r_cut, r_NN=r_NN, 

1016 mu=mu, mu_c=mu_c, dim=dim, c_stab=c_stab, 

1017 force_stab=force_stab, 

1018 reinitialize=reinitialize, 

1019 array_convention=array_convention, 

1020 solver=solver, 

1021 solve_tol=solve_tol, 

1022 apply_positions=apply_positions, 

1023 apply_cell=apply_cell, 

1024 estimate_mu_eigmode=estimate_mu_eigmode, 

1025 logfile=logfile) 

1026 

1027 self.A = A 

1028 self.hessian = hessian 

1029 self.morses = morses 

1030 self.bonds = bonds 

1031 self.angles = angles 

1032 self.dihedrals = dihedrals 

1033 

1034 def make_precon(self, atoms, reinitialize=None): 

1035 if self.r_NN is None: 

1036 self.r_NN = estimate_nearest_neighbour_distance(atoms, 

1037 self.neighbor_list) 

1038 

1039 if self.r_cut is None: 

1040 # This is the first time this function has been called, and no 

1041 # cutoff radius has been specified, so calculate it automatically. 

1042 self.r_cut = 2.0 * self.r_NN 

1043 elif self.r_cut < self.r_NN: 

1044 warning = ('WARNING: r_cut (%.2f) < r_NN (%.2f), ' 

1045 'increasing to 1.1*r_NN = %.2f' % (self.r_cut, 

1046 self.r_NN, 

1047 1.1 * self.r_NN)) 

1048 warnings.warn(warning) 

1049 self.r_cut = 1.1 * self.r_NN 

1050 

1051 if reinitialize is None: 

1052 # The caller has not specified whether or not to recalculate mu, 

1053 # so the Precon's setting is used. 

1054 reinitialize = self.reinitialize 

1055 

1056 if self.mu is None: 

1057 # Regardless of what the caller has specified, if we don't 

1058 # currently have a value of mu, then we need one. 

1059 reinitialize = True 

1060 

1061 if reinitialize: 

1062 self.estimate_mu(atoms) 

1063 

1064 if self.P is not None: 

1065 real_atoms = atoms 

1066 if isinstance(atoms, Filter): 

1067 real_atoms = atoms.atoms 

1068 if self.old_positions is None: 

1069 self.old_positions = real_atoms.positions 

1070 displacement, _ = find_mic(real_atoms.positions - 

1071 self.old_positions, 

1072 real_atoms.cell, real_atoms.pbc) 

1073 self.old_positions = real_atoms.get_positions() 

1074 max_abs_displacement = abs(displacement).max() 

1075 self.logfile.write('max(abs(displacements)) = %.2f A (%.2f r_NN)' % 

1076 (max_abs_displacement, 

1077 max_abs_displacement / self.r_NN)) 

1078 if max_abs_displacement < 0.5 * self.r_NN: 

1079 return 

1080 

1081 # Create the preconditioner: 

1082 start_time = time.time() 

1083 self._make_sparse_precon(atoms, force_stab=self.force_stab) 

1084 self.logfile.write('--- Precon created in %s seconds ---\n' % 

1085 (time.time() - start_time)) 

1086 

1087 def _make_sparse_precon(self, atoms, initial_assembly=False, 

1088 force_stab=False): 

1089 """Create a sparse preconditioner matrix based on the passed atoms. 

1090 

1091 Args: 

1092 atoms: the Atoms object used to create the preconditioner. 

1093 

1094 Returns: 

1095 A scipy.sparse.csr_matrix object, representing a d*N by d*N matrix 

1096 (where N is the number of atoms, and d is the value of self.dim). 

1097 BE AWARE that using numpy.dot() with this object will result in 

1098 errors/incorrect results - use the .dot method directly on the 

1099 sparse matrix instead. 

1100 

1101 """ 

1102 self.logfile.write('creating sparse precon: initial_assembly=%r, ' 

1103 'force_stab=%r, apply_positions=%r, ' 

1104 'apply_cell=%r\n' % 

1105 (initial_assembly, force_stab, 

1106 self.apply_positions, self.apply_cell)) 

1107 

1108 N = len(atoms) 

1109 start_time = time.time() 

1110 if self.apply_positions: 

1111 # compute neighbour list 

1112 i_list, j_list, rij_list, fixed_atoms = get_neighbours( 

1113 atoms, self.r_cut, self.neighbor_list) 

1114 self.logfile.write('--- neighbour list created in %s s ---\n' % 

1115 (time.time() - start_time)) 

1116 

1117 row = [] 

1118 col = [] 

1119 data = [] 

1120 

1121 # precon is mu_c*identity for cell DoF 

1122 start_time = time.time() 

1123 if isinstance(atoms, Filter): 

1124 i = N - 3 

1125 j = N - 2 

1126 k = N - 1 

1127 x = ijk_to_x(i, j, k) 

1128 row.extend(x) 

1129 col.extend(x) 

1130 if self.apply_cell: 

1131 data.extend(np.repeat(self.mu_c, 9)) 

1132 else: 

1133 data.extend(np.repeat(self.mu_c, 9)) 

1134 self.logfile.write('--- computed triplet format in %s s ---\n' % 

1135 (time.time() - start_time)) 

1136 

1137 conn = sparse.lil_matrix((N, N), dtype=bool) 

1138 

1139 if self.apply_positions and not initial_assembly: 

1140 if self.morses is not None: 

1141 for morse in self.morses: 

1142 self.add_morse(morse, atoms, row, col, data, conn) 

1143 

1144 if self.bonds is not None: 

1145 for bond in self.bonds: 

1146 self.add_bond(bond, atoms, row, col, data, conn) 

1147 

1148 if self.angles is not None: 

1149 for angle in self.angles: 

1150 self.add_angle(angle, atoms, row, col, data, conn) 

1151 

1152 if self.dihedrals is not None: 

1153 for dihedral in self.dihedrals: 

1154 self.add_dihedral(dihedral, atoms, row, col, data, conn) 

1155 

1156 if self.apply_positions: 

1157 for i, j, rij in zip(i_list, j_list, rij_list): 

1158 if not conn[i, j]: 

1159 coeff = self.get_coeff(rij) 

1160 x = ij_to_x(i, j) 

1161 row.extend(x) 

1162 col.extend(x) 

1163 data.extend(3 * [-coeff] + 3 * [coeff]) 

1164 

1165 row.extend(range(self.dim * N)) 

1166 col.extend(range(self.dim * N)) 

1167 if initial_assembly: 

1168 data.extend([self.mu * self.c_stab] * self.dim * N) 

1169 else: 

1170 data.extend([self.c_stab] * self.dim * N) 

1171 

1172 # create the matrix 

1173 start_time = time.time() 

1174 self.P = sparse.csc_matrix( 

1175 (data, (row, col)), shape=(self.dim * N, self.dim * N)) 

1176 self.logfile.write('--- created CSC matrix in %s s ---\n' % 

1177 (time.time() - start_time)) 

1178 

1179 if not initial_assembly: 

1180 self.P = apply_fixed(atoms, self.P) 

1181 

1182 self.P = self.P.tocsr() 

1183 self.create_solver() 

1184 

1185 

1186def make_precon(precon, atoms=None, **kwargs): 

1187 """ 

1188 Construct preconditioner from a string and optionally build for Atoms 

1189 

1190 Parameters 

1191 ---------- 

1192 precon - one of 'C1', 'Exp', 'Pfrommer', 'FF', 'Exp_FF', 'ID', None 

1193 or an instance of a subclass of `ase.optimize.precon.Precon` 

1194  

1195 atoms - ase.atoms.Atoms instance, optional 

1196 If present, build apreconditioner for this Atoms object 

1197  

1198 **kwargs - additional keyword arguments to pass to Precon constructor 

1199 

1200 Returns 

1201 ------- 

1202 precon - instance of relevant subclass of `ase.optimize.precon.Precon` 

1203 """ 

1204 lookup = { 

1205 'C1': C1, 

1206 'Exp': Exp, 

1207 'Pfrommer': Pfrommer, 

1208 'FF': FF, 

1209 'Exp_FF': Exp_FF, 

1210 'ID': IdentityPrecon, 

1211 'IdentityPrecon': IdentityPrecon, 

1212 None: IdentityPrecon 

1213 } 

1214 if isinstance(precon, str) or precon is None: 

1215 cls = lookup[precon] 

1216 precon = cls(**kwargs) 

1217 if atoms is not None: 

1218 precon.make_precon(atoms) 

1219 return precon 

1220 

1221 

1222class SplineFit: 

1223 """ 

1224 Fit a cubic spline fit to images 

1225 """ 

1226 def __init__(self, s, x): 

1227 self._s = s 

1228 self._x_data = x 

1229 self._x = CubicSpline(self._s, x, bc_type='not-a-knot') 

1230 self._dx_ds = self._x.derivative() 

1231 self._d2x_ds2 = self._x.derivative(2) 

1232 

1233 @property 

1234 def s(self): 

1235 return self._s 

1236 

1237 @property 

1238 def x_data(self): 

1239 return self._x_data 

1240 

1241 @property 

1242 def x(self): 

1243 return self._x 

1244 

1245 @property 

1246 def dx_ds(self): 

1247 return self._dx_ds 

1248 

1249 @property 

1250 def d2x_ds2(self): 

1251 return self._d2x_ds2 

1252 

1253 

1254class PreconImages: 

1255 def __init__(self, precon, images, **kwargs): 

1256 """ 

1257 Wrapper for a list of Precon objects and associated images 

1258  

1259 This is used when preconditioning a NEB object. Equation references 

1260 refer to Paper IV in the :class:`ase.neb.NEB` documentation, i.e. 

1261  

1262 S. Makri, C. Ortner and J. R. Kermode, J. Chem. Phys. 

1263 150, 094109 (2019) 

1264 https://dx.doi.org/10.1063/1.5064465 

1265 

1266 Args: 

1267 precon (str or list): preconditioner(s) to use 

1268 images (list of Atoms): Atoms objects that define the state 

1269 

1270 """ 

1271 self.images = images 

1272 if isinstance(precon, list): 

1273 if len(precon) != len(images): 

1274 raise ValueError(f'length mismatch: len(precon)={len(precon)} ' 

1275 f'!= len(images)={len(images)}') 

1276 self.precon = precon 

1277 return 

1278 P0 = make_precon(precon, images[0], **kwargs) 

1279 self.precon = [P0] 

1280 for image in images[1:]: 

1281 P = P0.copy() 

1282 P.make_precon(image) 

1283 self.precon.append(P) 

1284 self._spline = None 

1285 

1286 def __len__(self): 

1287 return len(self.precon) 

1288 

1289 def __iter__(self): 

1290 return iter(self.precon) 

1291 

1292 def __getitem__(self, index): 

1293 return self.precon[index] 

1294 

1295 def apply(self, all_forces, index=None): 

1296 """Apply preconditioners to stored images 

1297 

1298 Args: 

1299 all_forces (array): forces on images, shape (nimages, natoms, 3) 

1300 index (slice, optional): Which images to include. Defaults to all. 

1301 

1302 Returns: 

1303 precon_forces: array of preconditioned forces 

1304 """ 

1305 if index is None: 

1306 index = slice(None) 

1307 precon_forces = [] 

1308 for precon, image, forces in zip(self.precon[index], 

1309 self.images[index], 

1310 all_forces): 

1311 f_vec = forces.reshape(-1) 

1312 pf_vec, _ = precon.apply(f_vec, image) 

1313 precon_forces.append(pf_vec.reshape(-1, 3)) 

1314 

1315 return np.array(precon_forces) 

1316 

1317 def average_norm(self, i, j, dx): 

1318 """Average norm between images i and j 

1319 

1320 Args: 

1321 i (int): left image 

1322 j (int): right image 

1323 dx (array): vector 

1324 

1325 Returns: 

1326 norm: norm of vector wrt average of precons at i and j 

1327 """ 

1328 return np.sqrt(0.5 * (self.precon[i].dot(dx, dx) + 

1329 self.precon[j].dot(dx, dx))) 

1330 

1331 def get_tangent(self, i): 

1332 """Normalised tangent vector at image i 

1333 

1334 Args: 

1335 i (int): image of interest 

1336 

1337 Returns: 

1338 tangent: tangent vector, normalised with appropriate precon norm 

1339 """ 

1340 tangent = self.spline.dx_ds(self.spline.s[i]) 

1341 tangent /= self.precon[i].norm(tangent) 

1342 return tangent.reshape(-1, 3) 

1343 

1344 def get_residual(self, i, imgforce): 

1345 # residuals computed according to eq. 11 in the paper 

1346 P_dot_imgforce = self.precon[i].Pdot(imgforce.reshape(-1)) 

1347 return np.linalg.norm(P_dot_imgforce, np.inf) 

1348 

1349 def get_spring_force(self, i, k1, k2, tangent): 

1350 """Spring force on image 

1351 

1352 Args: 

1353 i (int): image of interest 

1354 k1 (float): spring constant for left spring 

1355 k2 (float): spring constant for right spring 

1356 tangent (array): tangent vector, shape (natoms, 3) 

1357 

1358 Returns: 

1359 eta: NEB spring forces, shape (natoms, 3) 

1360 """ 

1361 # Definition following Eq. 9 in Paper IV 

1362 nimages = len(self.images) 

1363 k = 0.5 * (k1 + k2) / (nimages ** 2) 

1364 curvature = self.spline.d2x_ds2(self.spline.s[i]).reshape(-1, 3) 

1365 # complete Eq. 9 by including the spring force 

1366 eta = k * self.precon[i].vdot(curvature, tangent) * tangent 

1367 return eta 

1368 

1369 def get_coordinates(self, positions=None): 

1370 """Compute displacements wrt appropriate precon metric for each image 

1371  

1372 Args: 

1373 positions (list or array, optional) - images positions. 

1374 Shape either (nimages * natoms, 3) or ((nimages-2)*natoms, 3) 

1375 

1376 Returns: 

1377 s : array shape (nimages,), reaction coordinates, in range [0, 1] 

1378 x : array shape (nimages, 3 * natoms), flat displacement vectors 

1379 """ 

1380 nimages = len(self.precon) 

1381 natoms = len(self.images[0]) 

1382 d_P = np.zeros(nimages) 

1383 x = np.zeros((nimages, 3 * natoms)) # flattened positions 

1384 if positions is None: 

1385 positions = [image.positions for image in self.images] 

1386 elif isinstance(positions, np.ndarray) and len(positions.shape) == 2: 

1387 positions = positions.reshape(-1, natoms, 3) 

1388 positions = [positions[i, :, :] for i in range(len(positions))] 

1389 if len(positions) == len(self.images) - 2: 

1390 # prepend and append the non-moving images 

1391 positions = ([self.images[0].positions] + positions + 

1392 [self.images[-1].positions]) 

1393 assert len(positions) == len(self.images) 

1394 

1395 x[0, :] = positions[0].reshape(-1) 

1396 for i in range(1, nimages): 

1397 x[i, :] = positions[i].reshape(-1) 

1398 dx, _ = find_mic(positions[i] - positions[i - 1], 

1399 self.images[i - 1].cell, 

1400 self.images[i - 1].pbc) 

1401 dx = dx.reshape(-1) 

1402 d_P[i] = self.average_norm(i, i - 1, dx) 

1403 

1404 s = d_P.cumsum() / d_P.sum() # Eq. A1 in paper IV 

1405 return s, x 

1406 

1407 def spline_fit(self, positions=None): 

1408 """Fit 3 * natoms cubic splines as a function of reaction coordinate 

1409 

1410 Returns: 

1411 fit : :class:`ase.optimize.precon.SplineFit` object 

1412 """ 

1413 s, x = self.get_coordinates(positions) 

1414 return SplineFit(s, x) 

1415 

1416 @property 

1417 def spline(self): 

1418 s, x = self.get_coordinates() 

1419 if self._spline and (np.abs(s - self._old_s).max() < 1e-6 and 

1420 np.abs(x - self._old_x).max() < 1e-6): 

1421 return self._spline 

1422 

1423 self._spline = self.spline_fit() 

1424 self._old_s = s 

1425 self._old_x = x 

1426 return self._spline