Note
Go to the end to download the full example code.
Internal / external field separation#
Ground magnetometers see both the ionospheric (external) currents and the telluric currents induced inside the Earth (internal). Placing divergence-free SECS on two shells – one in the ionosphere and one below the ground – separates the two contributions, as suggested in the conclusions of Amm and Viljanen (1999). All three magnetic field components are needed for the separation.

import matplotlib.pyplot as plt
import numpy as np
from pysecs import SECS, T_df
R_E = 6378e3
rng = np.random.default_rng(5)
# External shell in the ionosphere, internal image shell below ground
lat_g, lon_g = np.meshgrid(
np.linspace(35, 55, 6), np.linspace(-10, 20, 8), indexing="ij"
)
ext_locs = np.column_stack(
[lat_g.ravel(), lon_g.ravel(), np.full(lat_g.size, R_E + 110e3)]
)
int_locs = np.column_stack(
[lat_g.ravel(), lon_g.ravel(), np.full(lat_g.size, R_E - 500e3)]
)
# Synthetic truth: internal currents mirror a fraction of the external
ext_amps = rng.normal(0, 1e4, ext_locs.shape[0])
int_amps = -0.35 * ext_amps
nobs = 30
obs_locs = np.column_stack(
[rng.uniform(36, 54, nobs), rng.uniform(-9, 19, nobs), np.full(nobs, R_E)]
)
B_obs = np.tensordot(ext_amps, T_df(obs_locs, ext_locs), (0, 2)) + np.tensordot(
int_amps, T_df(obs_locs, int_locs), (0, 2)
)
B_obs += rng.normal(0, 0.02 * np.max(np.abs(B_obs)), B_obs.shape)
# Fit both shells at once by stacking their SEC locations
both = SECS(sec_df_loc=np.vstack([ext_locs, int_locs]))
both.fit(obs_locs, B_obs, epsilon=0.01)
# Split the fitted amplitudes back onto individual shells for prediction
n_ext = len(ext_locs)
ext_fit = SECS(sec_df_loc=ext_locs)
ext_fit.sec_amps = both.sec_amps[:, :n_ext]
int_fit = SECS(sec_df_loc=int_locs)
int_fit.sec_amps = both.sec_amps[:, n_ext:]
# Compare along a latitude profile
profile = np.column_stack(
[np.linspace(38, 52, 100), np.full(100, 5.0), np.full(100, R_E)]
)
B_ext_true = np.tensordot(ext_amps, T_df(profile, ext_locs), (0, 2))
B_int_true = np.tensordot(int_amps, T_df(profile, int_locs), (0, 2))
B_ext_pred = ext_fit.predict_B(profile)
B_int_pred = int_fit.predict_B(profile)
fig, ax = plt.subplots()
lats = profile[:, 0]
ax.plot(lats, B_ext_true[:, 0] * 1e9, "C0", label="external truth")
ax.plot(lats, B_ext_pred[:, 0] * 1e9, "C0--", label="external fit")
ax.plot(lats, B_int_true[:, 0] * 1e9, "C1", label="internal truth")
ax.plot(lats, B_int_pred[:, 0] * 1e9, "C1--", label="internal fit")
ax.set_xlabel("latitude (deg)")
ax.set_ylabel(r"B$_x$ (nT)")
ax.set_title("Two-shell separation of external and internal sources")
ax.legend()
plt.show()
Total running time of the script: (0 minutes 0.121 seconds)