Coverage for gpaw/occupations.py: 88%
439 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-09 00:21 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-09 00:21 +0000
1"""Smearing functions and occupation number calculators."""
2from __future__ import annotations
4import warnings
5from math import inf, nan, pi
6from typing import Any, Callable, Dict, List, NamedTuple, Tuple, cast
8import numpy as np
9from ase.units import Ha
10from scipy.special import erf
12from gpaw.band_descriptor import BandDescriptor
13from gpaw.mpi import MPIComm, broadcast_float, serial_comm
14from gpaw.typing import Array1D, Array2D
17class ParallelLayout(NamedTuple):
18 """Collection of parallel stuff."""
19 bd: BandDescriptor
20 kpt_comm: MPIComm
21 domain_comm: MPIComm
24def fermi_dirac(eig: np.ndarray,
25 fermi_level: float,
26 width: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
27 """Fermi-Dirac distribution function.
29 >>> f, _, _ = fermi_dirac(np.array([-1.0, 0.0, 1.0]), 0.0, 0.01)
30 >>> f.round(5)
31 array([1. , 0.5, 0. ])
33 """
34 x = (eig - fermi_level) / width
35 x = np.clip(x, -100, 100)
36 y = np.exp(x)
37 z = y + 1.0
38 f = 1.0 / z
39 dfde = (f - f**2) / width
40 y *= x
41 y /= z
42 y -= np.log(z)
43 e_entropy = y * width
44 return f, dfde, e_entropy
47def marzari_vanderbilt(eig: np.ndarray,
48 fermi_level: float,
49 width: float) -> Tuple[np.ndarray,
50 np.ndarray,
51 np.ndarray]:
52 """Marzari-Vanderbilt distribution (cold smearing).
54 See: :doi:`10.1103/PhysRevLett.82.3296`
55 """
56 x = (eig - fermi_level) / width
57 expterm = np.exp(-(x + (1 / np.sqrt(2)))**2)
58 f = expterm / np.sqrt(2 * np.pi) + 0.5 * (1 - erf(1. / np.sqrt(2) + x))
59 dfde = expterm * (2 + np.sqrt(2) * x) / np.sqrt(np.pi) / width
60 s = expterm * (1 + np.sqrt(2) * x) / (2 * np.sqrt(np.pi))
61 e_entropy = -s * width
62 return f, dfde, e_entropy
65def methfessel_paxton(eig: np.ndarray,
66 fermi_level: float,
67 width: float,
68 order: int = 0) -> Tuple[np.ndarray,
69 np.ndarray,
70 np.ndarray]:
71 """Methfessel-Paxton distribution."""
72 x = (eig - fermi_level) / width
73 f = 0.5 * (1 - erf(x))
74 for i in range(order):
75 f += (coff_function(i + 1) *
76 hermite_poly(2 * i + 1, x) * np.exp(-x**2))
77 dfde = 1 / np.sqrt(pi) * np.exp(-x**2)
78 for i in range(order):
79 dfde += (coff_function(i + 1) *
80 hermite_poly(2 * i + 2, x) * np.exp(-x**2))
81 dfde *= 1.0 / width
82 e_entropy = (0.5 * coff_function(order) *
83 hermite_poly(2 * order, x) * np.exp(-x**2))
84 e_entropy = -e_entropy * width
85 return f, dfde, e_entropy
88def coff_function(n):
89 return (-1)**n / (np.prod(np.arange(1, n + 1)) *
90 4**n * np.sqrt(np.pi))
93def hermite_poly(n, x):
94 if n == 0:
95 return 1
96 elif n == 1:
97 return 2 * x
98 else:
99 return (2 * x * hermite_poly(n - 1, x) -
100 2 * (n - 1) * hermite_poly(n - 2, x))
103class OccupationNumberCalculator:
104 """Base class for all occupation number calculators."""
105 name = 'unknown'
106 extrapolate_factor: float
108 def __init__(self,
109 parallel_layout: ParallelLayout = None):
110 """Object for calculating fermi level(s) and occupation numbers.
112 If fixmagmom=True then the fixed_magmom_value attribute must be set
113 and two fermi levels will be calculated.
114 """
115 if parallel_layout is None:
116 parallel_layout = ParallelLayout(BandDescriptor(1),
117 serial_comm,
118 serial_comm)
119 self.bd = parallel_layout.bd
120 self.kpt_comm = parallel_layout.kpt_comm
121 self.domain_comm = parallel_layout.domain_comm
123 @property
124 def parallel_layout(self) -> ParallelLayout:
125 return ParallelLayout(self.bd, self.kpt_comm, self.domain_comm)
127 def todict(self):
128 return {'name': self.name}
130 def copy(self,
131 parallel_layout: ParallelLayout = None,
132 bz2ibzmap: List[int] = None) -> 'OccupationNumberCalculator':
133 return create_occ_calc(
134 self.todict(),
135 parallel_layout=parallel_layout or self.parallel_layout)
137 def calculate(self,
138 nelectrons: float,
139 eigenvalues: list[list[float]],
140 weights: list[float],
141 fermi_levels_guess: list[float] = None,
142 fix_fermi_level: bool = False) -> tuple[Array2D,
143 list[float],
144 float]:
145 """Calculate occupation numbers and fermi level(s) from eigenvalues.
147 nelectrons:
148 Number of electrons.
149 eigenvalues: ndarray, shape=(nibzkpts, nbands)
150 Eigenvalues in Hartree.
151 weights: ndarray, shape=(nibzkpts,)
152 Weights of k-points in IBZ (must sum to 1).
153 parallel:
154 Parallel distribution of eigenvalues.
155 fermi_level_guesses:
156 Optional guess(es) at fermi level(s).
158 Returns a tuple containing:
160 * occupation numbers (in the range 0 to 1)
161 * fermi-level in Hartree
162 * entropy as -S*T in Hartree
164 >>> occ = ZeroWidth()
165 >>> occ.calculate(1, [[0, 1]], [1])
166 (array([[1., 0.]]), [0.5], 0.0)
167 """
169 eig_qn = [np.asarray(eig_n) for eig_n in eigenvalues]
170 weight_q = np.asarray(weights)
172 if fermi_levels_guess is None:
173 fermi_levels_guess = [nan]
175 f_qn = np.empty((len(weight_q), len(eig_qn[0])))
177 result = np.empty(2)
179 if self.domain_comm.rank == 0:
180 # Let the master domain do the work and broadcast results:
181 result[:] = self._calculate(
182 nelectrons, eig_qn, weight_q, f_qn,
183 fermi_levels_guess[0], fix_fermi_level)
185 self.domain_comm.broadcast(result, 0)
186 self.domain_comm.broadcast(f_qn, 0)
188 fermi_level, e_entropy = result
189 return f_qn, [float(fermi_level)], float(e_entropy)
191 def _calculate(self,
192 nelectrons: float,
193 eig_qn: List[Array1D],
194 weight_q: Array1D,
195 f_qn: Array2D,
196 fermi_level_guess: float,
197 fix_fermi_level: bool = False) -> tuple[float, float]:
198 raise NotImplementedError
201class FixMagneticMomentOccupationNumberCalculator(OccupationNumberCalculator):
202 """Base class for all occupation number objects."""
203 name = 'fixmagmom'
205 def __init__(self,
206 occ: OccupationNumberCalculator,
207 magmom: float):
208 """Object for calculating fermi level(s) and occupation numbers.
210 If fixmagmom=True then the fixed_magmom_value attribute must be set
211 and two fermi levels will be calculated.
212 """
213 self.occ = occ
214 self.fixed_magmom_value = magmom
215 self.extrapolate_factor = occ.extrapolate_factor
217 def __str__(self):
218 return (f'Fixed magnetic moment: {self.fixed_magmom_value:.3f}\n' +
219 str(self.occ))
221 def todict(self):
222 dct = self.occ.todict()
223 dct['fixmagmom'] = True
224 return dct
226 def calculate(self,
227 nelectrons: float,
228 eigenvalues: List[List[float]],
229 weights: List[float],
230 fermi_levels_guess: List[float] = None,
231 fix_fermi_level: bool = False) -> tuple[Array2D,
232 List[float],
233 float]:
235 magmom = self.fixed_magmom_value
237 if fermi_levels_guess is None:
238 fermi_levels_guess = [nan, nan]
240 f1_qn, fermi_levels1, e_entropy1 = self.occ.calculate(
241 (nelectrons + magmom) / 2,
242 eigenvalues[::2],
243 weights[::2],
244 fermi_levels_guess[:1],
245 fix_fermi_level)
247 f2_qn, fermi_levels2, e_entropy2 = self.occ.calculate(
248 (nelectrons - magmom) / 2,
249 eigenvalues[1::2],
250 weights[1::2],
251 fermi_levels_guess[1:],
252 fix_fermi_level)
254 f_qn = []
255 for f1_n, f2_n in zip(f1_qn, f2_qn):
256 f_qn += [f1_n, f2_n]
258 return (np.array(f_qn),
259 fermi_levels1 + fermi_levels2,
260 e_entropy1 + e_entropy2)
263class SmoothDistribution(OccupationNumberCalculator):
264 """Base class for Fermi-Dirac and other smooth distributions."""
265 def __init__(self, width: float, parallel_layout: ParallelLayout = None):
266 """Smooth distribution.
268 width: float
269 Width of distribution in eV.
270 fixmagmom: bool
271 Fix spin moment calculations. A separate Fermi level for
272 spin up and down electrons is found.
273 """
275 self._width = width
276 OccupationNumberCalculator.__init__(self, parallel_layout)
278 def todict(self):
279 return {'name': self.name, 'width': self._width}
281 def _calculate(self,
282 nelectrons,
283 eig_qn,
284 weight_q,
285 f_qn,
286 fermi_level_guess,
287 fix_fermi_level):
288 # Guess can be nan or inf:
289 if not np.isfinite(fermi_level_guess) or self._width == 0.0:
290 zero = ZeroWidth(self.parallel_layout)
291 fermi_level_guess, _ = zero._calculate(
292 nelectrons, eig_qn, weight_q, f_qn)
293 if self._width == 0.0 or np.isinf(fermi_level_guess):
294 return fermi_level_guess, 0.0
296 x = fermi_level_guess
298 data = np.empty(3)
300 def func(x, data=data):
301 """calculate excess electrons (and update occupation numbers)."""
302 data[:] = 0.0
303 for eig_n, weight, f_n in zip(eig_qn, weight_q, f_qn):
304 f_n[:], dfde_n, e_entropy_n = self.distribution(eig_n, x)
305 data += [weight * x_n.sum()
306 for x_n in [f_n, dfde_n, e_entropy_n]]
307 self.bd.comm.sum(data)
308 self.kpt_comm.sum(data)
309 f, dfde = data[:2]
310 df = f - nelectrons
311 return df, dfde
313 if fix_fermi_level:
314 fermi_level = x
315 func(fermi_level)
316 else:
317 fermi_level, niter = findroot(func, x)
319 e_entropy = data[2]
321 return fermi_level, e_entropy
324class FermiDiracCalculator(SmoothDistribution):
325 name = 'fermi-dirac'
326 extrapolate_factor = -0.5
328 def distribution(self,
329 eig_n: np.ndarray,
330 fermi_level: float) -> Tuple[np.ndarray,
331 np.ndarray,
332 np.ndarray]:
333 return fermi_dirac(eig_n, fermi_level, self._width)
335 def __str__(self):
336 return f'Fermi-Dirac:\n width: {self._width:.4f} # eV\n'
339class MarzariVanderbiltCalculator(SmoothDistribution):
340 name = 'marzari-vanderbilt'
341 # According to Nicola Marzari, one should not extrapolate M-V energies
342 # https://lists.quantum-espresso.org/pipermail/users/2005-October/003170.html
343 extrapolate_factor = 0.0
345 def distribution(self, eig_n, fermi_level):
346 return marzari_vanderbilt(eig_n, fermi_level, self._width)
348 def __str__(self):
349 return f'Marzari-Vanderbilt:\n width: {self._width:.4f} # eV\n'
352class MethfesselPaxtonCalculator(SmoothDistribution):
353 name = 'methfessel_paxton'
355 def __init__(self, width, order=0, parallel_layout: ParallelLayout = None):
356 SmoothDistribution.__init__(self, width, parallel_layout)
357 self.order = order
358 self.extrapolate_factor = -1.0 / (self.order + 2)
360 def todict(self):
361 dct = SmoothDistribution.todict(self)
362 dct['order'] = self.order
363 return dct
365 def __str__(self):
366 return ('Methfessel-Paxton:\n'
367 f' width: {self._width:.4f} # eV\n'
368 f' order: {self.order}\n')
370 def distribution(self, eig_n, fermi_level):
371 return methfessel_paxton(eig_n, fermi_level, self._width, self.order)
374def findroot(func: Callable[[float], Tuple[float, float]],
375 x: float,
376 tol: float = 1e-10) -> Tuple[float, int]:
377 """Function used for locating Fermi level.
379 The function should return a (value, derivative) tuple:
381 >>> x, _ = findroot(lambda x: (x, 1.0), 1.0)
382 >>> assert abs(x) < 1e-10
383 """
385 assert np.isfinite(x), x
387 xmin = -np.inf
388 xmax = np.inf
390 # Try 10 step using the gradient:
391 niter = 0
392 while True:
393 f, dfdx = func(x)
394 if abs(f) < tol:
395 return x, niter
396 if f < 0.0 and x > xmin:
397 xmin = x
398 elif f > 0.0 and x < xmax:
399 xmax = x
400 dx = -f / max(dfdx, 1e-18)
401 if niter == 10 or abs(dx) > 0.3 or not (xmin < x + dx < xmax):
402 break # try bisection
403 x += dx
404 niter += 1
406 # Bracket the solution:
407 if not np.isfinite(xmin):
408 xmin = x
409 fmin = f
410 step = 0.01
411 while fmin > tol:
412 xmin -= step
413 fmin = func(xmin)[0]
414 step *= 2
416 if not np.isfinite(xmax):
417 xmax = x
418 fmax = f
419 step = 0.01
420 while fmax < 0:
421 xmax += step
422 fmax = func(xmax)[0]
423 step *= 2
425 # Bisect:
426 while True:
427 x = (xmin + xmax) / 2
428 f = func(x)[0]
429 if abs(f) < tol:
430 return x, niter
431 if f > 0:
432 xmax = x
433 else:
434 xmin = x
435 niter += 1
436 assert niter < 1000
439def collect_eigelvalues(eig_qn: np.ndarray,
440 weight_q: np.ndarray,
441 bd: BandDescriptor,
442 kpt_comm: MPIComm) -> Tuple[np.ndarray,
443 np.ndarray,
444 np.ndarray]:
445 """Collect eigenvalues from bd.comm and kpt_comm."""
446 nkpts_r = np.zeros(kpt_comm.size, int)
447 nkpts_r[kpt_comm.rank] = len(weight_q)
448 kpt_comm.sum(nkpts_r)
449 nk = cast(int, nkpts_r.sum())
450 weight_k = np.zeros(nk)
451 k1 = nkpts_r[:kpt_comm.rank].sum()
452 k2 = k1 + len(weight_q)
453 weight_k[k1:k2] = weight_q
454 kpt_comm.sum(weight_k, 0)
456 eig_kn: Array2D = np.zeros((0, 0))
457 k = 0
458 for rank, nkpts in enumerate(nkpts_r):
459 for q in range(nkpts):
460 if rank == kpt_comm.rank:
461 eig_n = eig_qn[q]
462 eig_n = bd.collect(eig_n)
463 if bd.comm.rank == 0:
464 if kpt_comm.rank == 0:
465 if k == 0:
466 eig_kn = np.empty((nk, len(eig_n)))
467 if rank == 0:
468 eig_kn[k] = eig_n
469 else:
470 kpt_comm.receive(eig_kn[k], rank)
471 elif rank == kpt_comm.rank:
472 kpt_comm.send(eig_n, 0)
473 k += 1
474 return eig_kn, weight_k, nkpts_r
477def distribute_occupation_numbers(f_kn: np.ndarray, # input
478 f_qn: np.ndarray, # output
479 nkpts_r: np.ndarray,
480 bd: BandDescriptor,
481 kpt_comm: MPIComm) -> None:
482 """Distribute occupation numbers over bd.comm and kpt_comm."""
483 k = 0
484 for rank, nkpts in enumerate(nkpts_r):
485 for q in range(nkpts):
486 if kpt_comm.rank == 0:
487 if rank == 0:
488 if bd.comm.size == 1:
489 f_qn[q] = f_kn[k]
490 else:
491 bd.distribute(None if f_kn is None else f_kn[k],
492 f_qn[q])
493 elif f_kn is not None:
494 kpt_comm.send(f_kn[k], rank)
495 elif rank == kpt_comm.rank:
496 if bd.comm.size == 1:
497 kpt_comm.receive(f_qn[q], 0)
498 else:
499 if bd.comm.rank == 0:
500 f_n = bd.empty(global_array=True)
501 kpt_comm.receive(f_n, 0)
502 else:
503 f_n = None
504 bd.distribute(f_n, f_qn[q])
505 k += 1
508class ZeroWidth(OccupationNumberCalculator):
509 name = 'zero-width'
510 extrapolate_factor = 0.0
512 def todict(self):
513 return {'width': 0.0}
515 def __str__(self):
516 return '# Zero width'
518 def distribution(self, eig_n, fermi_level):
519 f_n = np.zeros_like(eig_n)
520 f_n[eig_n < fermi_level] = 1.0
521 f_n[eig_n == fermi_level] = 0.5
522 return f_n, np.zeros_like(eig_n), np.zeros_like(eig_n)
524 def _calculate(self,
525 nelectrons,
526 eig_qn,
527 weight_q,
528 f_qn,
529 fermi_level_guess=nan,
530 fix_fermi_level=False):
531 eig_kn, weight_k, nkpts_r = collect_eigelvalues(eig_qn, weight_q,
532 self.bd, self.kpt_comm)
534 if eig_kn.size != 0:
535 # Try to use integer weights (avoid round-off errors):
536 N = int(round(1 / min(weight_k)))
537 w_k = (weight_k * N).round().astype(int)
538 if abs(w_k - N * weight_k).max() > 1e-10:
539 # Did not work. Use original fractional weights:
540 w_k = weight_k
541 N = 1
543 f_kn = np.zeros_like(eig_kn)
544 f_m = f_kn.ravel()
545 w_kn = np.empty_like(eig_kn, dtype=w_k.dtype)
546 w_kn[:] = w_k[:, np.newaxis]
547 eig_m = eig_kn.ravel()
548 w_m = w_kn.ravel()
549 m_i = eig_m.argsort()
550 if fix_fermi_level:
551 fermi_level = fermi_level_guess
552 f_m[eig_m < fermi_level] = 1.0
553 f_m[eig_m == fermi_level] = 0.5
554 else:
555 w_i = w_m[m_i]
556 sum_i = np.add.accumulate(w_i)
557 filled_i = (sum_i <= nelectrons * N)
558 i = sum(filled_i)
559 f_m[m_i[:i]] = 1.0
560 if i == len(m_i):
561 fermi_level = inf
562 else:
563 extra = nelectrons * N - (sum_i[i - 1] if i > 0 else 0.0)
564 if extra > 0:
565 assert extra <= w_i[i]
566 f_m[m_i[i]] = extra / w_i[i]
567 fermi_level = eig_m[m_i[i]]
568 else:
569 fermi_level = (eig_m[m_i[i]] + eig_m[m_i[i - 1]]) / 2
570 else:
571 f_kn = None
572 fermi_level = nan
574 distribute_occupation_numbers(f_kn, f_qn, nkpts_r,
575 self.bd, self.kpt_comm)
577 if self.kpt_comm.rank == 0:
578 fermi_level = broadcast_float(fermi_level, self.bd.comm)
579 fermi_level = broadcast_float(fermi_level, self.kpt_comm)
581 e_entropy = 0.0
582 return fermi_level, e_entropy
585class FixedOccupationNumbers(OccupationNumberCalculator):
586 extrapolate_factor = 0.0
588 def __init__(self, numbers, parallel_layout: ParallelLayout = None):
589 """Fixed occupation numbers.
591 f_sn: ndarray, shape=(nspins, nbands)
592 Occupation numbers (in the range from 0 to 1)
594 Example (excited state with 4 electrons)::
596 occ = FixedOccupationNumbers([[1, 0, 1, 0], [1, 1, 0, 0]])
598 """
599 OccupationNumberCalculator.__init__(self, parallel_layout)
600 self.f_sn = np.array(numbers)
602 def _calculate(self,
603 nelectrons,
604 eig_qn,
605 weight_q,
606 f_qn,
607 fermi_level_guess=nan,
608 fix_fermi_level=False):
610 calc_fixed(self.bd, self.f_sn, f_qn)
612 return inf, 0.0
614 def todict(self):
615 return {'name': 'fixed', 'numbers': self.f_sn}
618class FixedOccupationNumbersUniform(OccupationNumberCalculator):
620 extrapolate_factor = 0.0
621 name = 'fixed-uniform'
623 def __init__(self, nelectrons, nspins, magmom, nkpts, nbands,
624 parallel_layout: ParallelLayout = None):
625 """
626 Uniform distribution of occupation numbers: each
627 k-point per spin has the same number of occupied states
628 Magnetic moment defines difference between two spins occ. numb.
629 """
630 OccupationNumberCalculator.__init__(self, parallel_layout)
632 def get_f(nelectrons, magmom, nkpts, nbands, spin):
633 """
634 :param nelectrons:
635 :param magmom:
636 :param nkpts:
637 :param nbands:
638 :param spin: +1 or -1
639 :return: occupation numbers per spin channel
640 """
642 f_qn = np.zeros(shape=(nkpts, nbands))
643 nelecps = (nelectrons + spin * magmom) / 2
644 assert int(nelecps) < nbands, 'need more bands!'
646 f_qn[:, : int(nelecps)] = 1.0
647 f_qn[:, int(nelecps)] = nelecps - int(nelecps)
649 return f_qn
651 if nspins == 2:
652 f1_qn = get_f(nelectrons, magmom, nkpts, nbands, 1)
653 f2_qn = get_f(nelectrons, magmom, nkpts, nbands, -1)
654 f_qn = []
655 for f1_n, f2_n in zip(f1_qn, f2_qn):
656 f_qn += [f1_n, f2_n]
657 else:
658 f_qn = get_f(nelectrons, magmom, nkpts, nbands, 0)
660 self.f_sn = np.array(f_qn)
661 self.nspins = nspins
662 self.magmom = magmom
664 def _calculate(self,
665 nelectrons,
666 eig_qn,
667 weight_q,
668 f_qn,
669 fermi_level_guess=nan,
670 fix_fermi_level=False):
671 assert not fix_fermi_level
673 calc_fixed(self.bd, self.f_sn, f_qn)
675 eig_kn, weight_k, nkpts_r = collect_eigelvalues(
676 eig_qn, weight_q, self.bd, self.kpt_comm)
678 def get_homo(eig_kn, nelectrons, deg, magmom, spin):
679 nelecps = int((nelectrons * deg + spin * magmom) / 2)
680 return np.max(eig_kn[:, np.maximum(nelecps - 1, 0)])
682 def get_lumo(eig_kn, nelectrons, deg, magmom, spin):
683 nelecps = int((nelectrons * deg + spin * magmom) / 2)
684 return np.min(eig_kn[:, nelecps])
686 deg = 3 - self.nspins
687 mm = self.magmom
688 if eig_kn.size != 0:
689 if self.nspins == 2:
690 hup = get_homo(eig_kn[::2], nelectrons, deg, mm, 1)
691 hdown = get_homo(eig_kn[1::2], nelectrons, deg, mm, -1)
692 homo = np.maximum(hup, hdown)
694 lup = get_lumo(eig_kn[::2], nelectrons, deg, mm, 1)
695 ldown = get_lumo(eig_kn[1::2], nelectrons, deg, mm, -1)
696 lumo = np.maximum(lup, ldown)
698 else:
699 homo = get_homo(eig_kn, nelectrons, deg, mm, 0)
700 lumo = get_lumo(eig_kn, nelectrons, deg, mm, 0)
701 fermi_level = (homo + lumo) / 2
702 else:
703 fermi_level = nan
705 if self.kpt_comm.rank == 0:
706 fermi_level = broadcast_float(fermi_level, self.bd.comm)
707 fermi_level = broadcast_float(fermi_level, self.kpt_comm)
709 return fermi_level, 0.0
711 def todict(self):
712 return {'name': 'fixed-uniform'}
714 def __str__(self):
715 return '# Uniform distribution of occupation numbers'
718def calc_fixed(bd, f_sn, f_qn):
719 if bd.nbands == f_sn.shape[1]:
720 for q, f_n in enumerate(f_qn):
721 s = q % len(f_sn)
722 bd.distribute(f_sn[s], f_n)
723 else:
724 # Non-collinear calculation:
725 bd.distribute(f_sn.T.flatten().copy(), f_qn[0])
728def FixedOccupations(f_sn):
729 warnings.warn(
730 "Please use occupations={'name': 'fixed', 'numbers': ...} instead.")
731 if len(f_sn) == 1:
732 f_sn = np.array(f_sn) / 2
733 return {'name': 'fixed', 'numbers': f_sn}
736class ThomasFermiOccupations(OccupationNumberCalculator):
737 name = 'orbital-free'
738 extrapolate_factor = 0.0
740 def _calculate(self,
741 nelectrons,
742 eig_qn,
743 weight_q,
744 f_qn,
745 fermi_level_guess=nan,
746 fix_fermi_level=False):
747 assert len(f_qn) == 1
748 f_qn[0][:] = [nelectrons]
749 return inf, 0.0
752def create_occ_calc(dct: Dict[str, Any],
753 *,
754 parallel_layout: ParallelLayout = None,
755 fixed_magmom_value=None,
756 rcell=None,
757 monkhorst_pack_size=None,
758 bz2ibzmap=None,
759 nspins=None,
760 nelectrons=None,
761 nkpts=None,
762 nbands=None
763 ) -> OccupationNumberCalculator:
764 """Surprise: Create occupation-number object.
766 The unit of width is eV and name must be one of:
768 * 'fermi-dirac'
769 * 'marzari-vanderbilt'
770 * 'methfessel-paxton'
771 * 'fixed'
772 * 'tetrahedron-method'
773 * 'improved-tetrahedron-method'
774 * 'orbital-free'
776 >>> occ = create_occ_calc({'width': 0.0})
777 >>> occ.calculate(nelectrons=3,
778 ... eigenvalues=[[0, 1, 2], [0, 2, 3]],
779 ... weights=[1, 1])
780 (array([[1., 1., 0.],
781 [1., 0., 0.]]), [1.5], 0.0)
782 """
783 kwargs = dct.copy()
784 fix_the_magnetic_moment = kwargs.pop('fixmagmom', False)
785 name = kwargs.pop('name', '')
786 kwargs['parallel_layout'] = parallel_layout
788 if name == 'unknown':
789 return OccupationNumberCalculator(**kwargs)
791 occ: OccupationNumberCalculator
793 if kwargs.get('width') == 0.0:
794 del kwargs['width']
795 occ = ZeroWidth(**kwargs)
796 elif name == 'methfessel-paxton':
797 occ = MethfesselPaxtonCalculator(**kwargs)
798 elif name == 'fermi-dirac':
799 occ = FermiDiracCalculator(**kwargs)
800 elif name == 'marzari-vanderbilt':
801 occ = MarzariVanderbiltCalculator(**kwargs)
802 elif name in {'tetrahedron-method', 'improved-tetrahedron-method'}:
803 from gpaw.tetrahedron import TetrahedronMethod
804 occ = TetrahedronMethod(rcell,
805 monkhorst_pack_size,
806 name == 'improved-tetrahedron-method',
807 bz2ibzmap,
808 **kwargs)
809 elif name == 'orbital-free':
810 return ThomasFermiOccupations(**kwargs)
811 elif name == 'fixed':
812 return FixedOccupationNumbers(**kwargs)
813 elif name == 'fixed-uniform':
814 return FixedOccupationNumbersUniform(
815 nelectrons, nspins, fixed_magmom_value,
816 nkpts, nbands, **kwargs)
817 else:
818 raise ValueError(f'Unknown occupation number object name: {name}')
820 if fix_the_magnetic_moment:
821 occ = FixMagneticMomentOccupationNumberCalculator(
822 occ, fixed_magmom_value)
824 return occ
827def occupation_numbers(occ, eig_skn, weight_k, nelectrons):
828 """Calculate occupation numbers from eigenvalues in eV (**deprecated**).
830 occ: dict
831 Example: {'name': 'fermi-dirac', 'width': 0.05} (width in eV).
832 eps_skn: ndarray, shape=(nspins, nibzkpts, nbands)
833 Eigenvalues.
834 weight_k: ndarray, shape=(nibzkpts,)
835 Weights of k-points in IBZ (must sum to 1).
836 nelectrons: int or float
837 Number of electrons.
839 Returns a tuple containing:
841 * f_skn (sums to nelectrons)
842 * fermi-level [Hartree]
843 * magnetic moment
844 * entropy as -S*T [Hartree]
845 """
847 warnings.warn('Please use one of the OccupationNumbers implementations',
848 DeprecationWarning)
849 occ = create_occ_calc(occ)
850 f_kn, (fermi_level,), e_entropy = occ.calculate(
851 nelectrons * len(eig_skn) / 2,
852 [eig_n for eig_kn in eig_skn for eig_n in eig_kn],
853 list(weight_k) * len(eig_skn))
855 f_kn *= np.array(weight_k)[:, np.newaxis]
857 if len(eig_skn) == 1:
858 f_skn = np.array([f_kn]) * 2
859 e_entropy *= 2
860 magmom = 0.0
861 else:
862 f_skn = np.array([f_kn[::2], f_kn[1::2]])
863 f1, f2 = f_skn.sum(axis=(1, 2))
864 magmom = f1 - f2
866 return f_skn, fermi_level * Ha, magmom, e_entropy * Ha
869def FermiDirac(width, fixmagmom=False):
870 return dict(name='fermi-dirac', width=width, fixmagmom=fixmagmom)
873def MarzariVanderbilt(width, fixmagmom=False):
874 return dict(name='marzari-vanderbilt', width=width, fixmagmom=fixmagmom)
877def MethfesselPaxton(width, order=0, fixmagmom=False):
878 return dict(name='methfessel-paxton', width=width, order=order,
879 fixmagmom=fixmagmom)