Coverage for gpaw/directmin/derivatives.py: 92%

660 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-07-14 00:18 +0000

1import numpy as np 

2from gpaw.directmin.tools import get_n_occ, get_indices, random_a, \ 

3 sort_orbitals_according_to_occ, sort_orbitals_according_to_energies 

4from ase.units import Hartree 

5from gpaw.mpi import world 

6from gpaw.io.logger import GPAWLogger 

7from gpaw.typing import RNG 

8from copy import deepcopy 

9from typing import Any, Dict, Union 

10 

11 

12class Derivatives: 

13 

14 def __init__(self, etdm, wfs, c_ref=None, a=None, 

15 update_c_ref=False, eps=1.0e-7, 

16 random_amat: Union[RNG, bool] = False): 

17 """ 

18 :param etdm: 

19 :param wfs: 

20 :param c_ref: reference orbitals C_ref 

21 :param a: skew-Hermitian matrix A 

22 :param update_c_ref: if True update reference orbitals 

23 :param eps: finite difference displacement 

24 :param random_amat: 

25 if True, use random matrix A with the `numpy.random` global RNG 

26 if an RNG, use random matrix A with said RNG 

27 """ 

28 

29 self.eps = eps 

30 

31 # initialize vectors of elements matrix A 

32 if a is None: 

33 if wfs.mode == 'lcao': 

34 self.a = {u: np.zeros_like(v, dtype=wfs.dtype) 

35 for u, v in etdm.a_vec_u.items()} 

36 else: 

37 self.a = {u: np.zeros_like(v, dtype=wfs.dtype) 

38 for u, v in etdm.U_k.items()} 

39 

40 if random_amat: 

41 extra_kwargs: Dict[str, Any] = {} 

42 if random_amat not in (True, ): # Explicitly specified RNG 

43 extra_kwargs.update(rng=random_amat) 

44 for kpt in wfs.kpt_u: 

45 u = etdm.kpointval(kpt) 

46 if wfs.mode == 'lcao': 

47 shape = etdm.a_vec_u[u].shape 

48 else: 

49 shape = etdm.U_k[u].shape 

50 a = random_a(shape, wfs.dtype, **extra_kwargs) 

51 wfs.gd.comm.broadcast(a, 0) 

52 self.a[u] = a 

53 

54 # initialize orbitals 

55 if c_ref is None and wfs.mode == 'lcao': 

56 self.c_ref = etdm.dm_helper.reference_orbitals 

57 else: 

58 self.c_ref = c_ref 

59 

60 # update ref orbitals if needed 

61 if update_c_ref: 

62 if wfs.mode == 'lcao': 

63 etdm.rotate_wavefunctions(wfs, self.a, self.c_ref) 

64 etdm.dm_helper.set_reference_orbitals(wfs, etdm.n_dim) 

65 self.c_ref = etdm.dm_helper.reference_orbitals 

66 self.a = {u: np.zeros_like(v, dtype=wfs.dtype) 

67 for u, v in etdm.a_vec_u.items()} 

68 else: 

69 etdm.rotate_wavefunctions(wfs, self.a) 

70 for kpt in wfs.kpt_u: 

71 k = etdm.kpointval(kpt) 

72 etdm.psit_knG[k] = kpt.psit_nG.copy() 

73 self.a = {u: np.zeros_like(v, dtype=wfs.dtype) 

74 for u, v in etdm.U_k.items()} 

75 

76 def get_analytical_derivatives(self, etdm, ham, wfs, dens, 

77 what2calc='gradient'): 

78 """ 

79 Calculate analytical gradient or approximation to the Hessian 

80 with respect to the elements of a skew-Hermitian matrix 

81 

82 :param etdm: 

83 :param ham: 

84 :param wfs: 

85 :param dens: 

86 :param what2calc: calculate gradient or Hessian 

87 :return: analytical gradient or Hessian 

88 """ 

89 

90 assert what2calc in ['gradient', 'hessian'] 

91 

92 if what2calc == 'gradient': 

93 # calculate analytical gradient 

94 if wfs.mode == 'lcao': 

95 analytical_der = etdm.get_energy_and_gradients( 

96 self.a, ham, wfs, dens, self.c_ref)[1] 

97 else: 

98 analytical_der = etdm.get_energy_and_gradients( 

99 self.a, wfs, dens, ham)[1] 

100 else: 

101 # Calculate analytical approximation to hessian 

102 if wfs.mode == 'lcao': 

103 ind_up = etdm.ind_up 

104 analytical_der = np.hstack([get_approx_analytical_hessian( 

105 kpt, etdm.dtype, 

106 ind_up[etdm.kpointval(kpt)]).copy() for kpt in wfs.kpt_u]) 

107 else: 

108 analytical_der = np.hstack([get_approx_analytical_hessian( 

109 kpt, etdm.dtype).copy() for kpt in wfs.kpt_u]) 

110 analytical_der = construct_real_hessian(analytical_der) 

111 analytical_der = np.diag(analytical_der) 

112 

113 return analytical_der 

114 

115 def get_numerical_derivatives(self, etdm, ham, wfs, dens, 

116 what2calc='gradient'): 

117 """ 

118 Calculate numerical gradient or Hessian with respect to 

119 the elements of a skew-Hermitian matrix using central finite 

120 differences 

121 

122 :param etdm: 

123 :param ham: 

124 :param wfs: 

125 :param dens: 

126 :param what2calc: calculate gradient or Hessian 

127 :return: numerical gradient or Hessian 

128 """ 

129 

130 assert what2calc in ['gradient', 'hessian'] 

131 

132 if wfs.mode == 'lcao': 

133 numerical_der = self.get_numerical_derivatives_lcao( 

134 etdm, ham, wfs, dens, what2calc=what2calc) 

135 else: 

136 if what2calc == 'gradient': 

137 numerical_der = self.get_numerical_gradient_fdpw( 

138 etdm, wfs, dens, ham, self.a) 

139 else: 

140 numerical_der = self.get_numerical_hessian_fdpw( 

141 etdm, wfs, dens, ham, self.a) 

142 

143 return numerical_der 

144 

145 def get_numerical_derivatives_lcao( 

146 self, etdm, ham, wfs, dens, what2calc='gradient'): 

147 

148 # total dimensionality if matrices are real 

149 dim = sum([len(a) for a in self.a.values()]) 

150 steps = [1.0, 1.0j] if etdm.dtype == complex else [1.0] 

151 use_energy_or_gradient = {'gradient': 0, 'hessian': 1} 

152 

153 matrix_exp = etdm.matrix_exp 

154 if what2calc == 'gradient': 

155 numerical_der = {u: np.zeros_like(v) 

156 for u, v in self.a.items()} 

157 else: 

158 numerical_der = np.zeros(shape=(len(steps) * dim, 

159 len(steps) * dim)) 

160 # have to use exact gradient when Hessian is calculated 

161 etdm.matrix_exp = 'egdecomp' 

162 

163 row = 0 

164 f = use_energy_or_gradient[what2calc] 

165 for step in steps: 

166 for kpt in wfs.kpt_u: 

167 u = etdm.kpointval(kpt) 

168 for i in range(len(self.a[u])): 

169 a = self.a[u][i] 

170 

171 self.a[u][i] = a + step * self.eps 

172 fplus = etdm.get_energy_and_gradients( 

173 self.a, ham, wfs, dens, self.c_ref)[f] 

174 

175 self.a[u][i] = a - step * self.eps 

176 fminus = etdm.get_energy_and_gradients( 

177 self.a, ham, wfs, dens, self.c_ref)[f] 

178 

179 derf = apply_central_finite_difference_approx( 

180 fplus, fminus, self.eps) 

181 

182 if what2calc == 'gradient': 

183 numerical_der[u][i] += step * derf 

184 else: 

185 numerical_der[row] = construct_real_hessian(derf) 

186 

187 row += 1 

188 self.a[u][i] = a 

189 

190 if what2calc == 'hessian': 

191 etdm.matrix_exp = matrix_exp 

192 

193 return numerical_der 

194 

195 def get_numerical_gradient_fdpw(self, etdm, wfs, dens, ham, A_s): 

196 dtype = etdm.dtype 

197 h = [self.eps, -self.eps] 

198 coef = [1.0, -1.0] 

199 Gr_n_x = {} 

200 Gr_n_y = {} 

201 

202 if dtype == complex: 

203 for kpt in wfs.kpt_u: 

204 k = etdm.n_kps * kpt.s + kpt.q 

205 dim = A_s[k].shape[0] 

206 iut = np.triu_indices(dim, 1) 

207 dim_gr = iut[0].shape[0] 

208 

209 for z in range(2): 

210 grad_num = np.zeros(shape=dim_gr, 

211 dtype=etdm.dtype) 

212 igr = 0 

213 for i, j in zip(*iut): 

214 A = A_s[k][i][j] 

215 for l in range(2): 

216 if z == 1: 

217 if i == j: 

218 A_s[k][i][j] = A + 1.0j * h[l] 

219 else: 

220 A_s[k][i][j] = A + 1.0j * h[l] 

221 A_s[k][j][i] = -np.conjugate( 

222 A + 1.0j * h[l]) 

223 else: 

224 if i == j: 

225 A_s[k][i][j] = A + 0.0j * h[l] 

226 else: 

227 A_s[k][i][j] = A + h[l] 

228 A_s[k][j][i] = -np.conjugate( 

229 A + h[l]) 

230 E = etdm.get_energy_and_gradients( 

231 A_s, wfs, dens, ham)[0] 

232 grad_num[igr] += E * coef[l] 

233 grad_num[igr] *= 1.0 / (2.0 * self.eps) 

234 if i == j: 

235 A_s[k][i][j] = A 

236 else: 

237 A_s[k][i][j] = A 

238 A_s[k][j][i] = -np.conjugate(A) 

239 igr += 1 

240 if z == 0: 

241 Gr_n_x[k] = grad_num.copy() 

242 else: 

243 Gr_n_y[k] = grad_num.copy() 

244 

245 Gr_n = {k: (Gr_n_x[k] + 1.0j * Gr_n_y[k]) for k in 

246 Gr_n_x.keys()} 

247 else: 

248 for kpt in wfs.kpt_u: 

249 k = etdm.n_kps * kpt.s + kpt.q 

250 dim = A_s[k].shape[0] 

251 iut = np.triu_indices(dim, 1) 

252 dim_gr = iut[0].shape[0] 

253 grad_num = np.zeros(shape=dim_gr, dtype=etdm.dtype) 

254 

255 igr = 0 

256 for i, j in zip(*iut): 

257 A = A_s[k][i][j] 

258 for l in range(2): 

259 A_s[k][i][j] = A + h[l] 

260 A_s[k][j][i] = -(A + h[l]) 

261 E = etdm.get_energy_and_gradients( 

262 A_s, wfs, dens, ham)[0] 

263 grad_num[igr] += E * coef[l] 

264 grad_num[igr] *= 1.0 / (2.0 * self.eps) 

265 A_s[k][i][j] = A 

266 A_s[k][j][i] = -A 

267 igr += 1 

268 

269 Gr_n_x[k] = grad_num.copy() 

270 

271 Gr_n = {k: (Gr_n_x[k]) for k in Gr_n_x.keys()} 

272 

273 return Gr_n 

274 

275 def get_numerical_hessian_fdpw(self, etdm, wfs, dens, ham, A_s): 

276 h = [self.eps, -self.eps] 

277 coef = [1.0, -1.0] 

278 num_hes = {} 

279 

280 for kpt in wfs.kpt_u: 

281 k = etdm.n_kps * kpt.s + kpt.q 

282 dim = A_s[k].shape[0] 

283 iut = np.tril_indices(dim, -1) 

284 dim_gr = iut[0].shape[0] 

285 hessian = np.zeros(shape=(dim_gr, dim_gr), 

286 dtype=etdm.dtype) 

287 ih = 0 

288 for i, j in zip(*iut): 

289 A = A_s[k][i][j] 

290 for l in range(2): 

291 A_s[k][i][j] = A + h[l] 

292 A_s[k][j][i] = -(A + h[l]) 

293 g = etdm.get_energy_and_gradients(A_s, wfs, dens, ham)[1] 

294 g = g[k][iut] 

295 hessian[ih, :] += g * coef[l] 

296 

297 hessian[ih, :] *= 1.0 / (2.0 * self.eps) 

298 

299 A_s[k][i][j] = A 

300 A_s[k][j][i] = -A 

301 ih += 1 

302 

303 num_hes[k] = hessian.copy() 

304 

305 return num_hes 

306 

307 

308class Davidson: 

309 """ 

310 Finite difference generalized Davidson partial diagonalizer to obtain a 

311 number of the eigenpairs with the smallest eigenvalues. 

312 

313 The following array indexation convention is used: 

314 

315 e: Target eigenpair 

316 w: Krylov subspace 

317 """ 

318 

319 def __init__(self, etdm, logfile=None, fd_mode=None, m=None, h=None, 

320 eps=None, cap_krylov=None, gmf=False, 

321 accurate_first_pdiag=True, remember_sp_order=None, 

322 sp_order=None, seed=None): 

323 """ 

324 :param etdm: ETDM object for which the partial eigendecomposition 

325 should be performed. 

326 :param logfile: Name string of the Davidson log file. Use '-' for 

327 stdout or None to discard. 

328 :param fd_mode: Finite difference mode for partial Hessian evaluation. 

329 Must be one of 'central' for central FD or 'forward' 

330 for forward FD. Central FD uses two e/g evaluations per 

331 Davidson step and target eigenpair with an error 

332 scaling as O(h^2), forward FD uses one with O(h) 

333 scaling. 

334 :param m: Memory parameter indicating how large the Krylov space should 

335 be able to become before resetting it with the Ritz vectors 

336 of the previous step or terminating the calculation if 

337 cap_krylov is True. 

338 :param h: Displacement (in radians of orbital rotation) for finite 

339 difference partial Hessian calculation. 

340 :param eps: Convergence threshold for maximum component of the 

341 residuals of the target eigenpairs. 

342 :param cap_krylov: If True, terminate the calculation if the Krylov 

343 space contains more than m vectors. 

344 :param gmf: Toggle usage with generalized mode following instead of 

345 stability analysis. The defaults and some actions will be 

346 different. 

347 :param accurate_first_pdiag: Approximate the target saddle point order 

348 better by performing a more accurate first 

349 partial diagonalization step at the 

350 expense of more gradient evaluations 

351 :param remember_sp_order: If True the number of target eigenpairs is 

352 saved after converging the partial Hessian 

353 eigendecomposition once and recovered for all 

354 subsequent calculations. If False the number 

355 of target eigenpairs is always gathered from 

356 the diagonal Hessian approximation in ETDM. 

357 :param sp_order: If given use this value for the number of target 

358 eigenpairs instead of gathering it from the diagonal 

359 Hessian approximation in ETDM. 

360 :param seed: Seed for random perturbation of initial Krylov space. 

361 """ 

362 

363 self.name = 'Davidson' 

364 self.gmf = gmf 

365 self.etdm = etdm 

366 self.fd_mode = fd_mode 

367 self.remember_sp_order = remember_sp_order 

368 self.sp_order = sp_order 

369 self.log_sp_order_once = True 

370 self.seed = seed 

371 self.V_w = [] # Krylov subspace 

372 self.C_we = [] # Preconditioner 

373 self.M = [] # Matrix used to get preconditioner every step 

374 self.W_w = [] # Hessian effect on Krylov subspace 

375 # Rayleigh matrix, smaller representation of the 

376 # diagonalization problem to solve: 

377 self.H_ww = [] 

378 self.lambda_e = [] # Target eigenvalues 

379 self.y_e = [] # Target eigenvectors in subspace representation 

380 self.x_e = [] # Target eigenvectors 

381 self.r_e = [] # Residuals of target eigenvectors 

382 self.t_e = [] # Krylov space extension vectors 

383 self.l = None # Number of target eigenpairs 

384 self.h = h 

385 self.m = m 

386 self.converged_e = False 

387 self.all_converged = False 

388 self.error_e = [] 

389 self.n_iter = 0 

390 self.eigenvalues = [] 

391 self.eigenvectors = [] 

392 self.reset = False 

393 self.eps = eps 

394 self.grad = None 

395 self.cap_krylov = cap_krylov 

396 self.dim_u = {} 

397 self.dimtot = 0 

398 self.nocc = {} 

399 self.nbands = 0 

400 self.c_ref = [] 

401 self.logfile = logfile 

402 self.logger = GPAWLogger(world) 

403 self.logger.fd = logfile 

404 self.first_run = accurate_first_pdiag 

405 self.lambda_w = [] # All eigenvalues 

406 self.y_w = [] # All eigenvectors in subspace representation 

407 self.x_w = [] # All eigenvectors 

408 self.check_inputs() 

409 

410 def check_inputs(self): 

411 defaults = self.set_defaults() 

412 assert self.etdm.name == 'etdm-lcao', 'Check etdm.' 

413 if self.logfile is not None: 

414 assert isinstance(self.logfile, str), 'Check logfile.' 

415 if self.fd_mode is None: 

416 self.fd_mode = defaults['fd_mode'] 

417 else: 

418 assert self.fd_mode in ['central', 'forward'], 'Check fd_mode.' 

419 if self.m is None: 

420 self.m = defaults['m'] 

421 else: 

422 assert isinstance(self.m, int) or np.isinf(self.m), 'Check m.' 

423 if self.h is None: 

424 self.h = defaults['h'] 

425 else: 

426 assert isinstance(self.h, float), 'Check h.' 

427 if self.eps is None: 

428 self.eps = defaults['eps'] 

429 else: 

430 assert isinstance(self.eps, float), 'Check eps.' 

431 if self.cap_krylov is None: 

432 self.cap_krylov = defaults['cap_krylov'] 

433 else: 

434 assert isinstance(self.cap_krylov, bool), 'Check cap_krylov.' 

435 if self.remember_sp_order is None: 

436 self.remember_sp_order = defaults['remember_sp_order'] 

437 else: 

438 assert isinstance(self.remember_sp_order, bool), \ 

439 'Check remember_sp_order.' 

440 if self.sp_order is not None: 

441 assert isinstance(self.sp_order, int), 'Check sp_order.' 

442 

443 def set_defaults(self): 

444 if self.gmf: 

445 return {'fd_mode': 'forward', 

446 'm': 300, 

447 'h': 1e-3, 

448 'eps': 1e-2, 

449 'cap_krylov': True, 

450 'remember_sp_order': True} 

451 else: 

452 return {'fd_mode': 'central', 

453 'm': np.inf, 

454 'h': 1e-3, 

455 'eps': 1e-3, 

456 'cap_krylov': False, 

457 'remember_sp_order': False} 

458 

459 def todict(self): 

460 return {'name': 'Davidson', 

461 'logfile': self.logfile, 

462 'fd_mode': self.fd_mode, 

463 'm': self.m, 

464 'h': self.h, 

465 'eps': self.eps, 

466 'cap_krylov': self.cap_krylov, 

467 'gmf': self.gmf, 

468 'remember_sp_order': self.remember_sp_order, 

469 'sp_order': self.sp_order} 

470 

471 def introduce(self): 

472 self.logger( 

473 '|-------------------------------------------------------|') 

474 self.logger( 

475 '| Davidson partial diagonalizer |') 

476 self.logger( 

477 '|-------------------------------------------------------|\n', 

478 flush=True) 

479 

480 def run(self, wfs, ham, dens, use_prev=False): 

481 self.initialize(wfs, use_prev) 

482 if not self.gmf: 

483 sort_orbitals_according_to_occ( 

484 wfs, self.etdm.constraints, update_mom=True) 

485 self.n_iter = 0 

486 self.c_ref = [deepcopy(wfs.kpt_u[x].C_nM) 

487 for x in range(len(wfs.kpt_u))] 

488 if self.fd_mode == 'forward' and self.grad is None: 

489 self.obtain_grad_at_c_ref(wfs, ham, dens) 

490 while not self.all_converged: 

491 self.iterate(wfs, ham, dens) 

492 if self.remember_sp_order: 

493 if self.sp_order is None: 

494 self.determine_sp_order_from_lambda() 

495 self.logger( 

496 'Saved target saddle point order as ' 

497 + str(self.sp_order) + ' for future partial ' 

498 'diagonalizations.', flush=True) 

499 elif self.log_sp_order_once: 

500 self.log_sp_order_once = False 

501 self.logger( 

502 'Using target saddle point order of ' 

503 + str(self.sp_order) + '.', flush=True) 

504 if self.gmf: 

505 self.obtain_x_in_krylov_subspace() 

506 for k, kpt in enumerate(wfs.kpt_u): 

507 kpt.C_nM = deepcopy(self.c_ref[k]) 

508 if not self.gmf: 

509 sort_orbitals_according_to_energies( 

510 ham, wfs, self.etdm.constraints) 

511 self.first_run = False 

512 

513 def obtain_grad_at_c_ref(self, wfs, ham, dens): 

514 a_vec_u = {} 

515 n_dim = {} 

516 for k, kpt in enumerate(wfs.kpt_u): 

517 n_dim[k] = wfs.bd.nbands 

518 a_vec_u[k] = np.zeros_like(self.etdm.a_vec_u[k]) 

519 self.grad = self.etdm.get_energy_and_gradients( 

520 a_vec_u, ham, wfs, dens, self.c_ref)[1] 

521 

522 def determine_sp_order_from_lambda(self): 

523 sp_order = 0 

524 for i in range(len(self.lambda_w)): 

525 if self.lambda_w[i] < 1e-8: 

526 sp_order += 1 

527 else: 

528 break 

529 self.sp_order = sp_order 

530 if self.sp_order == 0: 

531 self.sp_order = 1 

532 

533 def obtain_x_in_krylov_subspace(self): 

534 self.x_w = [] 

535 for i in range(len(self.lambda_w)): 

536 self.x_w.append( 

537 self.V_w[:, :len(self.lambda_w)] @ self.y_w[i].T) 

538 self.x_w = np.asarray(self.x_w).T 

539 

540 def initialize(self, wfs, use_prev=False): 

541 """ 

542 This is separate from __init__ since the initial Krylov space is 

543 obtained here every time a partial diagonalization is performed at 

544 different electronic coordinates. 

545 """ 

546 

547 dimz = 2 if self.etdm.dtype == complex else 1 

548 self.introduce() 

549 self.reset = False 

550 self.all_converged = False 

551 self.l = 0 

552 self.V_w = None 

553 self.nbands = wfs.bd.nbands 

554 appr_hess, appr_sp_order = self.estimate_spo_and_update_appr_hess( 

555 wfs, use_prev=use_prev) 

556 self.M = np.zeros(shape=self.dimtot * dimz) 

557 for i in range(self.dimtot * dimz): 

558 self.M[i] = np.real(appr_hess[i % self.dimtot]) 

559 if self.sp_order is not None: 

560 self.l = self.sp_order 

561 else: 

562 self.l = appr_sp_order if self.gmf else appr_sp_order + 2 

563 if self.l == 0: 

564 self.l = 1 

565 if self.l > self.dimtot * dimz: 

566 self.l = self.dimtot * dimz 

567 self.W_w = None 

568 self.error_e = [np.inf for x in range(self.l)] 

569 self.converged_e = [False for x in range(self.l)] 

570 self.form_initial_krylov_subspace( 

571 wfs, appr_hess, dimz, use_prev=use_prev) 

572 text = 'Davidson will target the ' + str(self.l) + ' lowest eigenpairs' 

573 if self.sp_order is None: 

574 text += '.' 

575 else: 

576 text += ' as recovered from previous calculation.' 

577 self.logger(text, flush=True) 

578 

579 def get_approximate_hessian_and_dim(self, wfs): 

580 appr_hess = [] 

581 self.dimtot = 0 

582 for k, kpt in enumerate(wfs.kpt_u): 

583 hdia = get_approx_analytical_hessian( 

584 kpt, self.etdm.dtype, ind_up=self.etdm.ind_up[k]) 

585 self.dim_u[k] = len(hdia) 

586 self.dimtot += len(hdia) 

587 appr_hess += list(hdia.copy()) 

588 self.nocc[k] = get_n_occ(kpt)[0] 

589 return appr_hess 

590 

591 def estimate_spo_and_update_appr_hess(self, wfs, use_prev=False): 

592 appr_sp_order = 0 

593 appr_hess = self.get_approximate_hessian_and_dim(wfs) 

594 if use_prev: 

595 for i in range(len(self.lambda_w)): 

596 if self.lambda_w[i] < -1e-4: 

597 appr_sp_order += 1 

598 if self.etdm.dtype == complex: 

599 appr_hess[i] \ 

600 = self.lambda_w[i] + 1.0j * self.lambda_w[i] 

601 else: 

602 appr_hess[i] = self.lambda_w[i] 

603 else: 

604 for i in range(len(appr_hess)): 

605 if np.real(appr_hess[i]) < -1e-4: 

606 appr_sp_order += 1 

607 return appr_hess, appr_sp_order 

608 

609 def form_initial_krylov_subspace( 

610 self, wfs, appr_hess, dimz, use_prev=False): 

611 rng = np.random.default_rng(self.seed) 

612 wfs.timer.start('Initial Krylov subspace') 

613 if use_prev: 

614 self.randomize_krylov_subspace(rng, dimz) 

615 else: 

616 self.initialize_randomized_krylov_subspace(rng, dimz, appr_hess) 

617 wfs.timer.start('Modified Gram-Schmidt') 

618 self.V_w = mgs(self.V_w) 

619 wfs.timer.stop('Modified Gram-Schmidt') 

620 self.V_w = self.V_w.T 

621 wfs.timer.stop('Initial Krylov subspace') 

622 

623 def randomize_krylov_subspace(self, rng, dimz, reps=1e-4): 

624 self.V_w = deepcopy(self.x_w.T[: self.l]) 

625 for i in range(self.l): 

626 for k in range(self.dimtot): 

627 for l in range(dimz): 

628 rand = np.zeros(shape=2) 

629 if world.rank == 0: 

630 rand[0] = rng.random() 

631 rand[1] = 1 if rng.random() > 0.5 else -1 

632 else: 

633 rand[0] = 0.0 

634 rand[1] = 0.0 

635 world.broadcast(rand, 0) 

636 self.V_w[i][l * self.dimtot + k] \ 

637 += rand[1] * reps * rand[0] 

638 

639 def initialize_randomized_krylov_subspace( 

640 self, rng, dimz, appr_hess, reps=1e-4): 

641 do_conj = False 

642 

643 # Just for F821 

644 v = None 

645 imin = None 

646 

647 self.V_w = [] 

648 for i in range(self.l): 

649 if do_conj: 

650 v[self.dimtot + imin] = -1.0 

651 do_conj = False 

652 else: 

653 v = np.zeros(self.dimtot * dimz) 

654 rdia = np.real(appr_hess).copy() 

655 imin = int(np.where(rdia == min(rdia))[0][0]) 

656 rdia[imin] = np.inf 

657 v[imin] = 1.0 

658 if self.etdm.dtype == complex: 

659 v[self.dimtot + imin] = 1.0 

660 do_conj = True 

661 for l in range(self.dimtot): 

662 for m in range(dimz): 

663 if l == imin: 

664 continue 

665 rand = np.zeros(shape=2) 

666 if world.rank == 0: 

667 rand[0] = rng.random() 

668 rand[1] = 1 if rng.random() > 0.5 else -1 

669 else: 

670 rand[0] = 0.0 

671 rand[1] = 0.0 

672 world.broadcast(rand, 0) 

673 v[m * self.dimtot + l] = rand[1] * reps * rand[0] 

674 self.V_w.append(v / np.linalg.norm(v)) 

675 self.V_w = np.asarray(self.V_w) 

676 

677 def iterate(self, wfs, ham, dens): 

678 self.t_e = [] 

679 self.evaluate_W(wfs, ham, dens) 

680 wfs.timer.start('Rayleigh matrix formation') 

681 self.H_ww = self.V_w.T @ self.W_w 

682 wfs.timer.stop('Rayleigh matrix formation') 

683 self.n_iter += 1 

684 wfs.timer.start('Rayleigh matrix diagonalization') 

685 eigv, eigvec = np.linalg.eigh(self.H_ww) 

686 wfs.timer.stop('Rayleigh matrix diagonalization') 

687 eigvec = eigvec.T 

688 self.lambda_w = deepcopy(eigv) 

689 self.y_w = deepcopy(eigvec) 

690 self.lambda_e = eigv[: self.l] 

691 self.y_e = eigvec[: self.l] 

692 self.calculate_ritz_vectors(wfs) 

693 self.calculate_residuals(wfs) 

694 for i in range(self.l): 

695 self.error_e[i] = np.abs(self.r_e[i]).max() 

696 self.all_converged = True 

697 for i in range(self.l): 

698 self.converged_e[i] = self.error_e[i] < self.eps 

699 self.all_converged = self.all_converged and self.converged_e[i] 

700 if self.all_converged: 

701 self.eigenvalues = deepcopy(self.lambda_e) 

702 self.eigenvectors = deepcopy(self.x_e) 

703 self.log() 

704 return 

705 self.calculate_preconditioner(wfs) 

706 self.augment_krylov_subspace(wfs) 

707 self.log() 

708 

709 def evaluate_W(self, wfs, ham, dens): 

710 wfs.timer.start('FD Hessian vector product') 

711 if self.W_w is None: 

712 self.W_w = [] 

713 Vt = self.V_w.T 

714 for i in range(len(Vt)): 

715 self.W_w.append(self.get_fd_hessian(Vt[i], wfs, ham, dens)) 

716 self.reset = False 

717 else: 

718 added = len(self.V_w[0]) - len(self.W_w[0]) 

719 self.W_w = self.W_w.T.tolist() 

720 Vt = self.V_w.T 

721 for i in range(added): 

722 self.W_w.append(self.get_fd_hessian( 

723 Vt[-added + i], wfs, ham, dens)) 

724 self.W_w = np.asarray(self.W_w).T 

725 wfs.timer.stop('FD Hessian vector product') 

726 

727 def calculate_ritz_vectors(self, wfs): 

728 wfs.timer.start('Ritz vector calculation') 

729 self.x_e = [] 

730 for i in range(self.l): 

731 self.x_e.append(self.V_w @ self.y_e[i].T) 

732 self.x_e = np.asarray(self.x_e) 

733 wfs.timer.stop('Ritz vector calculation') 

734 

735 def calculate_residuals(self, wfs): 

736 wfs.timer.start('Residual calculation') 

737 self.r_e = [] 

738 for i in range(self.l): 

739 self.r_e.append( 

740 self.x_e[i] * self.lambda_e[i] - self.W_w @ self.y_e[i].T) 

741 self.r_e = np.asarray(self.r_e) 

742 wfs.timer.stop('Residual calculation') 

743 

744 def calculate_preconditioner(self, wfs): 

745 n_dim = len(self.V_w) 

746 wfs.timer.start('Preconditioner calculation') 

747 self.C_we = np.zeros(shape=(self.l, n_dim)) 

748 for i in range(self.l): 

749 self.C_we[i] = -np.abs( 

750 np.repeat(self.lambda_e[i], n_dim) - self.M) ** -1 

751 for l in range(len(self.C_we[i])): 

752 if self.C_we[i][l] > -0.1 * Hartree: 

753 self.C_we[i][l] = -0.1 * Hartree 

754 wfs.timer.stop('Preconditioner calculation') 

755 

756 def augment_krylov_subspace(self, wfs): 

757 wfs.timer.start('Krylov subspace augmentation') 

758 self.get_new_krylov_subspace_directions(wfs) 

759 self.V_w = np.asarray(self.V_w) 

760 if self.cap_krylov: 

761 if len(self.V_w) > self.m: 

762 self.logger( 

763 'Krylov space exceeded maximum size. Partial ' 

764 'diagonalization is not fully converged. Current size ' 

765 'is ' + str(len(self.V_w) - len(self.t_e)) + '. Size at ' 

766 'next step would be ' + str(len(self.V_w)) + '.', 

767 flush=True) 

768 self.all_converged = True 

769 wfs.timer.start('Modified Gram-Schmidt') 

770 self.V_w = mgs(self.V_w) 

771 wfs.timer.stop('Modified Gram-Schmidt') 

772 self.V_w = self.V_w.T 

773 wfs.timer.stop('Krylov subspace augmentation') 

774 

775 def get_new_krylov_subspace_directions(self, wfs): 

776 wfs.timer.start('New directions') 

777 for i in range(self.l): 

778 if not self.converged_e[i] or (self.gmf and self.first_run): 

779 self.t_e.append(self.C_we[i] * self.r_e[i]) 

780 self.t_e = np.asarray(self.t_e) 

781 if len(self.V_w[0]) <= self.m: 

782 self.V_w = self.V_w.T.tolist() 

783 for i in range(len(self.t_e)): 

784 self.V_w.append(self.t_e[i]) 

785 elif not self.cap_krylov: 

786 self.reset = True 

787 self.V_w = deepcopy(self.x_e.tolist()) 

788 for i in range(len(self.t_e)): 

789 self.V_w.append(self.t_e[i]) 

790 self.W_w = None 

791 wfs.timer.stop('New directions') 

792 

793 def log(self): 

794 self.logger('Dimensionality of Krylov space: ' 

795 + str(len(self.V_w[0]) - len(self.t_e)), flush=True) 

796 if self.reset: 

797 self.logger('Reset Krylov space', flush=True) 

798 self.logger('\nEigenvalues:\n', flush=True) 

799 text = '' 

800 for i in range(self.l): 

801 text += '%10d ' 

802 indices = text % tuple(range(1, self.l + 1)) 

803 self.logger(indices, flush=True) 

804 text = '' 

805 for i in range(self.l): 

806 text += '%10.6f ' 

807 self.logger(text % tuple(self.lambda_e), flush=True) 

808 self.logger('\nResidual maximum components:\n', flush=True) 

809 self.logger(indices, flush=True) 

810 text = '' 

811 temp = list(np.round(deepcopy(self.error_e), 6)) 

812 for i in range(self.l): 

813 text += '%10s ' 

814 temp[i] = str(temp[i]) 

815 if self.converged_e[i]: 

816 temp[i] += 'c' 

817 self.logger(text % tuple(temp), flush=True) 

818 

819 def get_fd_hessian(self, vin, wfs, ham, dens): 

820 """ 

821 Get the dot product of the Hessian and a vector with a finite 

822 difference approximation. 

823 

824 :param vin: The vector 

825 :param wfs: 

826 :param ham: 

827 :param dens: 

828 :return: Dot product vector of the Hessian and the vector. 

829 """ 

830 

831 v = self.h * vin 

832 c_ref = deepcopy(self.c_ref) 

833 gp = self.calculate_displaced_grad(wfs, ham, dens, c_ref, v) 

834 hessi = [] 

835 if self.fd_mode == 'central': 

836 gm = self.calculate_displaced_grad(wfs, ham, dens, c_ref, -1.0 * v) 

837 for k in range(len(wfs.kpt_u)): 

838 hessi += list((gp[k] - gm[k]) * 0.5 / self.h) 

839 elif self.fd_mode == 'forward': 

840 for k in range(len(wfs.kpt_u)): 

841 hessi += list((gp[k] - self.grad[k]) / self.h) 

842 for k, kpt in enumerate(wfs.kpt_u): 

843 kpt.C_nM = c_ref[k] # LCAO-specific 

844 dens.update(wfs) 

845 if self.etdm.dtype == complex: 

846 hessc = np.zeros(shape=(2 * self.dimtot)) 

847 hessc[: self.dimtot] = np.real(hessi) 

848 hessc[self.dimtot:] = np.imag(hessi) 

849 return hessc 

850 else: 

851 return np.asarray(hessi) 

852 

853 def calculate_displaced_grad(self, wfs, ham, dens, c_ref, v): 

854 a_vec_u = {} 

855 n_dim = {} 

856 start = 0 

857 end = 0 

858 for k in range(len(wfs.kpt_u)): 

859 a_vec_u[k] = np.zeros_like(self.etdm.a_vec_u[k]) 

860 n_dim[k] = wfs.bd.nbands 

861 end += self.dim_u[k] 

862 a_vec_u[k] += v[start: end] 

863 if self.etdm.dtype == complex: 

864 a_vec_u[k] += 1.0j * v[self.dimtot + start: self.dimtot + end] 

865 start += self.dim_u[k] 

866 return self.etdm.get_energy_and_gradients( 

867 a_vec_u, ham, wfs, dens, c_ref)[1] 

868 

869 def break_instability(self, wfs, n_dim, c_ref, number, 

870 initial_guess='displace', ham=None, dens=None): 

871 """ 

872 Displaces orbital rotation coordinates in the direction of an 

873 instability. Uses a fixed displacement or performs a line search. 

874 

875 :param wfs: 

876 :param n_dim: 

877 :param c_ref: 

878 :param number: Instability index 

879 :param initial_guess: How to displace. Can be one of the following: 

880 displace: Use a fixed displacement; line_search: Performs a 

881 backtracking line search. 

882 :param ham: 

883 :param dens: 

884 """ 

885 

886 assert self.converged_e, 'Davidson cannot break instabilities since' \ 

887 + ' the partial eigendecomposition has not been converged.' 

888 assert len(self.lambda_e) >= number, 'Davidson cannot break' \ 

889 + ' instability no. ' + str(number) + ' since this eigenpair was' \ 

890 + 'not converged.' 

891 assert self.lambda_e[number - 1] < 0.0, 'Eigenvector no. ' \ 

892 + str(number) + ' does not represent an instability.' 

893 

894 step = self.etdm.line_search.max_step 

895 instability = step * self.x_e[number - 1] 

896 if initial_guess == 'displace': 

897 a_vec_u = self.displace(instability) 

898 elif initial_guess == 'line_search': 

899 assert ham is not None and dens is not None, 'Hamiltonian and' \ 

900 'density needed for' \ 

901 'line search.' 

902 a_vec_u = self.do_line_search( 

903 wfs, dens, ham, n_dim, c_ref, instability) 

904 self.etdm.rotate_wavefunctions(wfs, a_vec_u, c_ref) 

905 

906 def displace(self, instability): 

907 a_vec_u = deepcopy(self.etdm.a_vec_u) 

908 start = 0 

909 stop = 0 

910 for k in a_vec_u.keys(): 

911 stop += self.dim_u[k] 

912 a_vec_u[k] = instability[start: stop] 

913 start += self.dim_u[k] 

914 return a_vec_u 

915 

916 def do_line_search(self, wfs, dens, ham, n_dim, c_ref, instability): 

917 a_vec_u = {} 

918 p_vec_u = {} 

919 start = 0 

920 stop = 0 

921 for k in a_vec_u.keys(): 

922 stop += self.dim_u[k] 

923 p_vec_u[k] = instability[start: stop] 

924 start += self.dim_u[k] 

925 phi, g_vec_u = self.etdm.get_energy_and_gradients( 

926 a_vec_u, ham, wfs, dens, c_ref) 

927 der_phi = 0.0 

928 for k in g_vec_u: 

929 der_phi += g_vec_u[k].conj() @ p_vec_u[k] 

930 der_phi = der_phi.real 

931 der_phi = wfs.kd.comm.sum(der_phi) 

932 alpha = self.etdm.line_search.step_length_update( 

933 a_vec_u, p_vec_u, n_dim, ham, wfs, dens, c_ref, 

934 phi_0=phi, der_phi_0=der_phi, phi_old=None, 

935 der_phi_old=None, alpha_max=5.0, alpha_old=None, 

936 kpdescr=wfs.kd)[0] 

937 for k in a_vec_u.keys(): 

938 a_vec_u[k] = alpha * p_vec_u[k] 

939 return a_vec_u 

940 

941 def estimate_sp_order(self, calc, method='appr-hess', target_more=1): 

942 sort_orbitals_according_to_occ( 

943 calc.wfs, self.etdm.constraints, update_mom=method == 'full-hess') 

944 appr_hess, appr_sp_order = self.estimate_spo_and_update_appr_hess( 

945 calc.wfs) 

946 if calc.wfs.dtype == complex: 

947 appr_sp_order *= 2 

948 if method == 'full-hess': 

949 constraints_copy = deepcopy(self.etdm.constraints) 

950 self.etdm.constraints = [[] for _ in range(len(calc.wfs.kpt_u))] 

951 self.sp_order = appr_sp_order + target_more 

952 self.run(calc.wfs, calc.hamiltonian, calc.density) 

953 appr_hess, appr_sp_order = self.estimate_spo_and_update_appr_hess( 

954 calc.wfs, use_prev=True) 

955 self.etdm.constraints = deepcopy(constraints_copy) 

956 sort_orbitals_according_to_energies( 

957 calc.hamiltonian, calc.wfs, self.etdm.constraints) 

958 return appr_sp_order 

959 

960 

961def mgs(vin): 

962 """ 

963 Modified Gram-Schmidt orthonormalization 

964 

965 :param vin: Set of vectors. 

966 :return: Orthonormal set of vectors. 

967 """ 

968 

969 v = deepcopy(vin) 

970 q = np.zeros_like(v) 

971 for i in range(len(v)): 

972 q[i] = v[i] / np.linalg.norm(v[i]) 

973 for k in range(len(v)): 

974 v[k] = v[k] - np.dot(np.dot(q[i].T, v[k]), q[i]) 

975 return q 

976 

977 

978def construct_real_hessian(hess): 

979 

980 if hess.dtype == complex: 

981 hess_real = np.hstack((np.real(hess), np.imag(hess))) 

982 else: 

983 hess_real = hess 

984 

985 return hess_real 

986 

987 

988def apply_central_finite_difference_approx(fplus, fminus, eps): 

989 

990 if isinstance(fplus, dict) and isinstance(fminus, dict): 

991 assert (len(fplus) == len(fminus)) 

992 derf = np.hstack([(fplus[k] - fminus[k]) * 0.5 / eps 

993 for k in fplus.keys()]) 

994 elif isinstance(fplus, float) and isinstance(fminus, float): 

995 derf = (fplus - fminus) * 0.5 / eps 

996 else: 

997 raise ValueError() 

998 

999 return derf 

1000 

1001 

1002def get_approx_analytical_hessian(kpt, dtype, ind_up=None): 

1003 """ 

1004 Calculate the following diagonal approximation to the Hessian: 

1005 h_{lm, lm} = -2.0 * (eps_n[l] - eps_n[m]) * (f[l] - f[m]) 

1006 """ 

1007 

1008 f_n = kpt.f_n 

1009 eps_n = kpt.eps_n 

1010 if ind_up: 

1011 il1 = list(ind_up) 

1012 else: 

1013 il1 = list(get_indices(eps_n.shape[0])) 

1014 

1015 hess = np.zeros(len(il1[0]), dtype=dtype) 

1016 x = 0 

1017 for n, m in zip(*il1): 

1018 df = f_n[n] - f_n[m] 

1019 hess[x] = -2.0 * (eps_n[n] - eps_n[m]) * df 

1020 if abs(hess[x]) < 1.0e-10: 

1021 hess[x] = 0.0 

1022 if dtype == complex: 

1023 hess[x] += 1.0j * hess[x] 

1024 x += 1 

1025 

1026 return hess