IGRF calculation#

The International Geomagnetic Reference Field (IGRF) is time-variable spherical harmonic model of the Earth’s internal magnetic field [Alken2021] [IAGA2024]. The model is released every 5 years and allows us to calculate the internal magnetic field from 1900 until 5 years after the latest release (based on predictions of the secular variation). Harmonica allows calculating the 14th generation IGRF field with harmonica.IGRF14. Here’s how it works.

import datetime
import numpy as np
import pygmt
import boule as bl
import harmonica as hm

All of the functionality is wrapped in the IGRF14 class. When creating an instance of it, we need to provide the date on which we want to calculate the field:

igrf = hm.IGRF14("1954-07-29")

The date can be provided as an ISO 8601 formatted date string like above or as a Python datetime.datetime:

igrf = hm.IGRF14(datetime.datetime(1954, 7, 29, hour=1, minute=20))

Note

If the time is omited, the default is midnight. If a timezone is omited, the default is UTC.

Tip

Typically, for a short magnetometry survey (a few days to a week), using a single time for calculating the IGRF is adequate, since IGRF secular variations on these time scales are typically small. However, if the survey is longer, or high precision is required, it may be best to separate the survey by days or flights and calculate the IGRF using a more appropriate datetime for each section.

Calculating at given points#

To calculate the IGRF field at a particular point or set of points, we can use the harmonica.IGRF14.predict method. For example, let’s calculate the field on the date above at the Universidade de São Paulo campus. To do so, we need to provide the geodetic coordinates longitude, latitude, and geometric height (in meters) of the calculation point in that order:

field = igrf.predict((-46.73441817537987, -23.559276852800025, 700))
print(field)
(-5120.730455904012, 22239.393240125162, 8754.879929493665)

The return value field will always be a tuple with the eastward, northward, and upward components of the magnetic field. The vector is in a geodetic coordinate system with the horizontal (eastward and northward) directions parallel to the surface of the ellipsoid and the upward direction parallel to the ellipsoid normal at each given point.

We can print the results in a better way to view them:

print(f"Be={field[0]:.1f} nT | Bn={field[1]:.1f} nT | Bu={field[2]:.1f} nT")
Be=-5120.7 nT | Bn=22239.4 nT | Bu=8754.9 nT

We can convert this tuple of values into intensity (or amplitude), inclination and declination using harmonica.magnetic_vec_to_angles.

intensity, inc, dec = hm.magnetic_vec_to_angles(*field)
print(f"{intensity=:.1f} nT | {inc=:.1f}° | {dec=:.1f}°")
intensity=24443.0 nT | inc=-21.0° | dec=-13.0°

Note

While the vertical component of the magnetic field (\(B_u\)) is positive upward, the inclination is positive downward as per international convention. Hence why the value above is negative.

In addition to calculating the IGRF field at one location, multiple coordinates can be given as numpy arrays or lists:

field = igrf.predict((
    [-46.73441817537987, -157.81633280370718],
    [-23.559276852800025, 21.297542396621708],
    [700, 80],
))
print(field)
(array([-5120.7304559 ,  5569.26432127]), array([22239.39324013, 27863.10882347]), array([  8754.87992949, -22952.79770157]))

The resulting components will be numpy arrays with a shape that matches the shape of the coordinates.

Changing the reference ellipsoid#

The actual calculations (see the notes in harmonica.IGRF14) are performed in geocentric spherical coordinates. This means that the input coordinates must be converted from a geodetic system (which is what most data will come in) to a geocentric system and the output vector must be rotated back to the geodetic system. We use the ellipsoids in boule to handle these conversions. By default, we use the WGS84 ellipsoid but you can pass other Boule ellipsoids (or make your own with boule.Ellipsoid):

igrf_grs80 = hm.IGRF14("1954-07-29", ellipsoid=bl.GRS80)
field = igrf_grs80.predict((-46.73441817537987, -23.559276852800025, 700))
print(field)
(-5120.723844237039, 22239.4027452013, 8754.87004402814)

Notice that the field values are slightly different. So it can be important that the ellipsoid passed to the class is the one used for your coordinates.

Generating a grid#

If we want to make a grid of the IGRF values, we could create grid coordinates and pass them to the predict method. However, there are certain repeated operations that can be avoided when we know we’re calculating on a grid. Plus, it would be good to have the results in a xarray.Dataset that carried all of the associated metadata.

That’s what the harmonica.IGRF14.grid method is for! Calculations with it will be at least 2x faster than using predict and it packages the results in a xarray.Dataset full of metadata:

igrf = hm.IGRF14("2020-02-27")
grid = igrf.grid(region=(0, 360, -90, 90), height=0)
grid
<xarray.Dataset> Size: 54kB
Dimensions:    (latitude: 29, longitude: 57)
Coordinates:
  * longitude  (longitude) float64 456B 0.0 6.429 12.86 ... 347.1 353.6 360.0
  * latitude   (latitude) float64 232B -90.0 -83.57 -77.14 ... 77.14 83.57 90.0
    height     (latitude, longitude) float64 13kB 0.0 0.0 0.0 ... 0.0 0.0 0.0
Data variables:
    b_east     (latitude, longitude) float64 13kB 0.0 0.0 0.0 ... 0.0 0.0 0.0
    b_north    (latitude, longitude) float64 13kB 1.443e+04 ... 1.814e+03
    b_up       (latitude, longitude) float64 13kB 5.202e+04 ... -5.673e+04
Attributes:
    Conventions:  CF-1.8
    title:        IGRF14 magnetic field on 2020-02-27T00:00:00
    crs:          WGS84
    source:       Generated by spherical harmonic synthesis using library Har...
    description:  Three components of the magnetic field vector calculated on...
    units:        nT
    date:         2020-02-27T00:00:00
    references:   https://doi.org/10.5281/zenodo.14218973

We can plot the three components using pygmt on a nice map:

fig = pygmt.Figure()
for c in ["b_east", "b_north", "b_up"]:
    fig.grdimage(
        grid[c], cmap="polar+h", projection="W15c", region="g",
    )
    fig.coast(shorelines=True)
    fig.colorbar(
        position="JMR+ml+o0.5c",
        frame=[
            "a10000",
            f"x+l{grid[c].attrs['long_name']}",
            f"y+l{grid[c].attrs['units']}",
        ]
    )
    if c == "b_east":
        fig.basemap(frame=["a", f"+t{grid.attrs['title']}"])
    else:
        fig.basemap(frame="a")
    fig.shift_origin(yshift="-h-0.5c")
fig.show()
../_images/igrf_9_0.png

The grid spacing was calculated automatically to match the maximum degree of the spherical harmonic expansion (default is 13). It can also be adjusted by passing the spacing or shape arguments. For example, let’s set the spacing to 1 degree:

grid = igrf.grid(region=(0, 360, -90, 90), height=0, spacing=1)
grid
<xarray.Dataset> Size: 2MB
Dimensions:    (latitude: 181, longitude: 361)
Coordinates:
  * longitude  (longitude) float64 3kB 0.0 1.0 2.0 3.0 ... 358.0 359.0 360.0
  * latitude   (latitude) float64 1kB -90.0 -89.0 -88.0 -87.0 ... 88.0 89.0 90.0
    height     (latitude, longitude) float64 523kB 0.0 0.0 0.0 ... 0.0 0.0 0.0
Data variables:
    b_east     (latitude, longitude) float64 523kB 0.0 0.0 0.0 ... 0.0 0.0 0.0
    b_north    (latitude, longitude) float64 523kB 1.443e+04 ... 1.814e+03
    b_up       (latitude, longitude) float64 523kB 5.202e+04 ... -5.673e+04
Attributes:
    Conventions:  CF-1.8
    title:        IGRF14 magnetic field on 2020-02-27T00:00:00
    crs:          WGS84
    source:       Generated by spherical harmonic synthesis using library Har...
    description:  Three components of the magnetic field vector calculated on...
    units:        nT
    date:         2020-02-27T00:00:00
    references:   https://doi.org/10.5281/zenodo.14218973

We can also calculate the intensity (or amplitude) and the inclination and declination angles from the vector using harmonica.magnetic_vec_to_angles. We’ll add the resulting grids to our xarray.Dataset and add a little bit of metadata:

result = hm.magnetic_vec_to_angles(grid.b_east, grid.b_north, grid.b_up)
grid["intensity"], grid["inclination"], grid["declination"] = result
grid.intensity.attrs["long_name"] = "Intensity"
grid.intensity.attrs["units"] = "nT"
grid.inclination.attrs["long_name"] = "Inclination"
grid.inclination.attrs["units"] = "degrees"
grid.declination.attrs["long_name"] = "Declination"
grid.declination.attrs["units"] = "degrees"
grid
<xarray.Dataset> Size: 4MB
Dimensions:      (latitude: 181, longitude: 361)
Coordinates:
  * longitude    (longitude) float64 3kB 0.0 1.0 2.0 3.0 ... 358.0 359.0 360.0
  * latitude     (latitude) float64 1kB -90.0 -89.0 -88.0 ... 88.0 89.0 90.0
    height       (latitude, longitude) float64 523kB 0.0 0.0 0.0 ... 0.0 0.0 0.0
Data variables:
    b_east       (latitude, longitude) float64 523kB 0.0 0.0 0.0 ... 0.0 0.0 0.0
    b_north      (latitude, longitude) float64 523kB 1.443e+04 ... 1.814e+03
    b_up         (latitude, longitude) float64 523kB 5.202e+04 ... -5.673e+04
    intensity    (latitude, longitude) float64 523kB 5.398e+04 ... 5.676e+04
    inclination  (latitude, longitude) float64 523kB -74.5 -74.65 ... 88.17
    declination  (latitude, longitude) float64 523kB 0.0 0.0 0.0 ... 0.0 0.0 0.0
Attributes:
    Conventions:  CF-1.8
    title:        IGRF14 magnetic field on 2020-02-27T00:00:00
    crs:          WGS84
    source:       Generated by spherical harmonic synthesis using library Har...
    description:  Three components of the magnetic field vector calculated on...
    units:        nT
    date:         2020-02-27T00:00:00
    references:   https://doi.org/10.5281/zenodo.14218973

The angles will be in decimal degrees and the intensity in nT. We can plot them with PyGMT the same way we did the vector components:

fig = pygmt.Figure()
cmaps = {
    "intensity": "viridis",
    "inclination": "polar+h",
    "declination": "polar+h",
}
cb_annot = {
    "intensity": "a10000",
    "inclination": "a20+u\\260",
    "declination": "a40+u\\260",
}
for c in ["intensity", "inclination", "declination"]:
    fig.grdimage(
        grid[c], cmap=cmaps[c], projection="W0/15c",
    )
    fig.coast(shorelines=True)
    fig.colorbar(
        position="JMR+ml+o0.5c",
        frame=[
            cb_annot[c],
            f"x+l{grid[c].attrs['long_name']}",
            f"y+l{grid[c].attrs['units']}",
        ]
    )
    if c == "intensity":
        fig.basemap(frame=["a", f"+t{grid.attrs['title']}"])
    else:
        fig.basemap(frame="a")
    fig.shift_origin(yshift="-h-0.5c")
fig.show()
../_images/igrf_12_0.png

We can clearly see the South Atlantic Magnetic Anomaly in the intensity map!