Coverage for eminus / band_minimizer.py: 98.94%

189 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-21 14:20 +0000

1# SPDX-FileCopyrightText: 2023 The eminus developers 

2# SPDX-License-Identifier: Apache-2.0 

3"""Minimization algorithms for fixed Hamiltonians. 

4 

5Similar to :mod:`eminus.minimizer` but for a fixed Hamiltonian the implementation can be simplified 

6and made more performant. 

7""" 

8 

9import copy 

10import logging 

11 

12from . import backend as xp 

13from .dft import H, orth, orth_unocc 

14from .energies import get_Eband 

15from .logger import name 

16from .minimizer import cg_method, cg_test, check_convergence, linmin_test 

17from .utils import dotprod 

18 

19 

20def scf_step_occ(scf, W): 

21 """Perform one SCF step for an occupied band minimization calculation. 

22 

23 Args: 

24 scf: SCF object. 

25 W: Expansion coefficients of unconstrained wave functions in reciprocal space. 

26 

27 Returns: 

28 Band energy. 

29 """ 

30 atoms = scf.atoms 

31 scf.Y = orth(atoms, W) 

32 return get_Eband(scf, scf.Y, **scf._precomputed) 

33 

34 

35def scf_step_unocc(scf, Z): 

36 """Perform one SCF step for an unoccupied band minimization calculation. 

37 

38 Args: 

39 scf: SCF object. 

40 Z: Expansion coefficients of unconstrained wave functions in reciprocal space. 

41 

42 Returns: 

43 Band energy. 

44 """ 

45 atoms = scf.atoms 

46 scf.D = orth_unocc(atoms, scf.Y, Z) 

47 return get_Eband(scf, scf.D, **scf._precomputed) 

48 

49 

50def get_grad_occ(scf, ik, spin, W, **kwargs): 

51 """Calculate the occupied band energy gradient with respect to W. 

52 

53 Reference: Comput. Phys. Commun. 128, 1. 

54 

55 Args: 

56 scf: SCF object. 

57 ik: k-point index. 

58 spin: Spin variable to track whether to do the calculation for spin up or down. 

59 W: Expansion coefficients of unconstrained wave functions in reciprocal space. 

60 

61 Keyword Args: 

62 **kwargs: See :func:`H`. 

63 

64 Returns: 

65 Gradient. 

66 """ 

67 atoms = scf.atoms 

68 W = orth(atoms, W) 

69 HW = H(scf, ik, spin, W, **kwargs) 

70 WHW = W[ik][spin].conj().T @ HW 

71 OW = atoms.O(W[ik][spin]) 

72 U = W[ik][spin].conj().T @ OW 

73 invU = xp.linalg.inv(U) 

74 U12 = xp.sqrtm(invU) 

75 # grad E = (I - O(Y) Ydag) H(Y) U^-0.5 

76 return atoms.kpts.wk[ik] * ((HW - OW @ WHW) @ U12) 

77 

78 

79def get_grad_unocc(scf, ik, spin, Z, **kwargs): 

80 """Calculate the unoccupied band energy gradient with respect to Z. 

81 

82 Reference: Comput. Phys. Commun. 128, 1. 

83 

84 Args: 

85 scf: SCF object. 

86 ik: k-point index. 

87 spin: Spin variable to track whether to do the calculation for spin up or down. 

88 Z: Expansion coefficients of unconstrained wave functions in reciprocal space. 

89 

90 Keyword Args: 

91 **kwargs: See :func:`H`. 

92 

93 Returns: 

94 Gradient. 

95 """ 

96 atoms = scf.atoms 

97 Y = scf.Y[ik][spin][:, scf.atoms.occ.f[ik][spin] > 0] 

98 Ydag = Y.conj().T 

99 # We need X12 later, so orthogonalize in-place and only the current state 

100 rhoZ = Z[ik][spin] - Y @ Ydag @ atoms.O(Z[ik][spin]) 

101 X12 = xp.linalg.inv(xp.sqrtm(rhoZ.conj().T @ atoms.O(rhoZ))) 

102 D = rhoZ @ X12 

103 # Create the correct input shape for the Hamiltonian 

104 D_tmp = [None] * len(Z) 

105 D_tmp[ik] = xp.empty_like(Z[ik]) 

106 D_tmp[ik][spin] = D 

107 HD = H(scf, ik, spin, D_tmp, **kwargs) 

108 DHD = D.conj().T @ HD 

109 I = xp.eye(Z[ik].shape[1]) 

110 # grad E = (I - O(Y) Ydag) (I - O(D) Ddag) H(D) X^-0.5 

111 return atoms.kpts.wk[ik] * ((I - atoms.O(Y) @ Ydag) @ (HD - atoms.O(D) @ DHD) @ X12) 

112 

113 

114@name("steepest descent minimization") 

115def sd( 

116 scf, 

117 W, 

118 Nit, 

119 cost=scf_step_occ, 

120 grad=get_grad_occ, 

121 condition=check_convergence, 

122 betat=3e-5, 

123 **kwargs, 

124): 

125 """Steepest descent minimization algorithm for a fixed Hamiltonian. 

126 

127 Args: 

128 scf: SCF object. 

129 W: Expansion coefficients of unconstrained wave functions in reciprocal space. 

130 Nit: Maximum number of SCF steps. 

131 

132 Keyword Args: 

133 cost: Function that will run every SCF step. 

134 grad: Function that calculates the respective gradient. 

135 condition: Function to check and log the convergence condition. 

136 betat: Step size. 

137 **kwargs: Throwaway arguments. 

138 

139 Returns: 

140 Band energies per SCF cycle and optimized expansion coefficients. 

141 """ 

142 atoms = scf.atoms 

143 costs = [] 

144 

145 for _ in range(Nit): 

146 c = cost(scf, W) 

147 costs.append(c) 

148 if condition(scf, "sd", costs): 

149 break 

150 for ik in range(atoms.kpts.Nk): 

151 for spin in range(atoms.occ.Nspin): 

152 g = grad(scf, ik, spin, W, **scf._precomputed) 

153 W[ik][spin] = W[ik][spin] - betat * g 

154 return costs, W 

155 

156 

157@name("preconditioned line minimization") 

158def pclm( 

159 scf, 

160 W, 

161 Nit, 

162 cost=scf_step_occ, 

163 grad=get_grad_occ, 

164 condition=check_convergence, 

165 betat=3e-5, 

166 precondition=True, 

167 **kwargs, 

168): 

169 """Preconditioned line minimization algorithm for a fixed Hamiltonian. 

170 

171 Args: 

172 scf: SCF object. 

173 W: Expansion coefficients of unconstrained wave functions in reciprocal space. 

174 Nit: Maximum number of SCF steps. 

175 

176 Keyword Args: 

177 cost: Function that will run every SCF step. 

178 grad: Function that calculates the respective gradient. 

179 condition: Function to check and log the convergence condition. 

180 betat: Step size. 

181 precondition: Whether to use a preconditioner. 

182 **kwargs: Throwaway arguments. 

183 

184 Returns: 

185 Band energies per SCF cycle and optimized expansion coefficients. 

186 """ 

187 atoms = scf.atoms 

188 costs = [] 

189 

190 linmin = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin)) 

191 d = [xp.empty_like(Wk) for Wk in W] 

192 

193 if precondition: 

194 method = "pclm" 

195 else: 

196 method = "lm" 

197 

198 for i in range(Nit): 

199 c = cost(scf, W) 

200 costs.append(c) 

201 if condition(scf, method, costs, linmin): 

202 break 

203 for ik in range(atoms.kpts.Nk): 

204 for spin in range(atoms.occ.Nspin): 

205 g = grad(scf, ik, spin, W, **scf._precomputed) 

206 if scf._log.level <= logging.DEBUG and i > 0: 

207 linmin[ik][spin] = linmin_test(g, d[ik][spin]) 

208 if precondition: 

209 d[ik][spin] = -atoms.K(g, ik) 

210 else: 

211 d[ik][spin] = -g 

212 W[ik][spin] = W[ik][spin] + betat * d[ik][spin] 

213 gt = grad(scf, ik, spin, W, **scf._precomputed) 

214 beta = abs(betat * dotprod(g, d[ik][spin]) / dotprod(g - gt, d[ik][spin])) 

215 W[ik][spin] = W[ik][spin] + beta * d[ik][spin] 

216 return costs, W 

217 

218 

219@name("line minimization") 

220def lm( 

221 scf, 

222 W, 

223 Nit, 

224 cost=scf_step_occ, 

225 grad=get_grad_occ, 

226 condition=check_convergence, 

227 betat=3e-5, 

228 **kwargs, 

229): 

230 """Line minimization algorithm for a fixed Hamiltonian. 

231 

232 Args: 

233 scf: SCF object. 

234 W: Expansion coefficients of unconstrained wave functions in reciprocal space. 

235 Nit: Maximum number of SCF steps. 

236 

237 Keyword Args: 

238 cost: Function that will run every SCF step. 

239 grad: Function that calculates the respective gradient. 

240 condition: Function to check and log the convergence condition. 

241 betat: Step size. 

242 **kwargs: Throwaway arguments. 

243 

244 Returns: 

245 Band energies per SCF cycle and optimized expansion coefficients. 

246 """ 

247 return pclm(scf, W, Nit, cost, grad, condition, betat, precondition=False) 

248 

249 

250@name("preconditioned conjugate-gradient minimization") 

251def pccg( 

252 scf, 

253 W, 

254 Nit, 

255 cost=scf_step_occ, 

256 grad=get_grad_occ, 

257 condition=check_convergence, 

258 betat=3e-5, 

259 cgform=1, 

260 precondition=True, 

261): 

262 """Preconditioned conjugate-gradient minimization algorithm for a fixed Hamiltonian. 

263 

264 Args: 

265 scf: SCF object. 

266 W: Expansion coefficients of unconstrained wave functions in reciprocal space. 

267 Nit: Maximum number of SCF steps. 

268 

269 Keyword Args: 

270 cost: Function that will run every SCF step. 

271 grad: Function that calculates the respective gradient. 

272 condition: Function to check and log the convergence condition. 

273 betat: Step size. 

274 cgform: Conjugate gradient form. 

275 precondition: Whether to use a preconditioner. 

276 

277 Returns: 

278 Band energies per SCF cycle and optimized expansion coefficients. 

279 """ 

280 atoms = scf.atoms 

281 costs = [] 

282 

283 linmin = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin)) 

284 cg = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin)) 

285 norm_g = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin)) 

286 d = [xp.empty_like(Wk) for Wk in W] 

287 d_old = [xp.empty_like(Wk) for Wk in W] 

288 g_old = [xp.empty_like(Wk) for Wk in W] 

289 

290 if precondition: 

291 method = "pccg" 

292 else: 

293 method = "cg" 

294 

295 c = cost(scf, W) 

296 costs.append(c) 

297 condition(scf, method, costs) 

298 # Do the first step without the linmin and cg tests, and without the cg_method 

299 for ik in range(atoms.kpts.Nk): 

300 for spin in range(atoms.occ.Nspin): 

301 g = grad(scf, ik, spin, W, **scf._precomputed) 

302 if precondition: 

303 d[ik][spin] = -atoms.K(g, ik) 

304 else: 

305 d[ik][spin] = -g 

306 W[ik][spin] = W[ik][spin] + betat * d[ik][spin] 

307 gt = grad(scf, ik, spin, W, **scf._precomputed) 

308 beta = abs(betat * dotprod(g, d[ik][spin]) / dotprod(g - gt, d[ik][spin])) 

309 g_old[ik][spin], d_old[ik][spin] = g, d[ik][spin] 

310 W[ik][spin] = W[ik][spin] + beta * d[ik][spin] 

311 

312 for _ in range(1, Nit): 

313 c = cost(scf, W) 

314 costs.append(c) 

315 if condition(scf, method, costs, linmin, cg, norm_g): 

316 break 

317 for ik in range(atoms.kpts.Nk): 

318 for spin in range(atoms.occ.Nspin): 

319 g = grad(scf, ik, spin, W, **scf._precomputed) 

320 # Calculate linmin and cg for each spin separately 

321 if scf._log.level <= logging.DEBUG: 

322 linmin[ik][spin] = linmin_test(g, d[ik][spin]) 

323 cg[ik][spin] = cg_test(atoms, ik, g, g_old[ik][spin], precondition) 

324 beta, norm_g[ik][spin] = cg_method( 

325 scf, ik, cgform, g, g_old[ik][spin], d_old[ik][spin], precondition 

326 ) 

327 if precondition: 

328 d[ik][spin] = -atoms.K(g, ik) + beta * d_old[ik][spin] 

329 else: 

330 d[ik][spin] = -g + beta * d_old[ik][spin] 

331 W[ik][spin] = W[ik][spin] + betat * d[ik][spin] 

332 gt = grad(scf, ik, spin, W, **scf._precomputed) 

333 beta = abs(betat * dotprod(g, d[ik][spin]) / dotprod(g - gt, d[ik][spin])) 

334 g_old[ik][spin], d_old[ik][spin] = g, d[ik][spin] 

335 W[ik][spin] = W[ik][spin] + beta * d[ik][spin] 

336 return costs, W 

337 

338 

339@name("conjugate-gradient minimization") 

340def cg( 

341 scf, 

342 W, 

343 Nit, 

344 cost=scf_step_occ, 

345 grad=get_grad_occ, 

346 condition=check_convergence, 

347 betat=3e-5, 

348 cgform=1, 

349): 

350 """Conjugate-gradient minimization algorithm for a fixed Hamiltonian. 

351 

352 Args: 

353 scf: SCF object. 

354 W: Expansion coefficients of unconstrained wave functions in reciprocal space. 

355 Nit: Maximum number of SCF steps. 

356 

357 Keyword Args: 

358 cost: Function that will run every SCF step. 

359 grad: Function that calculates the respective gradient. 

360 condition: Function to check and log the convergence condition. 

361 betat: Step size. 

362 cgform: Conjugate gradient form. 

363 

364 Returns: 

365 Band energies per SCF cycle and optimized expansion coefficients. 

366 """ 

367 return pccg(scf, W, Nit, cost, grad, condition, betat, cgform, precondition=False) 

368 

369 

370@name("auto minimization") 

371def auto( 

372 scf, 

373 W, 

374 Nit, 

375 cost=scf_step_occ, 

376 grad=get_grad_occ, 

377 condition=check_convergence, 

378 betat=3e-5, 

379 cgform=1, 

380): 

381 """Automatic precond. conjugate-gradient minimization algorithm for a fixed Hamiltonian. 

382 

383 This function chooses an sd step over the pccg step if the energy goes up. 

384 

385 Args: 

386 scf: SCF object. 

387 W: Expansion coefficients of unconstrained wave functions in reciprocal space. 

388 Nit: Maximum number of SCF steps. 

389 

390 Keyword Args: 

391 cost: Function that will run every SCF step. 

392 grad: Function that calculates the respective gradient. 

393 condition: Function to check and log the convergence condition. 

394 betat: Step size. 

395 cgform: Conjugate gradient form. 

396 

397 Returns: 

398 Band energies per SCF cycle and optimized expansion coefficients. 

399 """ 

400 atoms = scf.atoms 

401 costs = [] 

402 

403 linmin = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin)) 

404 cg = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin)) 

405 norm_g = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin)) 

406 g = [xp.empty_like(Wk) for Wk in W] 

407 d = [xp.empty_like(Wk) for Wk in W] 

408 d_old = [xp.empty_like(Wk) for Wk in W] 

409 g_old = [xp.empty_like(Wk) for Wk in W] 

410 

411 # Do the first step without the linmin and cg tests, and without the cg_method 

412 for ik in range(atoms.kpts.Nk): 

413 for spin in range(atoms.occ.Nspin): 

414 g[ik][spin] = grad(scf, ik, spin, W, **scf._precomputed) 

415 d[ik][spin] = -atoms.K(g[ik][spin], ik) 

416 W[ik][spin] = W[ik][spin] + betat * d[ik][spin] 

417 gt = grad(scf, ik, spin, W, **scf._precomputed) 

418 beta = abs( 

419 betat * dotprod(g[ik][spin], d[ik][spin]) / dotprod(g[ik][spin] - gt, d[ik][spin]) 

420 ) 

421 g_old[ik][spin], d_old[ik][spin] = g[ik][spin], d[ik][spin] 

422 W[ik][spin] = W[ik][spin] + beta * d[ik][spin] 

423 

424 c = cost(scf, W) 

425 costs.append(c) 

426 if condition(scf, "pccg", costs): 

427 return costs 

428 

429 for _ in range(1, Nit): 

430 W_old = copy.deepcopy(W) 

431 for ik in range(atoms.kpts.Nk): 

432 for spin in range(atoms.occ.Nspin): 

433 g[ik][spin] = grad(scf, ik, spin, W, **scf._precomputed) 

434 # Calculate linmin and cg for each spin separately 

435 if scf._log.level <= logging.DEBUG: 

436 linmin[ik][spin] = linmin_test(g[ik][spin], d[ik][spin]) 

437 cg[ik][spin] = cg_test(atoms, ik, g[ik][spin], g_old[ik][spin]) 

438 beta, norm_g[ik][spin] = cg_method( 

439 scf, ik, cgform, g[ik][spin], g_old[ik][spin], d_old[ik][spin] 

440 ) 

441 d[ik][spin] = -atoms.K(g[ik][spin], ik) + beta * d_old[ik][spin] 

442 W[ik][spin] = W[ik][spin] + betat * d[ik][spin] 

443 gt = grad(scf, ik, spin, W, **scf._precomputed) 

444 beta = abs( 

445 betat 

446 * dotprod(g[ik][spin], d[ik][spin]) 

447 / dotprod(g[ik][spin] - gt, d[ik][spin]) 

448 ) 

449 g_old[ik][spin], d_old[ik][spin] = g[ik][spin], d[ik][spin] 

450 W[ik][spin] = W[ik][spin] + beta * d[ik][spin] 

451 

452 c = cost(scf, W) 

453 # If the energy does not go down use the steepest descent step and recalculate the energy 

454 if c > costs[-1]: 

455 W = W_old 

456 for ik in range(atoms.kpts.Nk): 

457 for spin in range(atoms.occ.Nspin): 

458 W[ik][spin] = W[ik][spin] - betat * g[ik][spin] 

459 c = cost(scf, W) 

460 costs.append(c) 

461 # Do not print cg and linmin if we do the sd step 

462 if condition(scf, "sd", costs, norm_g=norm_g): 

463 break 

464 else: 

465 costs.append(c) 

466 if condition(scf, "pccg", costs, linmin, cg, norm_g): 

467 break 

468 return costs, W 

469 

470 

471#: Map minimizer names with their respective implementation. 

472IMPLEMENTED = { 

473 "sd": sd, 

474 "lm": lm, 

475 "pclm": pclm, 

476 "cg": cg, 

477 "pccg": pccg, 

478 "auto": auto, 

479}