Electron Fermi Acceleration Inside Foreshock Transient Cores

Source code compat Author Update time

This example demonstrates electron acceleration via Fermi acceleration using a simple 1D model. It follows the setup described in Fermi acceleration of electrons inside foreshock transient cores. The simulation domain is $2\,R_E$ in x, where the initial magnetosheath locates at $x \in [0, 0.5] R_E$ with a magnetic field $B_z = 20\,\mathrm{nT}$ and the bow shock at $x = 0.5\,R_E$. The foreshock transient boundary is at $x = 1.5\,R_E$ initially and moves toward the bow shock at a speed $U=100\,\mathrm{km/s}$. The region beyond $x=1.5\,R_E$ is the foreshock transient sheath in which the magnetic field is $B_z=10\,\mathrm{nT}$. A convection electric field $E_y = -1\,\mathrm{mV/m}$, consistent with the velocity U, is introduced in the foreshock transient sheath. Between the two boundaries, $0.5\,R_E < x < 1.5\,R_E$, is the foreshock transient's core region.

In the first case, the magnetic field in the core region is set to zero. In the second case, a magnetic fluctuation is imposed:

\[\begin{aligned} \delta B_{x,y,z} = \sum_{N = N_0}^{N_1} \delta B_N \cos\left( \frac{2\pi N x}{L_0} + \phi_{x,y,z}^N \right) \\ \delta B_N = \tilde{B} (N/N_0)^{-1.2},\quad N = N_0, N_0 + 1, ..., N_1 \end{aligned}\]

Here $L_0 = 1\,R_E$ is the initial length of the core in the x direction. We choose $N_0 = 100, N_1 = 1000, \tilde{B}=0.2\,\mathrm{nT}$, and $\phi_{x,y,z}^N$ as the random phases of various modes between 0 and 2π (independently different in the x, y, and z directions). As the low-frequency wave speed ($\sim \mathcal{O}(10)\mathrm{km/s}$) is much smaller than the electron speed ($\sim \mathcal{O}(10^3)\mathrm{km/s}$), we do not include wave propagation in this 1D model.

One important note about Fermi acceleration for space plasmas is that space plasmas are collisionless. Electric field is the only way to accelerate charged particles, instead of elastic collisions.

using TestParticle
using TestParticle: kB, mₑ, Rₑ, qₑ
using OrdinaryDiffEq
using Random
using StaticArrays
using FHist
using CairoMakie, Printf

# For reproducible results
Random.seed!(1234)

# Analytic EM fields

function Bcase1(xu, t)
   Bz =
      if xu[1] < 0.5Rₑ
         20e-9
      elseif xu[1] > 1.5Rₑ + U*t
         10e-9
      else
         0.0
      end

   SA[0.0, 0.0, Bz]
end

function E(xu, t)
   Ey =
      if xu[1] > 1.5Rₑ + U*t
         -1e-3
      else
         0.0
      end

   SA[0.0, Ey, 0.0]
end

function Bcase2(xu, t)
   if xu[1] < 0.5Rₑ
      SA[0.0, 0.0, 20e-9]
   elseif xu[1] > 1.5Rₑ + U*t
      SA[0.0, 0.0, 10e-9]
   else
      δBfunc(xu)
   end
end

function get_B_perturb(x)
   L₀, N₀, N₁, B̃ = 1Rₑ, 100, 1000, 0.2e-9
   B = fill(0.0, 3, length(x)) # [T]
   # Eqs (4-5) from the paper
   for N in N₀:N₁
      δBn = B̃ * (N / N₀)^-1.2
      ϕx, ϕy, ϕz = rand(SVector{3, Float64}) .* 2
      for i in axes(B, 2)
         δBx = δBn * cospi(2 * N * x[i] / L₀ + ϕx)
         δBy = δBn * cospi(2 * N * x[i] / L₀ + ϕy)
         δBz = δBn * cospi(2 * N * x[i] / L₀ + ϕz)
         B[1,i] += δBx
         B[2,i] += δBy
         B[3,i] += δBz
      end
   end

   B
end

function isoutofdomain(xv, p, t)
   if xv[1] < 0 || xv[1] > 2Rₑ
      return true
   else
      return false
   end
end

function prob_func(prob, i, repeat)
   x0 = [(0.5 + rand())*Rₑ, 0.0, 0.0] # launched in the core region
   u0 = [0.0, 0.0, 0.0]
   T₀ = 10 # [eV]
   vth = √(2T₀*abs(qₑ)/mₑ) # [m/s]
   vdf = Maxwellian(u0, vth)
   v0 = TestParticle.sample(vdf)

   prob = @views remake(prob, u0=[x0..., v0...])
end

"Kinetic energy."
get_kinetic_energy(dx, dy, dz) = 1 // 2 * (dx^2 + dy^2 + dz^2)

function plot_multiple(sol)
   energy = map(x -> get_kinetic_energy(x[4:6]...), sol.u) .* mₑ ./ abs(qₑ)
   # Obtain the EM fields along the particle trajectory
   # [mV/m]
   E = [sol.prob.p[2](sol[:,istep], sol.t[istep]).*1e3 for istep in eachindex(sol)]
   # [nT]
   B = [sol.prob.p[3](sol[:,istep], sol.t[istep]).*1e9 for istep in eachindex(sol)]

   Ex = [e[1] for e in E]
   Ey = [e[2] for e in E]
   Ez = [e[3] for e in E]
   Bx = [b[1] for b in B]
   By = [b[2] for b in B]
   Bz = [b[3] for b in B]

   t = sol.t
   x = @views sol[1,:] ./ Rₑ
   y = @views sol[2,:] ./ Rₑ
   z = @views sol[3,:] ./ Rₑ
   vx = @views sol[4,:] ./ 1e3
   vy = @views sol[5,:] ./ 1e3
   vz = @views sol[6,:] ./ 1e3

   fig = Figure(size = (900, 600), fontsize = 20)

   xlabels = ("", "", "", "", "t [s]")
   ylabels = ("KE [eV]", "Locations [RE]", "V [km/s]","E [mV/m]", "B [nT]")
   limits = (
      (nothing, (nothing, nothing)),
      (nothing, (nothing, nothing)),
      (nothing, (nothing, nothing)),
      (nothing, (nothing, nothing)),
      (nothing, (nothing, nothing)))

   axs = [Axis(fig[row, col], xlabel=xlabels[row], ylabel=ylabels[row], limits=limits[row])
      for row in eachindex(xlabels), col in 1:1]

   linkxaxes!(axs...)

   lines!(axs[1], t, energy)
   lines!(axs[2], t, x, label="x")
   lines!(axs[2], t, y, label="y")
   lines!(axs[2], t, z, label="z")
   lines!(axs[3], t, vx, label="x")
   lines!(axs[3], t, vy, label="y")
   lines!(axs[3], t, vz, label="z")
   lines!(axs[4], t, Ex, label="x")
   lines!(axs[4], t, Ey, label="y")
   lines!(axs[4], t, Ez, label="z")
   lines!(axs[5], t, Bx, label="x")
   lines!(axs[5], t, By, label="y")
   lines!(axs[5], t, Bz, label="z")

   for ax in @view axs[2:5]
      axislegend(ax, framevisible = false, orientation = :horizontal)
   end

   fig
end

function plot_dist(sols; t=0, case=1, slice=:xy)
   ##TODO: Optimization
   vx = Vector{eltype(sols[1].u[1])}(undef, 0)
   vy = similar(vx)
   vz = similar(vx)
   for sol in sols
      if (sol.t[end] ≥ t) && (1.5Rₑ - U*sol.t[end] > sol[1,end] > 0.5Rₑ)
         v = sol(t)[4:6] ./ 1e3
         append!(vx, v[1])
         append!(vy, v[2])
         append!(vz, v[3])
      end
   end

   f = Figure(size=(700, 600), fontsize=18)

   if slice == :xy
      vars = (vx, vy)
      xlabel = L"V_x [km/s]"
      ylabel = L"V_y [km/s]"
   elseif slice == :xz
      vars = (vx, vz)
      xlabel = L"V_x [km/s]"
      ylabel = L"V_z [km/s]"
   elseif slice == :yz
      vars = (vy, vz)
      xlabel = L"V_y [km/s]"
      ylabel = L"V_z [km/s]"
   end
   h2d = Hist2D(vars; nbins=(50, 50))
   _, _heatmap = plot(f[1,1], h2d;
      axis=(title="t = $t s, Case = $case, Particle Count = $(length(vx))",
      xlabel=xlabel, ylabel=ylabel, aspect=1, limits=(-1e4, 1e4, -1e4, 1e4)))

   Colorbar(f[1,2], _heatmap)

   f
end

function find_max_acceleration_index(sols; countall=true, tend=40)
   if countall
      ratio = [get_kinetic_energy(sol[4:6,end]...) / get_kinetic_energy(sol[4:6,1]...)
         for sol in sols]
   else
      # only count the particles that are still trapped at t=tend
      ratio = [get_kinetic_energy(sol[4:6,end]...) / get_kinetic_energy(sol[4:6,1]...)
         for sol in sols if sol.t[end] > tend-0.1]
   end
   imax = argmax(ratio)

   energy_init = get_kinetic_energy(sols[imax][4:6,1]...) .* mₑ ./ abs(qₑ)
   energy_final = get_kinetic_energy(sols[imax][4:6,end]...) .* mₑ ./ abs(qₑ)
   @printf "Initial energy [eV]: %.2f " energy_init
   @printf "Final energy [eV]: %.2f " energy_final
   @printf "Kinetic energy change ratio: %.2f\n" ratio[imax]

   imax
end

const U = -100e3 # [m/s]

# Initial condition to be overwritten in prob_func
stateinit = zeros(6)
# Time span [s]
tspan = (0, 40)
# Number of particles
trajectories = 1000;

Case 1: 0 core field

param = prepare(E, Bcase1; species=Electron);
prob = ODEProblem(trace!, stateinit, tspan, param)
ensemble_prob = EnsembleProblem(prob; prob_func, safetycopy=false)

sols = solve(ensemble_prob, Vern9(), EnsembleThreads();
   isoutofdomain, trajectories, verbose=true);

# maximum acceleration ratio particle index
imax = find_max_acceleration_index(sols)

f = plot_multiple(sols[imax])

Trajectory of the most accelerated electron.

f = plot_dist(sols, t=tspan[1], case=1, slice=:xy)

Initial velocity distribution.

f = plot_dist(sols, t=tspan[2], case=1, slice=:xy)

Final velocity distribution

Case 2: B fluctuation core field In this case we use the native Boris pusher for demonstration. The smallest electron gyroperiod in the magnetosheath (B ∼ 20 nT) is about $2\times 10^{-3}\,\mathrm{s}$, and we use a time step $\Delta t = 2\times 10^{-4}\mathrm{s}$.

const δBfunc = let
   x = range(0.5Rₑ, 1.5Rₑ, length=10000)
   δB = get_B_perturb(x)
   TestParticle.Field(TestParticle.getinterp(δB, x, 1, 3))
end

dt = 2e-4 # [s]
param = prepare(E, Bcase2; species=Electron);
prob = TraceProblem(stateinit, tspan, param; prob_func)
sols = TestParticle.solve(prob; dt, trajectories, isoutofdomain, savestepinterval=100);

# maximum acceleration ratio particle index
imax = find_max_acceleration_index(sols)

f = plot_multiple(sols[imax])

Trajectory of the most accelerated electron. Note that there are locations where we see a jump in kinetic energy with no electric field peaks; these are artifacts because we only save every 100 steps.

f = plot_dist(sols, t=tspan[1], case=2, slice=:xy)

Initial velocity distribution.

f = plot_dist(sols, t=tspan[2], case=2, slice=:xy)

Final velocity distribution


This page was generated using DemoCards.jl and Literate.jl.