Coverage for gpaw/atom/basis.py: 94%

292 statements  

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

1"""This module is used to generate atomic orbital basis sets.""" 

2import sys 

3from io import StringIO 

4 

5import numpy as np 

6from ase.units import Hartree 

7from gpaw.utilities import devnull 

8 

9from gpaw import __version__ as version 

10from gpaw.utilities import divrl 

11from gpaw.atom.generator import Generator 

12from gpaw.atom.all_electron import ValenceData 

13from gpaw.atom.configurations import parameters 

14from gpaw.basis_data import Basis, BasisFunction, get_basis_name 

15from gpaw.atom.radialgd import (AERadialGridDescriptor, 

16 EquidistantRadialGridDescriptor) 

17 

18 

19def make_split_valence_basis_function(r_g, psi_g, l, gcut): 

20 """Get polynomial which joins psi smoothly at rcut. 

21 

22 Returns an array of function values f(r) * r, where:: 

23 

24 l 2 

25 f(r) = r * (a - b r ), r < rcut 

26 f(r) = psi(r), r >= rcut 

27 

28 where a and b are determined such that f(r) is continuous and 

29 differentiable at rcut. The parameter psi should be an atomic 

30 orbital. 

31 """ 

32 r1 = r_g[gcut] # ensure that rcut is moved to a grid point 

33 r2 = r_g[gcut + 1] 

34 y1 = psi_g[gcut] / r_g[gcut] 

35 y2 = psi_g[gcut + 1] / r_g[gcut + 1] 

36 b = - (y2 / r2**l - y1 / r1**l) / (r2**2 - r1**2) 

37 a = (y1 / r1**l + b * r1**2) 

38 psi_g2 = r_g**(l + 1) * (a - b * r_g**2) 

39 psi_g2[gcut:] = psi_g[gcut:] 

40 return psi_g2 

41 

42 

43def rsplit_by_norm(rgd, l, u, tailnorm_squared, txt): 

44 """Find radius outside which remaining tail has a particular norm.""" 

45 norm_squared = np.dot(rgd.dr_g, u * u) 

46 partial_norm_squared = 0. 

47 i = len(u) - 1 

48 absolute_tailnorm_squared = tailnorm_squared * norm_squared 

49 while partial_norm_squared < absolute_tailnorm_squared: 

50 # Integrate backwards. This is important since the pseudo 

51 # wave functions have strange behaviour near the core. 

52 partial_norm_squared += rgd.dr_g[i] * u[i]**2 

53 i -= 1 

54 rsplit = rgd.r_g[i + 1] 

55 msg = ('Tail norm %.03f :: rsplit=%.02f Bohr' % 

56 ((partial_norm_squared / norm_squared)**0.5, rsplit)) 

57 print(msg, file=txt) 

58 gsplit = rgd.floor(rsplit) 

59 splitwave = make_split_valence_basis_function(rgd.r_g, u, l, gsplit) 

60 return rsplit, partial_norm_squared, splitwave 

61 

62 

63class BasisMaker: 

64 """Class for creating atomic basis functions.""" 

65 def __init__(self, valence_data, name=None, run=True, gtxt='-', 

66 non_relativistic_guess=False, xc='PBE', 

67 save_setup=False): 

68 

69 if not isinstance(valence_data, ValenceData): 

70 raise TypeError('BasisMaker no longer accepts Generator or str. ' 

71 'Please provide ValenceData or use one of the ' 

72 'helper classmethods.') 

73 

74 self.name = name 

75 self.valence_data = valence_data 

76 rgd = valence_data.rgd 

77 rgd = AERadialGridDescriptor(rgd.a, 

78 rgd.b, 

79 len(rgd.r_g), 

80 default_spline_points=100) 

81 self.rgd = rgd 

82 

83 @classmethod 

84 def from_setup_and_generator(cls, setup, generator, **kwargs): 

85 valdata = ValenceData.from_setupdata_and_potentials( 

86 setup, vr_g=generator.vr, r2dvdr_g=generator.r2dvdr, 

87 scalarrel=generator.scalarrel) 

88 return cls(valdata, **kwargs) 

89 

90 @classmethod 

91 def from_symbol(cls, symbol, gtxt='-', xc='PBE', name=None, 

92 save_setup=False, 

93 generator_run_kwargs=None, **kwargs): 

94 generator = Generator(symbol, scalarrel=True, 

95 xcname=xc, txt=gtxt, 

96 nofiles=True, 

97 gpernode=Generator.default_gpernode * 4) 

98 

99 run_kwargs = { 

100 'write_xml': False, 

101 'name': name, 

102 **parameters[generator.symbol]} 

103 

104 if generator_run_kwargs is not None: 

105 run_kwargs.update(generator_run_kwargs) 

106 

107 setup = generator.run(**run_kwargs) 

108 

109 if save_setup: 

110 setup.write_xml() 

111 

112 return cls.from_setup_and_generator(setup, generator, name=name, 

113 **kwargs) 

114 

115 def smoothify(self, psi_mg, j): 

116 r"""Generate pseudo wave functions from all-electron ones. 

117 

118 The pseudo wave function is:: 

119 

120 ___ 

121 ~ \ / ~ \ ~ ~ 

122 | psi > = | psi > + ) | | phi > - | phi > ) < p | psi > , 

123 m m /__ \ i i / i m 

124 i 

125 

126 where the scalar products are found by solving:: 

127 

128 ___ 

129 ~ \ ~ ~ ~ 

130 < p | psi > = ) < p | phi > < p | psi > . 

131 i m /__ i j j m 

132 j 

133 

134 In order to ensure smoothness close to the core, the 

135 all-electron wave function and partial wave are then 

136 multiplied by a radial function which approaches 0 near the 

137 core, such that the pseudo wave function approaches:: 

138 

139 ___ 

140 ~ \ ~ ~ ~ 

141 | psi > = ) | phi > < p | psi > (for r << rcut), 

142 m /__ i i m 

143 i 

144 

145 which is exact if the projectors/pseudo partial waves are complete. 

146 """ 

147 

148 if psi_mg.ndim == 1: 

149 return self.smoothify(psi_mg[None], j)[0] 

150 

151 valdata = self.valence_data 

152 l = valdata.l_j[j] 

153 

154 def angular_buddies(array_j): 

155 return np.array([array_j[j] for j, l1 in enumerate(valdata.l_j) 

156 if l1 == l]) 

157 

158 u_ng = angular_buddies(valdata.phi_jg) 

159 q_ng = angular_buddies(valdata.pt_jg) 

160 s_ng = angular_buddies(valdata.phit_jg) 

161 

162 r_g = valdata.rgd.r_g 

163 

164 Pi_nn = np.dot(r_g * q_ng, u_ng.T) 

165 Q_nm = np.dot(r_g * q_ng, psi_mg.T) 

166 Qt_nm = np.linalg.solve(Pi_nn, Q_nm) 

167 

168 # Weight-function for truncating all-electron parts smoothly near core 

169 gmerge = valdata.rgd.floor(valdata.rcut_j[j]) 

170 w_g = np.ones_like(r_g) 

171 w_g[0:gmerge] = (r_g[0:gmerge] / r_g[gmerge])**2. 

172 w_g = w_g[None] 

173 

174 psit_mg = psi_mg * w_g + np.dot(Qt_nm.T, s_ng - u_ng * w_g) 

175 return psit_mg 

176 

177 def rcut_by_energy(self, j, esplit=.1, tolerance=.1, rguess=6., 

178 vconf_args=None): 

179 """Find confinement cutoff corresponding to given orbital energy shift. 

180 

181 Creates a confinement potential for the orbital given by j, 

182 such that the confined-orbital energy is (emin to emax) eV larger 

183 than the free-orbital energy.""" 

184 valdata = self.valence_data 

185 e_base = valdata.e_j[j] 

186 rc = rguess 

187 

188 if vconf_args is None: 

189 vconf = None 

190 else: 

191 amplitude, ri_rel = vconf_args 

192 vconf = valdata.get_confinement_potential( 

193 amplitude, ri_rel * rc, rc) 

194 

195 psi_g, e = valdata.solve_confined(j, rc, vconf) 

196 de_min, de_max = esplit / Hartree, (esplit + tolerance) / Hartree 

197 

198 rmin = 0. 

199 rmax = valdata.rgd.r_g[-1] 

200 

201 de = e - e_base 

202 while de < de_min or de > de_max: 

203 if de < de_min: # Move rc left -> smaller cutoff, higher energy 

204 rmax = rc 

205 rc = (rc + rmin) / 2. 

206 else: # Move rc right 

207 rmin = rc 

208 rc = (rc + rmax) / 2. 

209 if vconf is not None: 

210 vconf = valdata.get_confinement_potential( 

211 amplitude, ri_rel * rc, rc) 

212 psi_g, e = valdata.solve_confined(j, rc, vconf) 

213 de = e - e_base 

214 if valdata.rgd.floor(rmax) - valdata.rgd.floor(rmin) <= 1: 

215 # Adjacent points, cannot meet tolerance due to grid resolution 

216 break 

217 

218 return psi_g, e, de, vconf, rc 

219 

220 def generate(self, zetacount=2, polarizationcount=1, 

221 tailnorm=(0.16, 0.3, 0.6), energysplit=0.1, tolerance=1.0e-3, 

222 rcutpol_rel=1.0, 

223 rcutmax=20.0, 

224 rcharpol_rel=None, 

225 vconf_args=(12.0, 0.6), txt='-', 

226 include_energy_derivatives=False, 

227 jvalues=None, 

228 l_pol=None): 

229 """Generate an entire basis set. 

230 

231 This is a high-level method which will return a basis set 

232 consisting of several different basis vector types. 

233 

234 Parameters: 

235 

236 ===================== ================================================= 

237 ``zetacount`` Number of basis functions per occupied orbital 

238 ``polarizationcount`` Number of polarization functions 

239 ``tailnorm`` List of tail norms for split-valence scheme 

240 ``energysplit`` Energy increase defining confinement radius (eV) 

241 ``tolerance`` Tolerance of energy split (eV) 

242 ``rcutpol_rel`` Polarization rcut relative to largest other rcut 

243 ``rcutmax`` No cutoff will be greater than this value 

244 ``vconf_args`` Parameters (alpha, ri/rc) for conf. potential 

245 ``txt`` Log filename or '-' for stdout 

246 ===================== ================================================= 

247 

248 Returns a fully initialized Basis object. 

249 """ 

250 

251 if txt == '-': 

252 txt = sys.stdout 

253 elif txt is None: 

254 txt = devnull 

255 

256 if isinstance(tailnorm, float): 

257 tailnorm = (tailnorm,) 

258 if 1 + len(tailnorm) < max(polarizationcount, zetacount): 

259 raise ValueError( 

260 'Needs %d tail norm values, but only %d are specified' % 

261 (max(polarizationcount, zetacount) - 1, len(tailnorm))) 

262 

263 textbuffer = StringIO() 

264 

265 class TeeStream: # quick hack to both write and save output 

266 def __init__(self, out1, out2): 

267 self.out1 = out1 

268 self.out2 = out2 

269 

270 def write(self, string): 

271 self.out1.write(string) 

272 self.out2.write(string) 

273 

274 txt = TeeStream(txt, textbuffer) 

275 

276 if vconf_args is not None: 

277 amplitude, ri_rel = vconf_args 

278 

279 valdata = self.valence_data 

280 rgd = self.rgd 

281 

282 n_j = valdata.n_j 

283 l_j = valdata.l_j 

284 f_j = valdata.f_j 

285 

286 if jvalues is None: 

287 jvalues = [] 

288 sortkeys = [] 

289 for j in range(len(n_j)): 

290 if f_j[j] == 0 and l_j[j] != 0: 

291 continue 

292 jvalues.append(j) 

293 sortkeys.append(l_j[j]) 

294 

295 # Now order jvalues by l 

296 # 

297 # Use a stable sort so the energy ordering within each 

298 # angular momentum is guaranteed to be preserved 

299 args = np.argsort(sortkeys, kind='mergesort') 

300 jvalues = np.array(jvalues)[args] 

301 

302 if isinstance(energysplit, float): 

303 energysplit = [energysplit] * len(jvalues) 

304 

305 title = f'{valdata.xcname} Basis functions for {valdata.symbol}' 

306 print(title, file=txt) 

307 print('=' * len(title), file=txt) 

308 

309 singlezetas = [] 

310 energy_derivative_functions = [] 

311 multizetas = [[] for i in range(zetacount - 1)] 

312 polarization_functions = [] 

313 

314 splitvalencedescr = 'split-valence wave, fixed tail norm' 

315 derivativedescr = 'derivative of sz wrt. (ri/rc) of potential' 

316 

317 jvalues = [j for j in jvalues if n_j[j] > 0] 

318 

319 for vj, esplit in zip(jvalues, energysplit): 

320 l = l_j[vj] 

321 n = n_j[vj] 

322 assert n > 0 

323 orbitaltype = str(n) + 'spdf'[l] 

324 msg = 'Basis functions for l=%d, n=%d' % (l, n) 

325 print(file=txt) 

326 print(msg + '\n', '-' * len(msg), file=txt) 

327 print(file=txt) 

328 if vconf_args is None: 

329 adverb = 'sharply' 

330 else: 

331 adverb = 'softly' 

332 print('Zeta 1: %s confined pseudo wave,' % adverb, end=' ', 

333 file=txt) 

334 

335 u, e, de, vconf, rc = self.rcut_by_energy(vj, esplit, 

336 tolerance, 

337 vconf_args=vconf_args) 

338 if rc > rcutmax: 

339 rc = rcutmax # scale things down 

340 if vconf is not None: 

341 vconf = valdata.get_confinement_potential( 

342 amplitude, ri_rel * rc, rc) 

343 u, e = valdata.solve_confined(vj, rc, vconf) 

344 print('using maximum cutoff', file=txt) 

345 print('rc=%.02f Bohr' % rc, file=txt) 

346 else: 

347 print('fixed energy shift', file=txt) 

348 print('DE=%.03f eV :: rc=%.02f Bohr' 

349 % (de * Hartree, rc), file=txt) 

350 

351 if vconf is not None: 

352 print('Potential amp=%.02f :: ri/rc=%.02f' % 

353 (amplitude, ri_rel), file=txt) 

354 phit_g = self.smoothify(u, vj) 

355 bf = BasisFunction(n, l, rc, phit_g, 

356 '%s-sz confined orbital' % orbitaltype) 

357 norm = np.dot(valdata.rgd.dr_g, phit_g * phit_g)**.5 

358 print('Norm=%.03f' % norm, file=txt) 

359 singlezetas.append(bf) 

360 

361 zetacounter = iter(range(2, zetacount + 1)) 

362 

363 if include_energy_derivatives: 

364 assert zetacount > 1 

365 zeta = next(zetacounter) 

366 print('\nZeta %d: %s' % (zeta, derivativedescr), file=txt) 

367 vconf2 = valdata.get_confinement_potential( 

368 amplitude, ri_rel * rc * .99, rc) 

369 u2, e2 = valdata.solve_confined(vj, rc, vconf2) 

370 

371 phit2_g = self.smoothify(u2, vj) 

372 dphit_g = phit2_g - phit_g 

373 dphit_norm = np.dot(rgd.dr_g, dphit_g * dphit_g) ** .5 

374 dphit_g /= dphit_norm 

375 descr = '%s-dz E-derivative of sz' % orbitaltype 

376 bf = BasisFunction(None, l, rc, dphit_g, descr) 

377 energy_derivative_functions.append(bf) 

378 

379 for i, zeta in enumerate(zetacounter): 

380 print('\nZeta %d: %s' % (zeta, splitvalencedescr), file=txt) 

381 # Unresolved issue: how does the lack of normalization 

382 # of the first function impact the tail norm scheme? 

383 # Presumably not much, since most interesting stuff happens 

384 # close to the core. 

385 rsplit, norm, splitwave = rsplit_by_norm(rgd, l, phit_g, 

386 tailnorm[i]**2.0, 

387 txt) 

388 zetastring = '0sdtq56789'[zeta] 

389 descr = f'{orbitaltype}-{zetastring}z split-valence wave' 

390 bf = BasisFunction(None, l, rsplit, phit_g - splitwave, descr) 

391 multizetas[i].append(bf) 

392 

393 if polarizationcount > 0 or l_pol is not None: 

394 if l_pol is None: 

395 # Now make up some properties for the polarization orbital 

396 # We just use the cutoffs from the previous one times a factor 

397 # Find 'missing' values in lvalues 

398 lvalues = [l_j[vj] for vj in jvalues] 

399 for i in range(max(lvalues) + 1): 

400 if list(lvalues).count(i) == 0: 

401 l_pol = i 

402 break 

403 else: 

404 l_pol = max(lvalues) + 1 

405 

406 # Find the last state with l=l_pol - 1, which will be the state we 

407 # base the polarization function on 

408 for vj, bf in zip(jvalues[::-1], singlezetas[::-1]): 

409 if bf.l == l_pol - 1: 

410 j_pol = vj 

411 rcut = bf.rc * rcutpol_rel 

412 break 

413 else: 

414 raise ValueError('The requested value l_pol=%d requires l=%d ' 

415 'among valence states' % (l_pol, l_pol - 1)) 

416 rcut = min(rcut, rcutmax) 

417 msg = 'Polarization function: l=%d, rc=%.02f' % (l_pol, rcut) 

418 print('\n' + msg, file=txt) 

419 print('-' * len(msg), file=txt) 

420 # Make a single Gaussian for polarization function. 

421 # 

422 # It is known that for given l, the sz cutoff defined 

423 # by some fixed energy is strongly correlated to the 

424 # value of the characteristic radius which best reproduces 

425 # the wave function found by interpolation. 

426 # 

427 # We know that for e.g. d orbitals: 

428 # rchar ~= .37 rcut[sz](.3eV) 

429 # Since we don't want to spend a lot of time finding 

430 # these value for other energies, we just find the energy 

431 # shift at .3 eV now 

432 

433 u, e, de, vconf, rc_fixed = self.rcut_by_energy(j_pol, 

434 .3, 1e-2, 

435 6., (12., .6)) 

436 

437 default_rchar_rel = .25 

438 # Defaults for each l. Actually we don't care right now 

439 rchar_rels = {} 

440 

441 if rcharpol_rel is None: 

442 rcharpol_rel = rchar_rels.get(l_pol, default_rchar_rel) 

443 rchar = rcharpol_rel * rc_fixed 

444 gaussian = QuasiGaussian(1.0 / rchar**2, rcut) 

445 psi_pol = gaussian(rgd.r_g) * rgd.r_g**(l_pol + 1) 

446 norm = np.dot(rgd.dr_g, psi_pol * psi_pol) ** .5 

447 psi_pol /= norm 

448 print('Single quasi Gaussian', file=txt) 

449 msg = f'Rchar = {rcharpol_rel:.3f}*rcut = {rchar:.3f} Bohr' 

450 adjective = 'Gaussian' 

451 print(msg, file=txt) 

452 typestring = 'spdfg'[l_pol] 

453 type = f'{typestring}-type {adjective} polarization' 

454 bf_pol = BasisFunction(None, l_pol, rcut, psi_pol, type) 

455 

456 polarization_functions.append(bf_pol) 

457 for i in range(polarizationcount - 1): 

458 npol = i + 2 

459 levelstring = ['Secondary', 'Tertiary', 'Quaternary', 

460 'Quintary', 'Sextary', 'Septenary'][i] 

461 msg = f'\n{levelstring}: {splitvalencedescr}' 

462 print(msg, file=txt) 

463 rsplit, norm, splitwave = rsplit_by_norm(rgd, l_pol, psi_pol, 

464 tailnorm[i], txt) 

465 descr = ('%s-type split-valence polarization %d' 

466 % ('spdfg'[l_pol], npol)) 

467 bf_pol = BasisFunction(None, l_pol, rsplit, 

468 psi_pol - splitwave, 

469 descr) 

470 polarization_functions.append(bf_pol) 

471 

472 bf_j = [] 

473 bf_j.extend(singlezetas) 

474 bf_j.extend(energy_derivative_functions) 

475 for multizeta_list in multizetas: 

476 bf_j.extend(multizeta_list) 

477 bf_j.extend(polarization_functions) 

478 

479 rcmax = max([bf.rc for bf in bf_j]) 

480 

481 # The non-equidistant grids are really only suited for AE WFs 

482 d = 1.0 / 64 

483 equidistant_grid = np.arange(0.0, rcmax + d, d) 

484 ng = len(equidistant_grid) 

485 

486 for bf in bf_j: 

487 # We have been storing phit_g * r, but we just want phit_g 

488 bf.phit_g = divrl(bf.phit_g, 1, rgd.r_g) 

489 

490 gcut = min(int(1 + bf.rc / d), ng - 1) 

491 

492 assert equidistant_grid[gcut] >= bf.rc 

493 assert equidistant_grid[gcut - 1] <= bf.rc 

494 

495 bf.rc = equidistant_grid[gcut] 

496 # Note: bf.rc *must* correspond to a grid point (spline issues) 

497 bf.ng = gcut + 1 

498 # XXX all this should be done while building the basis vectors, 

499 # not here 

500 

501 # Quick hack to change to equidistant coordinates 

502 spline = rgd.spline(bf.phit_g, rgd.r_g[rgd.floor(bf.rc)], bf.l, 

503 points=100) 

504 bf.phit_g = np.array([spline(r) * r**bf.l 

505 for r in equidistant_grid[:bf.ng]]) 

506 bf.phit_g[-1] = 0. 

507 

508 basistype = get_basis_name(zetacount, polarizationcount) 

509 if self.name is None: 

510 compound_name = basistype 

511 else: 

512 compound_name = f'{self.name}.{basistype}' 

513 

514 basis = Basis( 

515 valdata.symbol, compound_name, 

516 rgd=EquidistantRadialGridDescriptor(d, ng), 

517 bf_j=bf_j, 

518 generatordata=textbuffer.getvalue().strip(), 

519 generatorattrs={'version': version}) 

520 textbuffer.close() 

521 

522 return basis 

523 

524 

525class QuasiGaussian: 

526 """Gaussian-like functions for expansion of orbitals. 

527 

528 Implements f(r) = A [G(r) - P(r)] where:: 

529 

530 G(r) = exp{- alpha r^2} 

531 P(r) = a - b r^2 

532 

533 with (a, b) such that f(rcut) == f'(rcut) == 0. 

534 """ 

535 def __init__(self, alpha, rcut, A=1.): 

536 self.alpha = alpha 

537 self.rcut = rcut 

538 expmar2 = np.exp(-alpha * rcut**2) 

539 a = (1 + alpha * rcut**2) * expmar2 

540 b = alpha * expmar2 

541 self.a = a 

542 self.b = b 

543 self.A = A 

544 

545 def __call__(self, r): 

546 """Evaluate function values at r, which is a numpy array.""" 

547 condition = (r < self.rcut) & (self.alpha * r**2 < 700.) 

548 r2 = np.where(condition, r**2., 0.) # prevent overflow 

549 g = np.exp(-self.alpha * r2) 

550 p = (self.a - self.b * r2) 

551 y = np.where(condition, g - p, 0.) 

552 return self.A * y