Coverage for eminus/tools.py: 96.12%
206 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-21 12:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-21 12:19 +0000
1# SPDX-FileCopyrightText: 2021 The eminus developers
2# SPDX-License-Identifier: Apache-2.0
3"""Various tools to check physical properties."""
5import math
6import numbers
8import numpy as np
9from scipy.optimize import minimize_scalar, root_scalar
11from . import backend as xp
12from .dft import get_epsilon, get_epsilon_unocc
13from .gga import get_grad_field, get_tau
14from .logger import log
15from .utils import handle_k
18def cutoff2gridspacing(E):
19 """Convert plane wave energy cut-off to a real-space grid spacing.
21 Reference: Phys. Rev. B 54, 14362.
23 Args:
24 E: Energy in Hartree.
26 Returns:
27 Grid spacing in Bohr.
28 """
29 return math.pi / math.sqrt(2 * E)
32def gridspacing2cutoff(h):
33 """Convert real-space grid spacing to plane wave energy cut-off.
35 Reference: Phys. Rev. B 54, 14362.
37 Args:
38 h: Grid spacing in Bohr.
40 Returns:
41 Cut-off in Hartree.
42 """
43 return 0.5 * (math.pi / h) ** 2
46def center_of_mass(coords, masses=None):
47 """Calculate the center of mass for a set of coordinates and masses.
49 Args:
50 coords: Array of real-space coordinates.
52 Keyword Args:
53 masses: Mass or weight for each coordinate.
55 Returns:
56 Center of mass.
57 """
58 if coords is None:
59 msg = 'The provided coordinates are "None".'
60 raise ValueError(msg)
61 if masses is None:
62 masses = xp.ones(len(coords))
63 return xp.sum(masses * coords.T, axis=1) / xp.sum(masses)
66@handle_k
67def orbital_center(obj, psirs):
68 """Calculate the orbital center of masses, e.g., from localized orbitals.
70 Args:
71 obj: Atoms or SCF object.
72 psirs: Set of orbitals in real-space.
74 Returns:
75 Center of masses.
76 """
77 atoms = obj._atoms
79 coms = [xp.empty((0, 3))] * 2
80 Ncom = psirs.shape[2]
81 for spin in range(atoms.occ.Nspin):
82 coms_spin = xp.empty((Ncom, 3))
84 # Squared orbitals
85 psi2 = xp.real(psirs[spin].conj() * psirs[spin])
86 for i in range(Ncom):
87 coms_spin[i] = center_of_mass(atoms.r, psi2[:, i])
88 coms[spin] = coms_spin
89 return coms
92def inertia_tensor(coords, masses=None):
93 """Calculate the inertia tensor for a set of coordinates and masses.
95 Reference: https://en.wikipedia.org/wiki/Moment_of_inertia
97 Args:
98 coords: Array of real-space coordinates.
100 Keyword Args:
101 masses: Mass or weight for each coordinate.
103 Returns:
104 Inertia tensor.
105 """
106 if masses is None:
107 masses = xp.ones(len(coords))
109 # The inertia tensor for a set of point masses can be calculated with a simple summation
110 # https://en.wikipedia.org/wiki/Moment_of_inertia#Definition_2
111 I = xp.empty((3, 3))
112 I[0, 0] = xp.sum(masses * (coords[:, 1] ** 2 + coords[:, 2] ** 2))
113 I[1, 1] = xp.sum(masses * (coords[:, 0] ** 2 + coords[:, 2] ** 2))
114 I[2, 2] = xp.sum(masses * (coords[:, 0] ** 2 + coords[:, 1] ** 2))
115 I[0, 1] = I[1, 0] = -xp.sum(masses * (coords[:, 0] * coords[:, 1]))
116 I[0, 2] = I[2, 0] = -xp.sum(masses * (coords[:, 0] * coords[:, 2]))
117 I[1, 2] = I[2, 1] = -xp.sum(masses * (coords[:, 1] * coords[:, 2]))
118 return I
121def get_dipole(scf, n=None):
122 """Calculate the electric dipole moment.
124 This function does not account for periodicity, it may be a good idea to center the system.
126 Reference: J. Chem. Phys. 155, 224109.
128 Args:
129 scf: SCF object.
131 Keyword Args:
132 n: Real-space electronic density.
134 Returns:
135 Electric dipole moment in e * Bohr.
136 """
137 atoms = scf.atoms
138 if n is None:
139 if scf.n is None:
140 log.error("There is no density to calculate a dipole moment.")
141 return 0
142 n = scf.n
144 # Diple moment: mu = \sum Z pos - \int n(r) r dr
145 mu = xp.zeros(3)
146 for i in range(atoms.Natoms):
147 mu += atoms.Z[i] * atoms.pos[i]
149 for dim in range(3):
150 mu[dim] -= atoms.dV * xp.sum(n * atoms.r[:, dim])
151 return mu
154def get_ip(scf):
155 """Calculate the ionization potential by calculating the negative HOMO energy.
157 Reference: Physica 1, 104.
159 Args:
160 scf: SCF object.
162 Returns:
163 Ionization potential in Hartree.
164 """
165 scf.kpts._assert_gamma_only()
166 epsilon = get_epsilon(scf, scf.W)[0]
167 # Account for spin-polarized calculations
168 epsilon = xp.sort(xp.ravel(epsilon))
169 return -epsilon[-1]
172@handle_k(mode="skip")
173def check_ortho(obj, func, eps=1e-9):
174 """Check the orthogonality condition for a set of functions.
176 Args:
177 obj: Atoms or SCF object.
178 func: A discretized set of functions.
180 Keyword Args:
181 eps: Tolerance for the condition.
183 Returns:
184 Orthogonality status for the set of functions.
185 """
186 atoms = obj._atoms
187 func = xp.atleast_3d(func)
189 # It makes no sense to calculate anything for only one function
190 if atoms.occ.Nstate == 1:
191 log.warning("Need at least two functions to check their orthogonality.")
192 return True
194 ortho_bool = True
195 # Check the condition for every combination
196 # Orthogonality condition: \int func1^* func2 dr = 0
197 for spin in range(atoms.occ.Nspin):
198 for i in range(atoms.occ.Nstate):
199 for j in range(i + 1, atoms.occ.Nstate):
200 res = atoms.dV * xp.sum(func[spin, :, i].conj() * func[spin, :, j])
201 tmp_bool = abs(res) < eps
202 ortho_bool *= tmp_bool
203 log.debug(f"Function {i} and {j}:\nValue: {res:.7f}\nOrthogonal: {tmp_bool}")
204 log.info(f"Orthogonal: {ortho_bool}")
205 return ortho_bool
208@handle_k(mode="skip")
209def check_norm(obj, func, eps=1e-9):
210 """Check the normalization condition for a set of functions.
212 Args:
213 obj: Atoms or SCF object.
214 func: A discretized set of functions.
216 Keyword Args:
217 eps: Tolerance for the condition.
219 Returns:
220 Normalization status for the set of functions.
221 """
222 atoms = obj._atoms
223 func = xp.atleast_3d(func)
225 norm_bool = True
226 # Check the condition for every function
227 # Normality condition: \int func^* func dr = 1
228 for spin in range(atoms.occ.Nspin):
229 for i in range(atoms.occ.Nstate):
230 res = atoms.dV * xp.sum(func[spin, :, i].conj() * func[spin, :, i])
231 tmp_bool = abs(1 - res) < eps
232 norm_bool *= tmp_bool
233 log.debug(f"Function {i}:\nValue: {res:.7f}\nNormalized: {tmp_bool}")
234 log.info(f"Normalized: {norm_bool}")
235 return norm_bool
238@handle_k(mode="skip")
239def check_orthonorm(obj, func, eps=1e-9):
240 """Check the orthonormality conditions for a set of functions.
242 Args:
243 obj: Atoms or SCF object.
244 func: A discretized set of functions.
246 Keyword Args:
247 eps: Tolerance for the condition.
249 Returns:
250 Orthonormality status for the set of functions.
251 """
252 atoms = obj._atoms
253 ortho_bool = check_ortho(atoms, func, eps)
254 norm_bool = check_norm(atoms, func, eps)
255 log.info(f"Orthonormal: {ortho_bool * norm_bool}")
256 return ortho_bool * norm_bool
259def get_isovalue(n, percent=85):
260 """Find an isovalue that contains a percentage of the electronic density.
262 Reference: J. Chem. Phys. 158, 164102.
264 Args:
265 n: Real-space electronic density.
267 Keyword Args:
268 percent: Amount of density that should be contained.
270 Returns:
271 Isovalue that contains the specified percentage of the density.
272 """
274 def deviation(isovalue):
275 """Wrapper function for finding the isovalue by minimization."""
276 n_mask = xp.sum(n[n > isovalue])
277 return float(xp.abs(percent - (n_mask / n_ref) * 100))
279 # Integrated density
280 n_ref = xp.sum(n)
281 # Finding the isovalue is an optimization problem, minimizing the deviation above
282 # The problem is bound by zero (no density) and the maximum value in n
283 res = minimize_scalar(deviation, bounds=(0, float(xp.max(n))), method="bounded")
284 return res.x
287def get_tautf(scf):
288 """Calculate the Thomas-Fermi kinetic energy densities per spin.
290 Reference: Phys. Lett. B 63, 395.
292 Args:
293 scf: SCF object.
295 Returns:
296 Real-space Thomas-Fermi kinetic energy density.
297 """
298 atoms = scf.atoms
299 # Use the definition with a division by two
300 tautf = 3 / 10 * (atoms.occ.Nspin * 3 * math.pi**2) ** (2 / 3) * scf.n_spin ** (5 / 3)
302 log.debug(f"Calculated Ekin: {scf.energies.Ekin:.6f} Eh")
303 log.debug(f"Integrated tautf: {xp.sum(tautf) * atoms.dV:.6f} Eh")
304 return tautf
307def get_tauw(scf):
308 """Calculate the von Weizsaecker kinetic energy densities per spin.
310 Reference: Z. Phys. 96, 431.
312 Args:
313 scf: SCF object.
315 Returns:
316 Real-space von Weizsaecker kinetic energy density.
317 """
318 atoms = scf.atoms
319 if scf.dn_spin is None:
320 dn_spin = get_grad_field(atoms, scf.n_spin)
321 else:
322 dn_spin = scf.dn_spin
323 dn2 = xp.linalg.norm(dn_spin, axis=2) ** 2
324 # Use the definition with a division by two
325 tauw = dn2 / (8 * scf.n_spin)
327 # For one- and two-electron systems the integrated KED has to be the same as the calculated KE
328 log.debug(f"Calculated Ekin: {scf.energies.Ekin:.6f} Eh")
329 log.debug(f"Integrated tauw: {xp.sum(tauw) * atoms.dV:.6f} Eh")
330 return tauw
333def get_elf(scf):
334 """Calculate the electron localization function.
336 Reference: J. Chem. Phys. 92, 5397.
338 Args:
339 scf: SCF object.
341 Returns:
342 Real-space electron localization function.
343 """
344 D = get_tau(scf.atoms, scf.Y) - get_tauw(scf)
345 D0 = get_tautf(scf)
346 X = D / D0
347 return 1 / (1 + X**2)
350def get_reduced_gradient(scf, eps=0):
351 """Calculate the reduced density gradient s.
353 Reference: Phys. Rev. Lett. 77, 3865.
355 Args:
356 scf: SCF object.
358 Kwargs:
359 eps: Threshold of the density where s should be truncated.
361 Returns:
362 Real-space reduced density gradient.
363 """
364 atoms = scf.atoms
365 if scf.dn_spin is None:
366 dn_spin = get_grad_field(atoms, scf.n_spin)
367 else:
368 dn_spin = scf.dn_spin
369 norm_dn = xp.linalg.norm(xp.sum(dn_spin, axis=0), axis=1)
371 kf = (3 * math.pi**2 * scf.n) ** (1 / 3)
372 with np.errstate(divide="ignore", invalid="ignore"):
373 s = norm_dn / (2 * kf * scf.n)
374 s[scf.n < eps] = 0
375 return s
378def get_spin_squared(scf):
379 """Calculate the expectation value of the squared spin operator <S^2>.
381 Reference: Appl. Phys. Express 12, 115506.
383 Args:
384 scf: SCF object.
386 Returns:
387 The DFT value for <S^2>.
388 """
389 atoms = scf.atoms
390 # <S^2> for a restricted calculation is always zero
391 if not atoms.unrestricted:
392 return 0
394 rhoXr = scf.n_spin[0] - scf.n_spin[1]
395 rhoXr[rhoXr < 0] = 0
396 rhoX = xp.sum(rhoXr) * atoms.dV
397 SX = 0.5 * (xp.sum(scf.n_spin[0]) - xp.sum(scf.n_spin[1])) * atoms.dV
398 return SX * (SX + 1) + rhoX
401def get_multiplicity(scf):
402 """Calculate the multiplicity from <S^2>.
404 Args:
405 scf: SCF object.
407 Returns:
408 Multiplicity 2S+1.
409 """
410 S2 = get_spin_squared(scf)
411 # <S^2> = S(S+1) = S^2+S+0.25-0.25 = (S+0.5)^2-0.25 => S = sqrt(<S^2>+0.25)-0.5
412 S = math.sqrt(S2 + 0.25) - 0.5
413 return 2 * S + 1
416def get_magnetization(scf):
417 """Calculate the total magnetization M.
419 Args:
420 scf: SCF object.
422 Returns:
423 Total magnetization.
424 """
425 # For a spin paired calculation the total magnetization is just zero
426 if not scf.atoms.unrestricted:
427 return 0
429 return xp.sum(scf.n_spin[0] - scf.n_spin[1]) / xp.sum(scf.n)
432def get_bandgap(scf):
433 """Calculate the band gap.
435 Args:
436 scf: SCF object.
438 Returns:
439 Band gap energy.
440 """
441 e_occ = get_epsilon(scf, scf.W, **scf._precomputed)
443 if scf.Z is None:
444 log.warning("The SCF object has no unoccupied energies, can't calculate band gap.")
445 return 0
447 e_unocc = get_epsilon_unocc(scf, scf.W, scf.Z, **scf._precomputed)
448 return xp.min(e_unocc) - xp.max(e_occ)
451def get_Efermi(obj, epsilon=None):
452 """Calculate the Fermi energy.
454 Reference: Phys. Rev. B 107, 195122.
456 Args:
457 obj: SCF or Occupations object.
459 Keyword Args:
460 epsilon: Eigenenergies.
462 Returns:
463 Fermi energy.
464 """
465 # Handle the obj argument
466 if hasattr(obj, "smearing"):
467 if epsilon is None:
468 log.error("When passing an Occupations object the eigenenergies have to be given.")
469 occ = obj
470 else:
471 occ = obj.atoms.occ
473 # Calculate the eigenenergies if necessary
474 if epsilon is None:
475 e_occ = get_epsilon(obj, obj.W, **obj._precomputed)
476 else:
477 e_occ = epsilon
479 def electron_root(Efermi):
480 """Number of electrons by Fermi distribution minus the actual number of electrons."""
481 occ_sum = 0
482 for ik in range(occ.Nk):
483 occ_sum += occ.wk[ik] * xp.sum(fermi_distribution(e_occ[ik], Efermi, occ.smearing))
484 return float(occ_sum * 2 / occ.Nspin - occ.Nelec)
486 # For smeared systems we have to find the root of an objective function
487 if occ.smearing > 0:
488 return root_scalar(electron_root, bracket=(float(xp.min(e_occ)), float(xp.max(e_occ)))).root
490 if obj.Z is None:
491 log.warning("The SCF object has no unoccupied energies, return the maximum energy instead.")
492 return xp.max(e_occ)
494 e_unocc = get_epsilon_unocc(obj, obj.W, obj.Z, **obj._precomputed)
495 return xp.max(e_occ) + (xp.min(e_unocc) - xp.max(e_occ)) / 2
498def fermi_distribution(E, mu, kbT):
499 """Calculate the Fermi distribution.
501 Reference: https://en.wikipedia.org/wiki/Fermi%E2%80%93Dirac_statistics
503 Args:
504 E: State energy.
505 mu: Chemical energy or Fermi energy.
506 kbT: Thermic energy or smearing width.
508 Returns:
509 Fermi distribution.
510 """
511 x = (E - mu) / kbT
512 with np.errstate(over="ignore"):
513 if isinstance(x, numbers.Real):
514 return 1 / (math.exp(x) + 1)
515 return 1 / (xp.exp(x) + 1)
518def electronic_entropy(E, mu, kbT):
519 """Calculate the electronic entropic energy.
521 Reference: J. Phys. Condens. Matter 1, 689.
523 Args:
524 E: State energy.
525 mu: Chemical energy or Fermi energy.
526 kbT: Thermic energy or smearing width.
528 Returns:
529 Electronic entropic energy.
530 """
531 # Condition taken from: https://gitlab.com/QEF/q-e/-/blob/master/Modules/w1gauss.f90
532 if abs((E - mu) / kbT) > 36:
533 return 0
534 f = fermi_distribution(E, mu, kbT)
535 if isinstance(f, numbers.Real):
536 return f * math.log(f) + (1 - f) * math.log(1 - f)
537 return f * xp.log(f) + (1 - f) * xp.log(1 - f)
540def get_dos(epsilon, wk, spin=0, npts=500, width=0.1):
541 """Calculate the total density of states.
543 Reference: https://gitlab.com/gpaw/gpaw/-/blob/master/gpaw/calculator.py
545 Args:
546 epsilon: Eigenenergies.
547 wk: Chemical energy or Fermi energy.
549 Keyword Args:
550 spin: Spin channel.
551 npts: Number of energy discretizations.
552 width: Gaussian width.
554 Returns:
555 Eigenenergies and DOS.
556 """
558 def delta(x, x0, width):
559 """Gaussian of given width centered at x0."""
560 return xp.exp(-(((x - x0) / width) ** 2)) / (math.sqrt(math.pi) * width)
562 energies = epsilon[:, spin].flatten()
563 emin = xp.min(energies) - 5 * width
564 emax = xp.max(energies) + 5 * width
565 e = xp.linspace(emin, emax, npts)
566 dos_e = xp.zeros(npts)
567 for e0, w in zip(energies, wk):
568 dos_e += w * delta(e, e0, width)
569 return e, dos_e