Coverage for gpaw/hgh.py: 79%
367 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
1import hashlib
2from typing import Dict
4import numpy as np
5from ase.data import atomic_numbers
7from gpaw.utilities import pack_hermitian
8from gpaw.atom.radialgd import AERadialGridDescriptor
9from gpaw.atom.configurations import configurations
10from gpaw.pseudopotential import PseudoPotential, get_radial_hartree_energy
13setups: Dict[str, 'HGHParameterSet'] = {} # Filled out during parsing below
14sc_setups: Dict[str, 'HGHParameterSet'] = {} # Semicore
17# Tabulated values of Gamma(m + 1/2)
18half_integer_gamma = [np.sqrt(np.pi)]
19for m in range(20):
20 half_integer_gamma.append(half_integer_gamma[m] * (m + 0.5))
23class HGHSetupData:
24 """Setup-compatible class implementing HGH pseudopotential.
26 To the PAW code this will appear as a legit PAW setup, but is
27 in fact considerably simpler. In particular, all-electron and
28 pseudo partial waves are all zero, and compensation charges do not
29 depend on environment.
31 A HGH setup has the following form::
33 ----
34 \
35 V = Vlocal + ) | p > h < p |
36 / i ij j
37 ----
38 ij
40 Vlocal contains a short-range term which is Gaussian-shaped and
41 implemented as vbar of a PAW setup, along with a long-range term
42 which goes like 1/r and is implemented in terms of a compensation
43 charge.
45 The non-local part contains KB projector functions which are
46 essentially similar to those in PAW, while h_ij are constants.
47 h_ij are provided by setting the K_p variable of the normal
48 setup.
50 Most other properties of a PAW setup do not exist for HGH setups, for
51 which reason they are generally set to zero:
53 * All-electron partial waves: always zero
54 * Pseudo partial waves: always zero
55 * Projectors: HGH projectors
56 * Zero potential (vbar): Gaussian times polynomial
57 * Compensation charges: One Gaussian-shaped spherically symmetric charge
58 * All-electron core density: Delta function corresponding to core electron
59 charge
60 * Pseudo core density: always zero
61 * Pseudo valence density: always zero
62 * PS/AE Kinetic energy density: always zero
63 * The mysterious constants K_p of a setup correspond to h_ij.
65 Note that since the pseudo partial waves are set to zero,
66 initialization of atomic orbitals requires loading a custom basis
67 set.
69 Absolute energies become numerically large since no atomic
70 reference is subtracted.
71 """
72 def __init__(self, hghdata):
73 if isinstance(hghdata, str):
74 symbol = hghdata
75 if symbol.endswith('.sc'):
76 hghdata = sc_setups[symbol[:-3]]
77 else:
78 hghdata = setups[symbol]
79 self.hghdata = hghdata
81 chemsymbol = hghdata.symbol
82 if '.' in chemsymbol:
83 chemsymbol, sc = chemsymbol.split('.')
84 assert sc == 'sc'
85 self.symbol = chemsymbol
86 self.type = hghdata.symbol
87 self.name = 'LDA'
88 self.initialize_setup_data()
90 def initialize_setup_data(self):
91 hghdata = self.hghdata
92 beta = 0.1
93 N = 450
94 rgd = AERadialGridDescriptor(beta / N, 1.0 / N, N,
95 default_spline_points=100)
96 # rgd = EquidistantRadialGridDescriptor(0.001, 10000)
97 self.rgd = rgd
99 self.Z = hghdata.Z
100 self.Nc = hghdata.Z - hghdata.Nv
101 self.Nv = hghdata.Nv
102 self.rcgauss = np.sqrt(2.0) * hghdata.rloc
104 threshold = 1e-8
105 if len(hghdata.c_n) > 0:
106 vloc_g = create_local_shortrange_potential(rgd.r_g, hghdata.rloc,
107 hghdata.c_n)
108 gcutvbar, rcutvbar = self.find_cutoff(rgd.r_g, rgd.dr_g, vloc_g,
109 threshold)
110 self.vbar_g = np.sqrt(4.0 * np.pi) * vloc_g[:gcutvbar]
111 else:
112 rcutvbar = 0.5
113 gcutvbar = rgd.ceil(rcutvbar)
114 self.vbar_g = np.zeros(gcutvbar)
116 nj = sum([v.nn for v in hghdata.v_l])
117 if nj == 0:
118 nj = 1 # Code assumes nj > 0 elsewhere, we fill out with zeroes
120 if not hghdata.v_l:
121 # No projectors. But the remaining code assumes that everything
122 # has projectors! We'll just add the zero function then
123 hghdata.v_l = [VNonLocal(0, 0.01, [0.])]
125 n_j = []
126 l_j = []
128 # j ordering is significant, must be nl rather than ln
129 for n, l in self.hghdata.nl_iter():
130 n_j.append(n + 1) # Note: actual n must be positive!
131 l_j.append(l)
132 assert nj == len(n_j)
133 self.nj = nj
134 self.l_j = l_j
135 self.l_orb_J = l_j
136 self.n_j = n_j
138 self.rcut_j = []
139 self.pt_jg = []
141 for n, l in zip(n_j, l_j):
142 # Note: even pseudopotentials without projectors will get one
143 # projector, but the coefficients h_ij should be zero so it
144 # doesn't matter
145 pt_g = create_hgh_projector(rgd.r_g, l, n, hghdata.v_l[l].r0)
146 norm = np.sqrt(np.dot(rgd.dr_g, pt_g**2 * rgd.r_g**2))
147 assert np.abs(1 - norm) < 1e-5, str(1 - norm)
148 gcut, rcut = self.find_cutoff(rgd.r_g, rgd.dr_g, pt_g, threshold)
149 if rcut < 0.5:
150 rcut = 0.5
151 gcut = rgd.ceil(rcut)
152 pt_g = pt_g[:gcut].copy()
153 rcut = max(rcut, 0.5)
154 self.rcut_j.append(rcut)
155 self.pt_jg.append(pt_g)
157 # This is the correct magnitude of the otherwise normalized
158 # compensation charge
159 self.Delta0 = -self.Nv / np.sqrt(4.0 * np.pi)
161 f_ln = self.hghdata.get_occupation_numbers()
162 f_j = [0] * nj
163 for j, (n, l) in enumerate(self.hghdata.nl_iter()):
164 try:
165 f_j[j] = f_ln[l][n]
166 except IndexError:
167 pass
168 self.f_ln = f_ln
169 self.f_j = f_j
171 r_g, lcomp, ghat = self.get_compensation_charge_functions()
172 assert lcomp == [0] and len(ghat) == 1
173 renormalized_ghat = self.Nv / (4.0 * np.pi) * ghat[0]
174 self.Eh_compcharge = get_radial_hartree_energy(r_g, renormalized_ghat)
176 def find_cutoff(self, r_g, dr_g, f_g, sqrtailnorm=1e-5):
177 g = len(r_g)
178 acc_sqrnorm = 0.0
179 while acc_sqrnorm <= sqrtailnorm:
180 g -= 1
181 acc_sqrnorm += (r_g[g] * f_g[g])**2.0 * dr_g[g]
182 if r_g[g] < 0.5: # XXX
183 return g, r_g[g]
184 return g, r_g[g]
186 def expand_hamiltonian_matrix(self):
187 """Construct K_p from individual h_nn for each l."""
188 ni = sum([2 * l + 1 for l in self.l_j])
190 H_ii = np.zeros((ni, ni))
192 # The H_ii used in gpaw is much larger and more general than the one
193 # required for HGH pseudopotentials. This means a lot of the elements
194 # must be assigned the same value. Not a performance issue though,
195 # since these are small matrices
196 M1start = 0
197 for n1, l1 in self.hghdata.nl_iter():
198 M1end = M1start + 2 * l1 + 1
199 M2start = 0
200 v = self.hghdata.v_l[l1]
201 for n2, l2 in self.hghdata.nl_iter():
202 M2end = M2start + 2 * l2 + 1
203 if l1 == l2:
204 h_nn = v.expand_hamiltonian_diagonal()
205 H_mm = np.identity(M2end - M2start) * h_nn[n1, n2]
206 H_ii[M1start:M1end, M2start:M2end] += H_mm
207 M2start = M2end
208 M1start = M1end
209 K_p = pack_hermitian(H_ii)
210 return K_p
212 def __str__(self):
213 return "HGHSetupData('%s')" % self.type
215 def __repr__(self):
216 return self.__str__()
218 def print_info(self, text, _setup):
219 self.hghdata.print_info(text)
221 def plot(self):
222 """Plot localized functions of HGH setup."""
223 import matplotlib.pyplot as plt
224 rgd = self.rgd
226 plt.subplot(211) # vbar, compensation charge
227 gcutvbar = len(self.vbar_g)
228 plt.plot(rgd.r_g[:gcutvbar], self.vbar_g, 'r', label='vloc',
229 linewidth=3)
230 rcc, gcc = self.get_compensation_charge_functions()
231 gcc = gcc[0]
233 plt.plot(rcc, gcc * self.Delta0, 'b--',
234 label='Comp charge [arb. unit]',
235 linewidth=3)
236 plt.legend(loc='best')
238 plt.subplot(212) # projectors
239 for j, (n, l, pt_g) in enumerate(zip(self.n_j, self.l_j, self.pt_jg)):
240 label = 'n=%d, l=%d' % (n, l)
241 plt.ylabel('$p_n^l(r)$')
242 ng = len(pt_g)
243 r_g = rgd.r_g[:ng]
244 plt.plot(r_g, pt_g, label=label)
245 plt.legend()
247 def create_basis_functions(self):
248 from gpaw.pseudopotential import generate_basis_functions
249 return generate_basis_functions(self)
251 def get_compensation_charge_functions(self):
252 alpha = self.rcgauss**-2
254 rcutgauss = self.rcgauss * 5.0
255 # smaller values break charge conservation
257 r = np.linspace(0.0, rcutgauss, 100)
258 g = alpha**1.5 * np.exp(-alpha * r**2) * 4.0 / np.sqrt(np.pi)
259 g[-1] = 0.0
260 return r, [0], [g]
262 def get_projectors(self):
263 # XXX equal-range projectors still required for some reason
264 maxlen = max([len(pt_g) for pt_g in self.pt_jg])
265 pt_j = []
266 for l, pt1_g in zip(self.l_j, self.pt_jg):
267 pt2_g = self.rgd.zeros()[:maxlen]
268 pt2_g[:len(pt1_g)] = pt1_g
269 pt_j.append(self.rgd.spline(pt2_g, self.rgd.r_g[maxlen - 1], l))
270 return pt_j
272 def get_local_potential(self):
273 n = len(self.vbar_g)
274 return self.rgd.spline(self.vbar_g, self.rgd.r_g[n - 1], l=0)
276 def build(self, xcfunc, lmax, basis, filter=None):
277 if basis is None:
278 basis = self.create_basis_functions()
279 setup = PseudoPotential(self, basis)
280 setup.fingerprint = hashlib.md5(str(self.hghdata).encode()).hexdigest()
281 return setup
284def create_local_shortrange_potential(r_g, rloc, c_n):
285 rr_g = r_g / rloc # "Relative r"
286 rr2_g = rr_g**2
287 rr4_g = rr2_g**2
288 rr6_g = rr4_g * rr2_g
290 gaussianpart = np.exp(-.5 * rr2_g)
291 polypart = np.zeros(r_g.shape)
292 for c, rrn_g in zip(c_n, [1, rr2_g, rr4_g, rr6_g]):
293 polypart += c * rrn_g
295 vloc_g = gaussianpart * polypart
296 return vloc_g
299def create_hgh_projector(r_g, l, n, r0):
300 poly_g = r_g**(l + 2 * (n - 1))
301 gauss_g = np.exp(-.5 * r_g**2 / r0**2)
302 A = r0**(l + (4 * n - 1) / 2.0)
303 assert (4 * n - 1) % 2 == 1
304 B = half_integer_gamma[l + (4 * n - 1) // 2]**.5
305 pt_g = 2.**.5 / A / B * poly_g * gauss_g
306 return pt_g
309# Coefficients determining off-diagonal elements of h_nn for l = 0...2
310# given the diagonal elements
311hcoefs_l = [
312 [-.5 * (3. / 5.)**.5, .5 * (5. / 21.)**.5, -.5 * (100. / 63.)**.5],
313 [-.5 * (5. / 7.)**.5, 1. / 6. * (35. / 11.)**.5, -1. / 6. * 14. / 11.**.5],
314 [-.5 * (7. / 9.)**.5, .5 * (63. / 143)**.5, -.5 * 18. / 143.**.5]]
317class VNonLocal:
318 """Wrapper class for one nonlocal term of an HGH potential."""
319 def __init__(self, l, r0, h_n):
320 self.l = l
321 self.r0 = r0
322 h_n = np.array(h_n)
323 nn = len(h_n)
324 self.nn = nn
325 self.h_n = h_n
327 def expand_hamiltonian_diagonal(self):
328 """Construct full atomic Hamiltonian from diagonal elements."""
329 nn = self.nn
330 h_n = self.h_n
331 h_nn = np.zeros((nn, nn))
332 for n, h in enumerate(h_n):
333 h_nn[n, n] = h
334 if self.l > 2:
335 # print 'Warning: no diagonal elements for l=%d' % l
336 # Some elements have projectors corresponding to l=3, but
337 # the HGH article only specifies how to calculate the
338 # diagonal elements of the atomic hamiltonian for l = 0, 1, 2 !
339 return
340 coefs = hcoefs_l[self.l]
341 if nn > 2:
342 h_nn[0, 2] = h_nn[2, 0] = coefs[1] * h_n[2]
343 h_nn[1, 2] = h_nn[2, 1] = coefs[2] * h_n[2]
344 if nn > 1:
345 h_nn[0, 1] = h_nn[1, 0] = coefs[0] * h_n[1]
346 return h_nn
348 def copy(self):
349 return VNonLocal(self.l, self.r0, self.h_n.copy())
351 def serialize(self): # no spin-orbit part
352 return ' '.join([' ', '%-10s' % self.r0] +
353 ['%10f' % h for h in self.h_n])
356class HGHParameterSet:
357 """Wrapper class for HGH-specific data corresponding to one element."""
358 def __init__(self, symbol, Z, Nv, rloc, c_n, v_l):
359 self.symbol = symbol # Identifier, e.g. 'Na', 'Na.sc', ...
360 self.Z = Z # Actual atomic number
361 self.Nv = Nv # Valence electron count
362 self.rloc = rloc # Characteristic radius of local part
363 self.c_n = np.array(c_n) # Polynomial coefficients for local part
364 self.v_l = list(v_l) # Non-local parts
366 Z, nlfe_j = configurations[self.symbol.split('.')[0]]
367 self.configuration = nlfe_j
369 def __str__(self):
370 strings = ['HGH setup for %s\n' % self.symbol,
371 ' Valence Z=%d, rloc=%.05f\n' % (self.Nv, self.rloc)]
373 if len(self.c_n) > 0:
374 coef_string = ', '.join(['%.05f' % c for c in self.c_n])
375 else:
376 coef_string = 'zeros'
377 strings.append(' Local part coeffs: %s\n' % coef_string)
378 strings.append(' Projectors:\n')
379 if not self.v_l:
380 strings.append(' None\n')
381 for v in self.v_l:
382 strings.append(' l=%d, rc=%.05f\n' % (v.l, v.r0))
383 strings.append(' Diagonal coefficients of nonlocal parts:')
384 if not self.v_l:
385 strings.append('\n None\n')
386 for v in self.v_l:
387 strings.append('\n')
388 strings.append(' l=%d: ' % v.l +
389 ', '.join(['%8.05f' % h for h in v.h_n]))
390 return ''.join(strings)
392 def copy(self):
393 other = HGHParameterSet(self.symbol, self.Z, self.Nv, self.rloc,
394 self.c_n, self.v_l)
395 return other
397 def print_info(self, txt):
398 txt(str(self))
399 txt()
401 def nl_iter(self):
402 for n in range(4):
403 for l, v in enumerate(self.v_l):
404 if n < v.nn:
405 yield n, l
407 def get_occupation_numbers(self):
408 nlfe_j = list(self.configuration)
409 nlfe_j.reverse()
410 f_ln = [[], [], []] # [[s], [p], [d]]
411 # f states will be ignored as the atomic Hamiltonians
412 # of those are, carelessly, not defined in the article.
413 lmax = len(self.v_l) - 1
414 Nv = 0
415 # Right. We need to find the occupation numbers of each state and
416 # put them into a nice list of lists f_ln.
417 #
418 # We loop over states starting with the least bound one
419 # (i.e. reversed nlfe_j), adding the occupation numbers of each state
420 # as appropriate. Once we have the right number of electrons, we
421 # end the loop.
422 #
423 # Some states in the standard configuration might
424 # be f-type; these should be skipped (unless the HGH setup actually
425 # has a valence f-state; however as noted above, some of the
426 # parameters are undefined in that case so are ignored anyway). More
427 # generally if for some state l > lmax,
428 # we can skip that state.
429 for n, l, f, e in nlfe_j:
430 if l > lmax:
431 continue
432 Nv += f
433 f_n = f_ln[l]
434 assert f_n == [] or self.symbol.endswith('.sc')
435 f_n.append(f)
436 if Nv >= self.Nv:
437 break
438 assert Nv == self.Nv
439 return f_ln
441 def zeropad(self):
442 """Return a new HGHParameterSet with all arrays zero padded so they
443 have the same (max) length for all such HGH setups. Makes
444 plotting multiple HGH setups easier because they have compatible
445 arrays."""
446 c_n = np.zeros(4)
447 for n, c in enumerate(self.c_n):
448 c_n[n] = c
449 v_l = []
450 for l, v in enumerate(self.v_l):
451 h_n = np.zeros(3)
452 h_n[:len(v.h_n)] = list(v.h_n)
453 v2 = VNonLocal(l, v.r0, h_n)
454 v_l.append(v2)
455 for l in range(len(self.v_l), 3):
456 v_l.append(VNonLocal(l, 0.5, np.zeros(3)))
457 copy = HGHParameterSet(self.symbol, self.Z, self.Nv, self.rloc, c_n,
458 v_l)
459 return copy
461 def serialize(self):
462 string1 = '%-5s %-12s %10s ' % (self.symbol, self.Z, self.rloc)
463 string2 = ' '.join(['%.10s' % c for c in self.c_n])
464 nonlocal_strings = [v.serialize() for v in self.v_l]
465 return '\n'.join([string1 + string2] + nonlocal_strings)
468def parse_local_part(string):
469 """Create HGHParameterSet object with local part initialized."""
470 tokens = iter(string.split())
471 symbol = next(tokens)
472 actual_chemical_symbol = symbol.split('.')[0]
473 Z = atomic_numbers[actual_chemical_symbol]
474 Nv = int(next(tokens))
475 rloc = float(next(tokens))
476 c_n = [float(token) for token in tokens]
477 return symbol, Z, Nv, rloc, c_n
480class HGHBogusNumbersError(ValueError):
481 """Error which is raised when the HGH parameters contain f-type
482 or higher projectors. The HGH article only defines atomic Hamiltonian
483 matrices up to l=2, so these are meaningless."""
484 pass
487def parse_hgh_setup(lines):
488 """Initialize HGHParameterSet object from text representation."""
489 lines = iter(lines)
490 symbol, Z, Nv, rloc, c_n = parse_local_part(next(lines))
492 def pair_up_nonlocal_lines(lines):
493 try:
494 yield (next(lines), '')
495 while True:
496 yield (next(lines), next(lines))
497 except StopIteration:
498 return
500 v_l = []
501 for l, (non_local, spinorbit) in enumerate(pair_up_nonlocal_lines(lines)):
502 # we discard the spinorbit 'k_n' data so far
503 nltokens = non_local.split()
504 r0 = float(nltokens[0])
505 h_n = [float(token) for token in nltokens[1:]]
507 # if h_n[-1] == 0.0: # Only spin-orbit contributes. Discard.
508 # h_n.pop()
509 # Actually the above causes trouble. Probably it messes up state
510 # ordering or something else that shouldn't have any effect.
512 vnl = VNonLocal(l, r0, h_n)
513 v_l.append(vnl)
514 if l > 2:
515 raise HGHBogusNumbersError
517 hgh = HGHParameterSet(symbol, Z, Nv, rloc, c_n, v_l)
518 return hgh
521def str2hgh(string):
522 return parse_hgh_setup(string.splitlines())
525def hgh2str(hgh):
526 return hgh.serialize()
529def parse_setups(lines):
530 """Read HGH data from file."""
531 setups = {}
532 entry_lines = [i for i in range(len(lines))
533 if lines[i][0].isalpha()]
534 lines_by_element = [lines[entry_lines[i]:entry_lines[i + 1]]
535 for i in range(len(entry_lines) - 1)]
536 lines_by_element.append(lines[entry_lines[-1]:])
538 for elines in lines_by_element:
539 try:
540 hgh = parse_hgh_setup(elines)
541 except HGHBogusNumbersError:
542 continue
543 assert hgh.symbol not in setups
544 setups[hgh.symbol] = hgh
545 return setups
548def plot(symbol, extension=None):
549 import matplotlib.pyplot as plt
550 try:
551 s = HGHSetupData(symbol)
552 except IndexError:
553 print('Nooooo')
554 return
555 s.plot()
556 if extension is not None:
557 plt.savefig(f'hgh.{symbol}.{extension}')
560def plot_many(*symbols):
561 import matplotlib.pyplot as plt
562 if not symbols:
563 symbols = setups.keys() + [key + '.sc' for key in sc_setups.keys()]
564 for symbol in symbols:
565 plt.figure(1)
566 plot(symbol, extension='png')
567 plt.clf()
570def parse_default_setups():
571 from gpaw.hgh_parameters import parameters
572 lines = parameters.splitlines()
573 setups0 = parse_setups(lines)
574 for key, value in setups0.items():
575 if key.endswith('.sc'):
576 sym, sc = key.split('.')
577 sc_setups[sym] = value
578 else:
579 setups[key] = value
582parse_default_setups()