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.

Two-shell separation of external and internal sources
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)

Gallery generated by Sphinx-Gallery