import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from matplotlib.animation import FuncAnimation
Theory
This post is based on the homework report that was written during the solar terrestrial physics class in 2022.
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
= 0.31 * 1e-4
Be # Re = 6371 km = 6371 * 10^3 m
= 6371 * 1e3
Re = Be * (Re**3)
C
def B(x, y, z):
"""dipole field at (x, y, z)"""
= np.sqrt(x**2 + y**2 + z**2)
r = -1 * C * (3 * x * z) / (r**5)
Bx = -1 * C * (3 * y * z) / (r**5)
By = C * (r**2 - 3 * z**2) / (r**5)
Bz return Bx, By, Bz
def field_line_3D(phi, L=6.6):
"""dipole field line (3D)"""
= np.deg2rad(phi)
phi = np.linspace(0, 2 * np.pi, 1000)
theta = L * np.sin(theta) ** 2
rf = rf * np.sin(theta) * np.cos(phi)
xf = rf * np.sin(theta) * np.sin(phi)
yf = rf * np.cos(theta)
zf return xf, yf, zf
def field_line_2D(L=6.6):
"""dipole field line (2D)"""
= np.linspace(0, 2 * np.pi, 1000)
lamb = L * np.cos(lamb) ** 2
rf2 = rf2 * np.cos(lamb)
xf2 = rf2 * np.sin(lamb)
zf2 return xf2, zf2
def dSdt(S, t, q_over_m):
"""dS/dt for odeint"""
= S
x, y, z, vx, vy, vz = B(x, y, z)
Bx, By, Bz = q_over_m * (vy * Bz - vz * By)
dvxdt = q_over_m * (vz * Bx - vx * Bz)
dvydt = q_over_m * (vx * By - vy * Bx)
dvzdt return [vx, vy, vz, dvxdt, dvydt, dvzdt]
= plt.figure(figsize=(8, 8))
fig = fig.add_subplot()
ax = -7, 7
xmin, xmax = -7, 7
ymin, ymax
ax.add_patch(
plt.Circle(0, 0), 1, zorder=99, facecolor="white", edgecolor="black", label="Earth"
(
)
)"equal")
ax.set_aspect(
ax.legend()=0, color="black", linewidth=0.5)
ax.axhline(yTrue, which="both")
ax.xaxis.grid(
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)= np.arange(xmin + 1, xmax, 1)
majors
ax.xaxis.set_major_locator(ticker.FixedLocator(majors))"x ($R_E$)")
ax.set_xlabel("z ($R_E$)")
ax.set_ylabel(
= [2, 4, 6, 8, 10, 20, 100]
Lvalues = ["r", "darkorange", "gold", "green", "blue", "magenta", "purple"]
colors = np.linspace(0, 2 * np.pi, 1000)
lamb for i, L in enumerate(Lvalues):
= field_line_2D(L)
x, z =f"L={L}", color=colors[i])
ax.plot(x, z, label
ax.legend()"Magnetic Dipole Field line (2D)")
ax.set_title( plt.show()
= "Proton"
species
= 1.602e-19
e = e # C
q
= 1.67e-27
mH = mH # kg
m = q / m
q_over_m
= 2000 # keV E_keV
# L-parameter
= 6.6
L
# start at equator
= L * Re, 0, 0
x0, y0, z0
= 1e3 * e # J
keV_to_J
# particle energy (keV) and pitch angle
= E_keV * keV_to_J # J
E = 30
pitch_angle_deg = np.deg2rad(pitch_angle_deg)
alpha
# particle velocity
= np.sqrt(2 * E / m)
v0
# vx = v_perp
# vy = 0
# vz = v_para
= v0 * np.sin(alpha)
vx0 = 0
vy0 = v0 * np.cos(alpha)
vz0
# bounce time_scale
= 290 * (np.pi * L / 10) * np.sqrt(m / (mH * E_keV))
t_B print(f"bounce time scale ~ {t_B} s")
= [x0, y0, z0, vx0, vy0, vz0]
S0
# number of bounce
= 3
n
= 0
tmin = n * t_B
tmax
= np.linspace(tmin, tmax, 1000)
t
# solve ODE
= odeint(dSdt, S0, t, args=(q_over_m,))
sol = sol.T
x, y, z, vx, vy, vz = x / Re, y / Re, z / Re
x, y, z
print(f"t_max ~ {tmax:.4f} s")
bounce time scale ~ 13.44549539521195 s
t_max ~ 40.3365 s
= 0.8
trajectory_linewidth = 0.5 fieldline_linewidth
# xyzrange = 10
= plt.figure(figsize=(8, 8))
fig = fig.add_subplot(projection="3d")
ax "equal")
ax.set_aspect(# 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,="red",
color="Particle Trajectory",
label=trajectory_linewidth,
linewidth=200,
zorder
)"x ($R_E$)")
ax.set_xlabel("y ($R_E$)")
ax.set_ylabel("z ($R_E$)")
ax.set_zlabel(-L, L)
ax.set_xlim(-L, L)
ax.set_ylim(-L, L)
ax.set_zlim(for az in np.arange(0, 361, 20):
= field_line_3D(az, L)
xf, yf, zf if az == 360:
ax.plot(
xf,
yf,
zf,="blue",
color=fieldline_linewidth,
linewidth=-1,
zorder=f"Magnetic Field (L={L})",
label
)else:
="blue", linewidth=fieldline_linewidth, zorder=-1)
ax.plot(xf, yf, zf, color
# Sphere with radius Re
= np.linspace(0, 2 * np.pi, 1000)
u = np.linspace(0, np.pi, 1000)
v = 1 * np.outer(np.cos(u), np.sin(v))
xs = 1 * np.outer(np.sin(u), np.sin(v))
ys = 1 * np.outer(np.ones(np.size(u)), np.cos(v))
zs
ax.set_title(f"{species}, E = {E_keV/1000:.0f} MeV, initial pitch angle = {pitch_angle_deg} deg"
)="white", alpha=1)
ax.plot_surface(xs, ys, zs, color
ax.legend() plt.show()
= plt.figure(figsize=(8, 8))
fig = fig.add_subplot()
ax "equal")
ax.set_aspect(
ax.add_patch(
plt.Circle(0, 0), 1, zorder=99, facecolor="white", edgecolor="black", label="Earth"
(
)
)-10, 10], [0, 0], color="black")
ax.plot([0, 0], [-10, 10], color="black")
ax.plot([
ax.plot(
x,
y,="red",
color="Particle Trajectory",
label=trajectory_linewidth,
linewidth=200,
zorder
)"x ($R_E$)")
ax.set_xlabel("y ($R_E$)")
ax.set_ylabel(-L - 1, L + 1)
ax.set_xlim(-L - 1, L + 1)
ax.set_ylim(= np.linspace(0, 2 * np.pi, 100)
theta = L
rc = rc * np.cos(theta)
xc = rc * np.sin(theta)
yc ="green", zorder=-1, label=f"Circle with radius L={L}")
plt.plot(xc, yc, color
ax.set_title(f"{species}, E = {E_keV/1000:.0f} MeV, initial pitch angle = {pitch_angle_deg} deg"
)=1)
ax.legend(loc plt.show()
= plt.figure(figsize=(8, 8))
fig = fig.add_subplot()
ax
ax.add_patch(
plt.Circle(0, 0), 1, zorder=99, facecolor="white", edgecolor="black", label="Earth"
(
)
)"equal")
ax.set_aspect(-10, 10], [0, 0], color="black")
ax.plot([0, 0], [-10, 10], color="black")
ax.plot([
ax.plot(
x,
z,="red",
color="Particle Trajectory",
label=trajectory_linewidth,
linewidth=200,
zorder
)"x ($R_E$)")
ax.set_xlabel("z ($R_E$)")
ax.set_ylabel(-L - 1, L + 1)
ax.set_xlim(-L - 1, L + 1)
ax.set_ylim(
= field_line_2D(L)
xf2, zf2 =-1, color="blue", label=f"Magnetic field line (L={L})")
ax.plot(xf2, zf2, zorder
ax.set_title(f"{species}, E = {E_keV/1000:.0f} MeV, initial pitch angle = {pitch_angle_deg} deg"
)
ax.legend() plt.show()
= plt.figure(figsize=(8, 8))
fig = fig.add_subplot()
ax
ax.add_patch(
plt.Circle(0, 0), 1, zorder=99, facecolor="white", edgecolor="black", label="Earth"
(
)
)"equal")
ax.set_aspect(-10, 10], [0, 0], color="black")
ax.plot([0, 0], [-10, 10], color="black")
ax.plot([
ax.plot(
x,
z,="red",
color="Particle Trajectory",
label=trajectory_linewidth,
linewidth=200,
zorder
)"x ($R_E$)")
ax.set_xlabel("z ($R_E$)")
ax.set_ylabel(-L - 1, L + 1)
ax.set_xlim(-L - 1, L + 1)
ax.set_ylim(
= field_line_2D(L)
xf2, zf2 =-1, color="blue", label=f"Magnetic field line (L={L})")
ax.plot(xf2, zf2, zorder
ax.set_title(f"{species}, E = {E_keV/1000:.0f} MeV, initial pitch angle = {pitch_angle_deg} deg"
)
ax.legend() plt.show()
= plt.figure(figsize=(16, 8))
fig = plt.subplot(121)
ax1 = plt.subplot(122, projection="3d")
ax2
= x
xdata = y
ydata = z
zdata
= ax1.scatter([], [], color="green")
sc1 = ax1.plot([], [], "r-", zorder=99)
(ln1,) = ax2.scatter([], [], [], color="green")
sc2 = ax2.plot([], [], [], "r-", zorder=99)
(ln2,)
# Field line (2D)
= field_line_2D(L)
xf2, zf2 =-1, color="blue", label=f"Magnetic field line (L={L})")
ax1.plot(xf2, zf2, zorder
# Earth (2D)
0, 0), 1, zorder=99, facecolor="white", edgecolor="black"))
ax1.add_patch(plt.Circle((
# Field line (3D)
for az in np.arange(0, 361, 20):
= field_line_3D(az, L)
xf, yf, zf if az == 360:
ax2.plot(
xf,
yf,
zf,="blue",
color=fieldline_linewidth,
linewidth=-1,
zorder=f"Magnetic Field (L={L})",
label
)else:
="blue", linewidth=fieldline_linewidth, zorder=-1)
ax2.plot(xf, yf, zf, color
# Earth (3D)
= np.linspace(0, 2 * np.pi, 1000)
u = np.linspace(0, np.pi, 1000)
v = 1 * np.outer(np.cos(u), np.sin(v))
xs = 1 * np.outer(np.sin(u), np.sin(v))
ys = 1 * np.outer(np.ones(np.size(u)), np.cos(v))
zs ="white", alpha=1)
ax2.plot_surface(xs, ys, zs, color
def init():
"equal")
ax1.set_aspect(-10, 10], [0, 0], color="black")
ax1.plot([0, 0], [-10, 10], color="black")
ax1.plot(["x ($R_E$)")
ax1.set_xlabel("z ($R_E$)")
ax1.set_ylabel(-L - 1, L + 1)
ax1.set_xlim(-L - 1, L + 1)
ax1.set_ylim(
"equal")
ax2.set_aspect(-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.plot(["x ($R_E$)")
ax2.set_xlabel("y ($R_E$)")
ax2.set_ylabel("z ($R_E$)")
ax2.set_zlabel(-L, L)
ax2.set_xlim(-L, L)
ax2.set_ylim(-L, L)
ax2.set_zlim(
return ln1, ln2
def update(frame):
- 1], zdata[frame - 1]])
sc1.set_offsets([xdata[frame
ln1.set_data(xdata[:frame], zdata[:frame])f"t={frame}")
ln1.set_label(
ax1.legend()
= ([xdata[frame - 1]], [ydata[frame - 1]], [zdata[frame - 1]])
sc2._offsets3d
ln2.set_data(xdata[:frame], ydata[:frame])
ln2.set_3d_properties(zdata[:frame])f"t={frame}")
ln2.set_label(
ax2.legend()return ln1, ln2
= FuncAnimation(
ani =np.arange(1, len(xdata)), init_func=init, blit=True
fig, update, frames
)
"simulation.gif", fps=30) ani.save(
"simulation.mp4", fps=30) ani.save(