Source code for spinifex.magnetic.models

"""Module for getting the Earth magnetic field"""

from __future__ import annotations

from dataclasses import dataclass
from typing import NamedTuple, Protocol

import astropy.units as u
import numpy as np
from astropy.coordinates import ITRS, AltAz
from ppigrf import igrf

from spinifex.geometry.get_ipp import IPP
from spinifex.times import get_unique_days


[docs] class MagneticProfile(NamedTuple): """Data object to hold Magnetic field profile and uncertainties"""
[docs] magnetic_field: u.Quantity
[docs] magnetic_field_error: u.Quantity
[docs] class MagneticFieldFunction(Protocol): """Magnetic field callable"""
[docs] def __call__(self, ipp: IPP) -> MagneticProfile: ...
@dataclass
[docs] class MagneticModels: """Supported magnetic field models"""
[docs] ppigrf: MagneticFieldFunction
[docs] def get_ppigrf_magnetic_field(ipp: IPP) -> MagneticProfile: """Get the magnetic field at a given EarthLocation""" RMS_E = 87 RMS_N = 73 RMS_U = 114 # constants from https://geomag.bgs.ac.uk/research/modelling/IGRF.html unique_days = get_unique_days(ipp.times) b_par = np.zeros(ipp.loc.shape, dtype=float) relative_uncertainty = np.zeros_like(b_par) for u_day in unique_days: indices = np.floor(ipp.times.mjd) == np.floor(u_day.mjd) loc = ipp.loc[indices] b_e, b_n, b_u = igrf( lon=loc.lon.deg, lat=loc.lat.deg, h=loc.height.to(u.km).value, date=u_day.to_datetime(), ) # ppigrf adds an extra axis for time, we remove it by taking the first element b_magn = np.sqrt(b_e**2 + b_n**2 + b_u**2)[0] # relative uncertainty is 1/2 of the relative uncertainty (rms / b_magn**2) of the # sum of individual uncertainties of the squares (2 * rms_<enu> * b_<enu>) # multiply by b_magn to get absolute value rms = (RMS_E * b_e + RMS_N * b_n + RMS_U * b_u) / (b_magn) relative_uncertainty[indices] = rms / b_magn b_az = np.arctan2(b_e, b_n) b_el = np.arctan2(b_u, np.sqrt(b_n**2 + b_e**2)) b_altaz = AltAz(az=b_az[0] * u.rad, alt=b_el[0] * u.rad, location=loc) b_itrs = b_altaz.transform_to(ITRS()) # project to LOS los = ipp.los[indices][:, np.newaxis] b_par[indices] = ( los[:, :, 0] * b_itrs.x + los[:, :, 1] * b_itrs.y + los[:, :, 2] * b_itrs.z ) b_par[indices] *= b_magn # magnitude along LOS, return MagneticProfile( magnetic_field=u.Quantity(b_par * u.nanotesla), magnetic_field_error=u.Quantity( np.abs(b_par * relative_uncertainty) * u.nanotesla ), )
[docs] magnetic_models = MagneticModels(ppigrf=get_ppigrf_magnetic_field)
[docs] def parse_magnetic_model(magnetic_model_name: str) -> MagneticFieldFunction: """parse magnetic model name Parameters ---------- magnetic_model_name : str name of the magnetic model Returns ------- MagneticFieldFunction magnetic field function Raises ------ TypeError if the magnetic model is not known """ try: return getattr(magnetic_models, magnetic_model_name) # type: ignore[no-any-return] except AttributeError as e: msg = f"Unknown magnetic model {magnetic_model_name}. Supported models are {list(magnetic_models.__annotations__.keys())}" raise TypeError(msg) from e