Source code for gpaw.hyperfine

"""Hyperfine parameters.

See:

    First-principles calculations of defects in oxygen-deficient
    silica exposed to hydrogen

    Peter E. Blöchl

    Phys. Rev. B 62, 6158 – Published 1 September 2000

    https://doi.org/10.1103/PhysRevB.62.6158

"""
import argparse
from math import pi
from typing import List

import ase.units as units
import numpy as np
from scipy.integrate import simpson

from gpaw import get_scipy_version
from gpaw.atom.aeatom import Channel
from gpaw.atom.configurations import configurations
from gpaw.atom.radialgd import RadialGridDescriptor
from gpaw.calculator import GPAW
from gpaw.gaunt import gaunt
from gpaw.grid_descriptor import GridDescriptor
from gpaw.pw.descriptor import PWDescriptor
from gpaw.setup import Setup
from gpaw.typing import Array1D, Array2D, Array3D
from gpaw.utilities import unpack_density
from gpaw.xc.functional import XCFunctional

# Fine-structure constant: (~1/137)
alpha = 0.5 * units._mu0 * units._c * units._e**2 / units._hplanck

G_FACTOR_E = 2.00231930436256


[docs]def hyperfine_parameters(calc: GPAW, exclude_core=False) -> Array3D: r"""Calculate isotropic and anisotropic hyperfine coupling parameters. One tensor (:math:`A_{ij}`) per atom is returned in eV units. In Hartree atomic units, we have the isotropic part :math:`a = \text{Tr}(\mathbf{A}) / 3`: .. math:: a = \frac{2 \alpha^2 g_e m_e}{3 m_p} \int \delta_T(\mathbf{r}) \rho_s(\mathbf{r}) d\mathbf{r}, and the anisotropic part :math:`\mathbf{A} - a`: .. math:: \frac{\alpha^2 g_e m_e}{4 \pi m_p} \int \frac{3 r_i r_j - \delta_{ij} r^2}{r^5} \rho_s(\mathbf{r}) d\mathbf{r}. Remember to multiply each tensor by the g-factors of the nuclei. Use ``exclude_core=True`` to exclude contribution from "frozen" core. """ dens = calc.density nt_sR = dens.nt_sG A_avv = smooth_part( nt_sR[0] - nt_sR[1], dens.gd, calc.atoms.get_scaled_positions()) D_asp = calc.density.D_asp for a, D_sp in D_asp.items(): density_sii = unpack_density(D_sp) setup = calc.wfs.setups[a] A_vv = paw_correction(density_sii, setup, calc.hamiltonian.xc, exclude_core) A_avv[a] += A_vv A_avv *= pi * alpha**2 * G_FACTOR_E * units._me / units._mp * units.Ha return A_avv
def smooth_part(spin_density_R: Array3D, gd: GridDescriptor, spos_ac: Array2D, ecut: float = None) -> Array3D: """Contribution from pseudo spin-density.""" pd = PWDescriptor(ecut, gd) spin_density_G = pd.fft(spin_density_R) G_Gv = pd.get_reciprocal_vectors(add_q=False) # eiGR_aG = np.exp(-1j * spos_ac.dot(gd.cell_cv).dot(G_Gv.T)) eiGR_aG = np.exp(-1j * spos_ac @ gd.cell_cv @ G_Gv.T) # Isotropic term: W1_a = pd.integrate(spin_density_G, eiGR_aG) / gd.dv * (2 / 3) spin_density_G[0] = 0.0 G2_G = pd.G2_qG[0].copy() G2_G[0] = 1.0 spin_density_G /= G2_G # Anisotropic term: W_vva = np.empty((3, 3, len(spos_ac))) for v1 in range(3): for v2 in range(3): W_a = pd.integrate(G_Gv[:, v1] * G_Gv[:, v2] * spin_density_G, eiGR_aG) W_vva[v1, v2] = -W_a / gd.dv W_a = np.trace(W_vva) / 3 for v in range(3): W_vva[v, v] -= W_a W_vva[v, v] += W1_a return W_vva.transpose((2, 0, 1)) # Normalization constants for xy, yz, 3z^2-r^2, xz, x^2-y^2: Y2_m = (np.array([15 / 4, 15 / 4, 5 / 16, 15 / 4, 15 / 16]) / pi)**0.5 # Second derivatives: Y2_mvv = np.array([[[0, 1, 0], [1, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 1], [0, 1, 0]], [[-2, 0, 0], [0, -2, 0], [0, 0, 4]], [[0, 0, 1], [0, 0, 0], [1, 0, 0]], [[2, 0, 0], [0, -2, 0], [0, 0, 0]]]) def paw_correction(density_sii: Array3D, setup: Setup, xc: XCFunctional = None, exclude_core: bool = False) -> Array2D: """Corrections from 1-center expansions of spin-density.""" # Spherical part: spin_density_ii = density_sii[0] - density_sii[1] D0_jj = expand(spin_density_ii, setup.l_j, l=0)[0] phit_jg = np.array(setup.data.phit_jg) phi_jg = np.array(setup.data.phi_jg) rgd = setup.rgd # Spin-density from pseudo density: nt0 = phit_jg[:, 0].dot(D0_jj).dot(phit_jg[:, 0]) / (4 * pi)**0.5 # All-electron contribution diverges as r^-beta and must be integrated # over a small region of size rT: n0_g = np.einsum('ab, ag, bg -> g', D0_jj, phi_jg, phi_jg) / (4 * pi)**0.5 if not exclude_core and setup.Nc > 0 and xc is not None: n0_g += core_contribution(density_sii, setup, xc) beta = 2 * (1 - (1 - (setup.Z * alpha)**2)**0.5) rT = setup.Z * alpha**2 n0 = integrate(n0_g, rgd, rT, beta) W1 = (n0 - nt0) * 2 / 3 # isotropic result # Now the anisotropic part from the l=2 part of the density: D2_mjj = expand(spin_density_ii, setup.l_j, 2) dn2_mg = np.einsum('mab, ag, bg -> mg', D2_mjj, phi_jg, phi_jg) dn2_mg -= np.einsum('mab, ag, bg -> mg', D2_mjj, phit_jg, phit_jg) A_m = dn2_mg[:, 1:].dot(rgd.dr_g[1:] / rgd.r_g[1:]) / 5 A_m *= Y2_m # W_vv: Array2D = Y2_mvv.T.dot(A_m) # type: ignore W_vv = Y2_mvv.T @ A_m W = np.trace(W_vv) / 3 for v in range(3): W_vv[v, v] -= W W_vv[v, v] += W1 return W_vv def expand(D_ii: Array2D, l_j: List[int], l: int) -> Array3D: """Get expansion coefficients.""" G_LLm = gaunt(lmax=2)[:, :, l**2:(l + 1)**2] D_mjj = np.empty((2 * l + 1, len(l_j), len(l_j))) i1a = 0 for j1, l1 in enumerate(l_j): i1b = i1a + 2 * l1 + 1 i2a = 0 for j2, l2 in enumerate(l_j): i2b = i2a + 2 * l2 + 1 D_mjj[:, j1, j2] = np.einsum('ab, abm -> m', D_ii[i1a:i1b, i2a:i2b], G_LLm[l1**2:(l1 + 1)**2, l2**2:(l2 + 1)**2]) i2a = i2b i1a = i1b return D_mjj def delta(r: Array1D, rT: float) -> Array1D: """Extended delta function.""" return 2 / rT / (1 + 2 * r / rT)**2 def integrate(n0_g: Array1D, rgd: RadialGridDescriptor, rT: float, beta: float) -> float: """Take care of r^-beta divergence.""" r_g = rgd.r_g a_i = np.polyfit(r_g[1:5], n0_g[1:5] * r_g[1:5]**beta, 3) r4 = r_g[4] dr = rT / 50 n = max(int(r4 / dr / 2) * 2 + 1, 3) r_j = np.linspace(0, r4, n) b_i = np.arange(3, -1, -1) + 1 - beta d0 = 2 / rT # delta(0, rT) d1 = -8 / rT**2 n0 = a_i.dot(d0 * r4**b_i / b_i + d1 * r4**(b_i + 1) / (b_i + 1)) d_j = delta(r_j, rT) - (d0 + d1 * r_j) head_j = d_j * np.polyval(a_i, r_j) head_j[1:] *= r_j[1:]**-beta n0 += simpson(head_j, x=r_j) tail_g = n0_g[4:] * delta(r_g[4:], rT) if get_scipy_version() >= [1, 11]: n0 += simpson(tail_g, x=r_g[4:]) else: n0 += simpson(tail_g, x=r_g[4:], even='first') return n0 def core_contribution(density_sii: Array3D, setup: Setup, xc: XCFunctional) -> Array1D: """Calculate spin-density from "frozen" core.""" # Spherical part: D_sjj = [expand(density_ii, setup.l_j, 0)[0] for density_ii in density_sii] phi_jg = np.array(setup.data.phi_jg) rgd = setup.rgd # Densities with frozen core: n_sg = np.einsum('ag, sab, bg -> sg', phi_jg, D_sjj, phi_jg) / (4 * pi)**0.5 n_sg += setup.data.nc_g * (0.5 / (4 * pi)**0.5) # Potential: v_sg = np.zeros_like(n_sg) xc.calculate_spherical(rgd, n_sg, v_sg) vr_sg = v_sg * rgd.r_g vr_sg -= setup.Z vr_sg += rgd.poisson(n_sg.sum(axis=0)) # Find first bound s-state included in PAW potential: for n, l in zip(setup.n_j, setup.l_j): if l == 0: assert n > 0 n0 = n break else: assert False, (setup.n_j, setup.l_j) # Initial guesses for core s-states: eigs = [e for n, l, f, e in configurations[setup.symbol][1] if n < n0 and l == 0] # Solve spherical scalar-relativistic Schrödinger equation: core_spin_density_g = rgd.zeros() sign = 1.0 for vr_g in vr_sg: channel = Channel(l=0, f_n=[1] * len(eigs)) channel.e_n = eigs channel.phi_ng = rgd.empty(len(eigs)) channel.solve2(vr_g, rgd=rgd, scalar_relativistic=True, Z=setup.Z) assert channel.solve2ok core_spin_density_g += sign * channel.calculate_density() sign = -1.0 return core_spin_density_g # From https://en.wikipedia.org/wiki/Gyromagnetic_ratio # Units: MHz/T gyromagnetic_ratios = {'H': (1, 42.577478518), 'He': (3, -32.434), 'Li': (7, 16.546), 'C': (13, 10.7084), 'N': (14, 3.077), 'O': (17, -5.772), 'F': (19, 40.052), 'Na': (23, 11.262), 'Al': (27, 11.103), 'Si': (29, -8.465), 'P': (31, 17.235), 'Fe': (57, 1.382), 'Cu': (63, 11.319), 'Zn': (67, 2.669), 'Xe': (129, -11.777)} def main(argv: List[str] = None) -> None: """Command-line interface.""" parser = argparse.ArgumentParser( prog='python3 -m gpaw.hyperfine', description='Calculate hyperfine parameters.') add = parser.add_argument add('file', metavar='input-file', help='GPW-file (with or without wave functions).') add('-g', '--g-factors', help='G-factors. Example: "-g H:5.6,O:-0.76".') add('-u', '--units', default='ueV', choices=['ueV', 'MHz'], help='Units. Must be "ueV" (micro-eV, default) or "MHz".') add('-x', '--exclude-core', action='store_true') add('-d', '--diagonalize', action='store_true', help='Show eigenvalues of tensor.') args = parser.parse_intermixed_args(argv) calc = GPAW(args.file) atoms = calc.get_atoms() symbols = atoms.symbols magmoms = atoms.get_magnetic_moments() total_magmom = atoms.get_magnetic_moment() g_factors = {symbol: ratio * 1e6 * 4 * pi * units._mp / units._e for symbol, (n, ratio) in gyromagnetic_ratios.items()} if args.g_factors: for symbol, value in (part.split(':') for part in args.g_factors.split(',')): g_factors[symbol] = float(value) if args.units == 'ueV': scale = 1e6 unit = 'μeV' else: scale = units._e / units._hplanck * 1e-6 unit = 'MHz' A_avv = hyperfine_parameters(calc, args.exclude_core) print('Hyperfine coupling paramters ' f'in {unit}:\n') if args.diagonalize: columns = ['1.', '2.', '3.'] else: columns = ['xx', 'yy', 'zz', 'yz', 'xz', 'xy'] print(' atom magmom ', ' '.join(columns)) used = {} for a, A_vv in enumerate(A_avv): symbol = symbols[a] magmom = magmoms[a] g_factor = g_factors.get(symbol, 1.0) used[symbol] = g_factor A_vv *= g_factor * scale if args.diagonalize: numbers = np.linalg.eigvalsh(A_vv).tolist() else: numbers = [A_vv[0, 0], A_vv[1, 1], A_vv[2, 2], A_vv[1, 2], A_vv[0, 2], A_vv[0, 1]] print(f'{a:3} {symbol:>2} {magmom:6.3f}', ''.join(f'{x:9.2f}' for x in numbers)) print('\nCore correction', 'NOT included!' if args.exclude_core else 'included') print(f'Total magnetic moment: {total_magmom:.3f}') print('\nG-factors used:') for symbol, g in used.items(): print(f'{symbol:2} {g:10.3f}') if __name__ == '__main__': main()