Source code for sunkit_magex.pfss.pfss

"""
Code for calculating a PFSS extrapolation.
"""
import numpy as np

import sunkit_magex.pfss

HAS_NUMBA = False
try:
    import numba
    HAS_NUMBA = True
except Exception:
    pass


def _eigh(A):
    return np.linalg.eigh(A)


def _compute_r_term(m, k, ns, Q, brt, lam, ffm, nr, ffp, psi, psir):
    for l in range(ns):
        # Ignore the l=0 and m=0 term; for a globally divergence free field
        # this term is zero anyway, but numerically it may be small which
        # causes numerical issues when solving for c, d
        if l == 0 and m == 0:
            continue
        # - sum (c_{lm} + d_{lm}) * lam_{l}
        # lam[l] is small so this blows up
        cdlm = np.dot(Q[:, l], np.asfortranarray(brt[:, m])) / lam[l]
        # - ratio c_{lm}/d_{lm} [numerically safer this way up]
        ratio = (ffm[l]**(nr - 1) - ffm[l]**nr) / (ffp[l]**nr - ffp[l]**(nr - 1))
        dlm = cdlm / (1.0 + ratio)
        clm = ratio * dlm
        psir[:, l] = clm * ffp[l]**k + dlm * ffm[l]**k

    # - compute entry for this m in psit = Sum_l c_{lm}Q_{lm}**j
    psi[:, :, m] = np.dot(psir, Q.T)
    return psi, psir


def _als_alp(nr, nphi, Fs, psi, Fp, als, alp):
    for j in range(nr + 1):
        for i in range(nphi + 1):
            als[i, :, j] = Fs * (psi[j, :, ((i - 1) % nphi)] - psi[j, :, ((i) % nphi)])
        for i in range(nphi):
            alp[i, 1:-1, j] = Fp[1:-1] * (psi[j, 1:, i] - psi[j, :-1, i])
    return als, alp


def _A_diag(A, ns, Vg, Uc, mu, m):
    for j in range(ns):
        A[j, j] = Vg[j] + Vg[j + 1] + Uc[j] * mu[m]
    return A


if HAS_NUMBA:
    _eigh = numba.jit(nopython=True)(_eigh)
    _compute_r_term = numba.jit(nopython=True)(_compute_r_term)
    _als_alp = numba.jit(nopython=True)(_als_alp)
    _A_diag = numba.jit(nopython=True)(_A_diag)


[docs] def pfss(input): r""" Compute PFSS model. Extrapolates a 3D PFSS using an eigenfunction method in :math:`r,s,p` coordinates, on the dumfric grid (equally spaced in :math:`\rho = \ln(r/r_{sun})`, :math:`s= \cos(\theta)`, and :math:`p=\phi`). Parameters ---------- input : ~sunkit_magex.pfss.Input Input parameters. Returns ------- ~sunkit_magex.pfss.Output Notes ----- In order to avoid numerical issues, the monopole term (which should be zero for a physical magnetic field anyway) is explicitly excluded from the solution. The output should have zero current to machine precision, when computed with the DuMFriC staggered discretization. """ br0 = input.br nr = input.grid.nr ns = input.grid.ns nphi = input.grid.nphi # Coordinates: ds = input.grid.ds dp = input.grid.dp dr = input.grid.dr input.grid.rg input.grid.rc sg = input.grid.sg sc = input.grid.sc k = np.linspace(0, nr, nr + 1) Fp = sg * 0 # Lp/Ls on p-ribs Fp[1:-1] = np.sqrt(1 - sg[1:-1]**2) / (np.arcsin(sc[1:]) - np.arcsin(sc[:-1])) * dp Vg = Fp / ds / dp Fs = (np.arcsin(sg[1:]) - np.arcsin(sg[:-1])) / np.sqrt(1 - sc**2) / dp # Ls/Lp on s-ribs Uc = Fs / ds / dp # FFT in phi of photospheric distribution at each latitude: brt = np.fft.rfft(br0, axis=1) brt = brt.astype(np.complex128) # Prepare tridiagonal matrix: # - create off-diagonal part of the matrix: A = np.zeros((ns, ns)) for j in range(ns - 1): A[j, j + 1] = -Vg[j + 1] A[j + 1, j] = A[j, j + 1] # - term required for m-dependent part of matrix: mu = np.fft.fftfreq(nphi) mu = 4 * np.sin(np.pi * mu)**2 # - initialise: psir = np.zeros((nr + 1, ns), dtype='complex') psi = np.zeros((nr + 1, ns, nphi), dtype='complex') e1 = np.exp(dr) fact = np.sinh(dr) * (e1 - 1) # Loop over azimuthal modes (positive m): for m in range(nphi // 2 + 1): # - set diagonal terms of matrix: A = _A_diag(A, ns, Vg, Uc, mu, m) # - compute eigenvectors Q_{lm} and eigenvalues lam_{lm}: # (note that A is symmetric so use special solver) lam, Q = _eigh(A) Q = Q.astype(np.complex128) # - solve quadratic: Flm = 0.5 * (1 + e1 + lam * fact) ffp = Flm + np.sqrt(Flm**2 - e1) ffm = e1 / ffp # - compute radial term for each l (for this m): psi, psir = _compute_r_term(m, k, ns, Q, brt, lam, ffm, nr, ffp, psi, psir) if (m > 0): psi[:, :, nphi - m] = np.conj(psi[:, :, m]) # Past this point only psi, Fs, Fp are needed # Compute psi by inverse fft: psi = np.real(np.fft.ifft(psi, axis=2)) # Hence compute vector potential [note index order, for netcdf]: # (note that alr is zero by definition) alr = np.zeros((nphi + 1, ns + 1, nr)) als = np.zeros((nphi + 1, ns, nr + 1)) alp = np.zeros((nphi, ns + 1, nr + 1)) als, alp = _als_alp(nr, nphi, Fs, psi, Fp, als, alp) return sunkit_magex.pfss.Output(alr, als, alp, input.grid, input.map)