import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from matplotlib.animation import FuncAnimationThis post is based on the homework report from the “Solar-Terrestrial Physics” lecture in 2022 by Professor Dong-Hun Lee at Kyung Hee University.
Theory
Dipole Magnetic field
\mathbf{B} = -\frac{\mu_0}{4\pi}\left( \frac{\mathbf{m}}{r^3} - \frac{3(\mathbf{m}\cdot\mathbf{r})\mathbf{r}}{r^5} \right)
In (r, \lambda, \phi) coordinates, we describe the magnetic field as follows:
B_r = Z = -\frac{\mu_0 m}{2\pi}\frac{\sin\lambda}{r^3}
B_\lambda = H = \frac{\mu_0 m}{4\pi}\frac{\cos\lambda}{r^3}
B_\phi = 0
Therefore, the magnetic field \mathbf{B} does not depend on longitude \phi.
\mathbf{B} = \mathbf{B}(r, \lambda)
The strength B is given by:
B = \frac{\mu_0 m}{4\pi r^3}(1+3\sin^2\lambda)^{\textstyle \frac{1}{2}}
For the Earth’s magnetic field at the equator, denoted as B_E, it can be expressed as:
B_E = \frac{\mu_0 m}{4\pi R_E^3}
The actual value for B_E is approximately 0.31 Gauss (G).
The components of the magnetic field can be re-express in terms of B_E as follows:
B_r = -\frac{2B_E}{(r/R_E)^3}\sin\lambda
B_\lambda = \frac{B_E}{(r/R_E)^3}\cos\lambda
B_\phi = 0
The magnetic field line in the meridian (when \phi=\text{const} or in the (r,\lambda)-plane) is given by:
r = r_\text{eq} \cos^2\lambda
Here, r_\text{eq} = LR_E and L is called L-parameter.
L-parameter describes the set of magnetic field lines which cross the Earth’s magnetic equator at a number of Earth-radii equal to the L-parameter. For example, L=2 describes the set of the Earth’s magnetic field lines which cross the Earth’s magnetic equator two earth radii from the center of the Earth.1
The dipole model of the Earth’s magnetic field is a first order approximation of the rather complex true Earth’s magnetic field. Due to effects of the interplanetary magnetic field (IMF), and the solar wind, the dipole model is particularly inaccurate at high L-shells (e.g., above L=3), but may be a good approximation for lower L-shells. For more precise work, or for any work at higher L-shells, a more accurate model that incorporates solar effects, such as the Tsyganenko magnetic field model, is recommended.2
\begin{align*} \mathbf{B}(\mathbf{r}) & = B_r(r, \lambda) \hat{r} + B_\lambda (r, \lambda) \hat{\lambda} \\ & = B_x(x, y)\hat{x} + B_y(x, y)\hat{y} \end{align*}
Newton’s equation of motion for a charged particle in magnetic field
\frac{d^2\mathbf{r}}{dt^2} = \frac{q}{m}\mathbf{v}\times\mathbf{B}
\frac{d^2x}{dt^2}\hat{x} + \frac{d^2y}{dt^2}\hat{y} + \frac{d^2z}{dt^2}\hat{z} = \frac{q}{m}(\hat{x}(v_yB_z - v_zB_y) + \hat{y}(v_zB_x - v_xB_z) + \hat{z}(v_xB_y - v_yB_x))
\frac{d^2x}{dt^2} = \frac{q}{m}(v_yB_z - v_zB_y)
\frac{d^2y}{dt^2} = \frac{q}{m}(v_zB_x - v_xB_z)
\frac{d^2z}{dt^2} = \frac{q}{m}(v_xB_y - v_yB_x)
The coupled 1st ODEs
\frac{dx}{dt} = v_x
\frac{dy}{dt} = v_y
\frac{dz}{dt} = v_z
\frac{dv_x}{dt} = \frac{q}{m}(v_yB_z - v_zB_y)
\frac{dv_y}{dt} = \frac{q}{m}(v_zB_x - v_xB_z)
\frac{dv_z}{dt} = \frac{q}{m}(v_xB_y - v_yB_x)
The state vector for odeint
S = (x, y, z, v_x, v_y, v_z)
\frac{dS}{dt} = (\frac{dx}{dt}, \frac{dy}{dt}, \frac{dz}{dt}, \frac{dv_x}{dt}, \frac{dv_y}{dt},\frac{dv_z}{dt})
Code
# Be = 0.31 G = 0.31 * 10^-4 T
Be = 0.31 * 1e-4
# Re = 6371 km = 6371 * 10^3 m
Re = 6371 * 1e3
C = Be * (Re**3)
def B(x, y, z):
"""dipole field at (x, y, z)"""
r = np.sqrt(x**2 + y**2 + z**2)
Bx = -1 * C * (3 * x * z) / (r**5)
By = -1 * C * (3 * y * z) / (r**5)
Bz = C * (r**2 - 3 * z**2) / (r**5)
return Bx, By, Bz
def field_line_3D(phi, L=6.6):
"""dipole field line (3D)"""
phi = np.deg2rad(phi)
theta = np.linspace(0, 2 * np.pi, 1000)
rf = L * np.sin(theta) ** 2
xf = rf * np.sin(theta) * np.cos(phi)
yf = rf * np.sin(theta) * np.sin(phi)
zf = rf * np.cos(theta)
return xf, yf, zf
def field_line_2D(L=6.6):
"""dipole field line (2D)"""
lamb = np.linspace(0, 2 * np.pi, 1000)
rf2 = L * np.cos(lamb) ** 2
xf2 = rf2 * np.cos(lamb)
zf2 = rf2 * np.sin(lamb)
return xf2, zf2
def dSdt(S, t, q_over_m):
"""dS/dt for odeint"""
x, y, z, vx, vy, vz = S
Bx, By, Bz = B(x, y, z)
dvxdt = q_over_m * (vy * Bz - vz * By)
dvydt = q_over_m * (vz * Bx - vx * Bz)
dvzdt = q_over_m * (vx * By - vy * Bx)
return [vx, vy, vz, dvxdt, dvydt, dvzdt]fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot()
xmin, xmax = -7, 7
ymin, ymax = -7, 7
ax.add_patch(
plt.Circle(
(0, 0), 1, zorder=99, facecolor="white", edgecolor="black", label="Earth"
)
)
ax.set_aspect("equal")
ax.legend()
ax.axhline(y=0, color="black", linewidth=0.5)
ax.xaxis.grid(True, which="both")
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)
majors = np.arange(xmin + 1, xmax, 1)
ax.xaxis.set_major_locator(ticker.FixedLocator(majors))
ax.set_xlabel("x ($R_E$)")
ax.set_ylabel("z ($R_E$)")
Lvalues = [2, 4, 6, 8, 10, 20, 100]
colors = ["r", "darkorange", "gold", "green", "blue", "magenta", "purple"]
lamb = np.linspace(0, 2 * np.pi, 1000)
for i, L in enumerate(Lvalues):
x, z = field_line_2D(L)
ax.plot(x, z, label=f"L={L}", color=colors[i])
ax.legend()
ax.set_title("Magnetic Dipole Field line (2D)")
plt.show()
species = "Proton"
e = 1.602e-19
q = e # C
mH = 1.67e-27
m = mH # kg
q_over_m = q / m
E_keV = 2000 # keV# L-parameter
L = 6.6
# start at equator
x0, y0, z0 = L * Re, 0, 0
keV_to_J = 1e3 * e # J
# particle energy (keV) and pitch angle
E = E_keV * keV_to_J # J
pitch_angle_deg = 30
alpha = np.deg2rad(pitch_angle_deg)
# particle velocity
v0 = np.sqrt(2 * E / m)
# vx = v_perp
# vy = 0
# vz = v_para
vx0 = v0 * np.sin(alpha)
vy0 = 0
vz0 = v0 * np.cos(alpha)
# bounce time_scale
t_B = 290 * (np.pi * L / 10) * np.sqrt(m / (mH * E_keV))
print(f"bounce time scale ~ {t_B} s")
S0 = [x0, y0, z0, vx0, vy0, vz0]
# number of bounce
n = 3
tmin = 0
tmax = n * t_B
t = np.linspace(tmin, tmax, 1000)
# solve ODE
sol = odeint(dSdt, S0, t, args=(q_over_m,))
x, y, z, vx, vy, vz = sol.T
x, y, z = x / Re, y / Re, z / Re
print(f"t_max ~ {tmax:.4f} s")bounce time scale ~ 13.44549539521195 s
t_max ~ 40.3365 s
trajectory_linewidth = 0.8
fieldline_linewidth = 0.5# xyzrange = 10
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(projection="3d")
ax.set_aspect("equal")
# ax.plot([-xyzrange,xyzrange], [0,0], [0, 0], color='black')
# ax.plot([0,0], [-xyzrange,xyzrange], [0, 0], color='black')
# ax.plot([0,0], [0,0], [-xyzrange, xyzrange], color='black')
ax.plot(
x,
y,
z,
color="red",
label="Particle Trajectory",
linewidth=trajectory_linewidth,
zorder=200,
)
ax.set_xlabel("x ($R_E$)")
ax.set_ylabel("y ($R_E$)")
ax.set_zlabel("z ($R_E$)")
ax.set_xlim(-L, L)
ax.set_ylim(-L, L)
ax.set_zlim(-L, L)
for az in np.arange(0, 361, 20):
xf, yf, zf = field_line_3D(az, L)
if az == 360:
ax.plot(
xf,
yf,
zf,
color="blue",
linewidth=fieldline_linewidth,
zorder=-1,
label=f"Magnetic Field (L={L})",
)
else:
ax.plot(xf, yf, zf, color="blue", linewidth=fieldline_linewidth, zorder=-1)
# Sphere with radius Re
u = np.linspace(0, 2 * np.pi, 1000)
v = np.linspace(0, np.pi, 1000)
xs = 1 * np.outer(np.cos(u), np.sin(v))
ys = 1 * np.outer(np.sin(u), np.sin(v))
zs = 1 * np.outer(np.ones(np.size(u)), np.cos(v))
ax.set_title(
f"{species}, E = {E_keV/1000:.0f} MeV, initial pitch angle = {pitch_angle_deg} deg"
)
ax.plot_surface(xs, ys, zs, color="white", alpha=1)
ax.legend()
plt.show()
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot()
ax.set_aspect("equal")
ax.add_patch(
plt.Circle(
(0, 0), 1, zorder=99, facecolor="white", edgecolor="black", label="Earth"
)
)
ax.plot([-10, 10], [0, 0], color="black")
ax.plot([0, 0], [-10, 10], color="black")
ax.plot(
x,
y,
color="red",
label="Particle Trajectory",
linewidth=trajectory_linewidth,
zorder=200,
)
ax.set_xlabel("x ($R_E$)")
ax.set_ylabel("y ($R_E$)")
ax.set_xlim(-L - 1, L + 1)
ax.set_ylim(-L - 1, L + 1)
theta = np.linspace(0, 2 * np.pi, 100)
rc = L
xc = rc * np.cos(theta)
yc = rc * np.sin(theta)
plt.plot(xc, yc, color="green", zorder=-1, label=f"Circle with radius L={L}")
ax.set_title(
f"{species}, E = {E_keV/1000:.0f} MeV, initial pitch angle = {pitch_angle_deg} deg"
)
ax.legend(loc=1)
plt.show()
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot()
ax.add_patch(
plt.Circle(
(0, 0), 1, zorder=99, facecolor="white", edgecolor="black", label="Earth"
)
)
ax.set_aspect("equal")
ax.plot([-10, 10], [0, 0], color="black")
ax.plot([0, 0], [-10, 10], color="black")
ax.plot(
x,
z,
color="red",
label="Particle Trajectory",
linewidth=trajectory_linewidth,
zorder=200,
)
ax.set_xlabel("x ($R_E$)")
ax.set_ylabel("z ($R_E$)")
ax.set_xlim(-L - 1, L + 1)
ax.set_ylim(-L - 1, L + 1)
xf2, zf2 = field_line_2D(L)
ax.plot(xf2, zf2, zorder=-1, color="blue", label=f"Magnetic field line (L={L})")
ax.set_title(
f"{species}, E = {E_keV/1000:.0f} MeV, initial pitch angle = {pitch_angle_deg} deg"
)
ax.legend()
plt.show()
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot()
ax.add_patch(
plt.Circle(
(0, 0), 1, zorder=99, facecolor="white", edgecolor="black", label="Earth"
)
)
ax.set_aspect("equal")
ax.plot([-10, 10], [0, 0], color="black")
ax.plot([0, 0], [-10, 10], color="black")
ax.plot(
x,
z,
color="red",
label="Particle Trajectory",
linewidth=trajectory_linewidth,
zorder=200,
)
ax.set_xlabel("x ($R_E$)")
ax.set_ylabel("z ($R_E$)")
ax.set_xlim(-L - 1, L + 1)
ax.set_ylim(-L - 1, L + 1)
xf2, zf2 = field_line_2D(L)
ax.plot(xf2, zf2, zorder=-1, color="blue", label=f"Magnetic field line (L={L})")
ax.set_title(
f"{species}, E = {E_keV/1000:.0f} MeV, initial pitch angle = {pitch_angle_deg} deg"
)
ax.legend()
plt.show()
fig = plt.figure(figsize=(16, 8))
ax1 = plt.subplot(121)
ax2 = plt.subplot(122, projection="3d")
xdata = x
ydata = y
zdata = z
sc1 = ax1.scatter([], [], color="green")
(ln1,) = ax1.plot([], [], "r-", zorder=99)
sc2 = ax2.scatter([], [], [], color="green")
(ln2,) = ax2.plot([], [], [], "r-", zorder=99)
# Field line (2D)
xf2, zf2 = field_line_2D(L)
ax1.plot(xf2, zf2, zorder=-1, color="blue", label=f"Magnetic field line (L={L})")
# Earth (2D)
ax1.add_patch(plt.Circle((0, 0), 1, zorder=99, facecolor="white", edgecolor="black"))
# Field line (3D)
for az in np.arange(0, 361, 20):
xf, yf, zf = field_line_3D(az, L)
if az == 360:
ax2.plot(
xf,
yf,
zf,
color="blue",
linewidth=fieldline_linewidth,
zorder=-1,
label=f"Magnetic Field (L={L})",
)
else:
ax2.plot(xf, yf, zf, color="blue", linewidth=fieldline_linewidth, zorder=-1)
# Earth (3D)
u = np.linspace(0, 2 * np.pi, 1000)
v = np.linspace(0, np.pi, 1000)
xs = 1 * np.outer(np.cos(u), np.sin(v))
ys = 1 * np.outer(np.sin(u), np.sin(v))
zs = 1 * np.outer(np.ones(np.size(u)), np.cos(v))
ax2.plot_surface(xs, ys, zs, color="white", alpha=1)
def init():
ax1.set_aspect("equal")
ax1.plot([-10, 10], [0, 0], color="black")
ax1.plot([0, 0], [-10, 10], color="black")
ax1.set_xlabel("x ($R_E$)")
ax1.set_ylabel("z ($R_E$)")
ax1.set_xlim(-L - 1, L + 1)
ax1.set_ylim(-L - 1, L + 1)
ax2.set_aspect("equal")
ax2.plot([-10, 10], [0, 0], [0, 0], color="black")
ax2.plot([0, 0], [-10, 10], [0, 0], color="black")
ax2.plot([0, 0], [0, 0], [-10, 10], color="black")
ax2.set_xlabel("x ($R_E$)")
ax2.set_ylabel("y ($R_E$)")
ax2.set_zlabel("z ($R_E$)")
ax2.set_xlim(-L, L)
ax2.set_ylim(-L, L)
ax2.set_zlim(-L, L)
return ln1, ln2
def update(frame):
sc1.set_offsets([xdata[frame - 1], zdata[frame - 1]])
ln1.set_data(xdata[:frame], zdata[:frame])
ln1.set_label(f"t={frame}")
ax1.legend()
sc2._offsets3d = ([xdata[frame - 1]], [ydata[frame - 1]], [zdata[frame - 1]])
ln2.set_data(xdata[:frame], ydata[:frame])
ln2.set_3d_properties(zdata[:frame])
ln2.set_label(f"t={frame}")
ax2.legend()
return ln1, ln2
ani = FuncAnimation(
fig, update, frames=np.arange(1, len(xdata)), init_func=init, blit=True
)
ani.save("simulation.gif", fps=30)
ani.save("simulation.mp4", fps=30)