Anomalous Transport
HallThruster has a few anomalous transport models built in and allows users to define their own. This page describes these models and the process by which algebraic and multi-equation transport models can be added by the user.
The AnomalousTransportModel interface is not yet finalized and subject to revision. Keep this in mind when using this feature.
Built-in Models
!!! warning Incomplete documentation Some models (those relating to pressure-dependent effects) have not been finalized and are not represented in this documentation. Please see the source code for a complete listing.
NoAnom()
Model for no anomalous transport (anomalous collision frequency = 0).
anom_model = NoAnom()
Bohm(c)
Model where the anomalous collision frequency scales with the electron cyclotron frequency ωce times some scaling factor c
TwoZoneBohm(c1, c2)
HallThruster's default anomalous transport option. This is a standard model of anomalous transport frequently used in Hall thruster simulations. The anomalous collision frequency is defined as
\[\begin{aligned} \nu_{AN} &= c_1 \omega_{ce} \quad z < L_{ch} \\ &= c_2 \omega_{ce} \quad z > L_{ch} \end{aligned}\]
In the above expression, $c_1$ and $c_2$ are tunable coefficients, $\omega_{ce} = e B / m_e$ is the electron cyclotron frequency, and $L_{ch}$ is the channel length. A TwoZoneBohm
model is initialized as follows
anom_model = TwoZoneBohm(c1, c2)
The transition between the zones is determined by the user-provided transition function. This defaults to a step function.
MultiLogBohm(z, c)
Model similar to that employed in Hall2De, where the mobility is Bohm-like (i.e. νan(z) = c(z) * ωce(z)
) and z is in meters.
The function c(z)
is defined by a sequence of nodes (z, c)
provided by the user. At z = z[1]
, c(z) = c[1]
, and so forth.
At z[i] < z < z[i+1]
, log(c)
is defined by linearly interpolating between log(c[i])
and log(c[i+1])
.
For z < z[1]
, c = c[1]
and for z > z[end]
, c(z) = c[end]
.
The user may also provide a single array of [z[1], z[2], ..., z[end], c[1], c[2], ..., c[end]]
. The number of z
values must be equal to the number of c values.
The AnomalousTransportModel
interface
Currently, HallThruster.jl expects all models to be written to be callable structs, taking arguments U, params, i
, where U
is the system state vector, params
are the simulation parameters (including the cache of all variables), and i
is the index of the cell.
Custom anomalous transport models
Users of HallThruster may define their own models by defining a custom subtype of AnomalousTransportModel
. Suppose we want to implement $nu_{AN} = \beta \omega_{ce}$ (classic Bohm diffusion). This is a fixed anomalous transport model and does not change as the simulation progresses. We would first define our type:
using HallThruster
struct BohmDiffusion <: AnomalousTransportModel
β::Float64
end
We then need to define the function which computes the anomalous transport in each cell. This is a mutating function which takes two arguments: the vector of anomalous collision frequency values to be updated, and the solver params.
function (model::BohmDiffusion)(νan, params)
e = HallThruster.e
me = HallThruster.me
B = params.cache.B
for i in eachindex(νan)
ωce = e * B[i] / me
νan[i] = model.β * ωce
end
return νan
end
We can now set anom_model = BohmDiffusion
in our config struct (see Configuration) and the simulation will correctly compute the anomalous transport according to our model.
More complex anomalous transport models
Up until now, we have only defined algebraic models of anomalous electron transport. For more high-fidelity models, we might need to solve multiple partial differential equations. Even if we don't want to do that, we still might want to compute other anomalous transport- related quantities at the same time that we update the anomalous transport. Lets see how we might do this.
As a first example, let's compute an anomalous transport that depends on energy density of electrostatic waves in the plasma. This model derives from Lafleur, Chabert, and Balruud (2016) and has the following form:
\[\begin{aligned} \nu_{AN} = K \frac{\nabla \cdot \mathbf{u}_i W}{m_e n_e c_s v_{de}} \end{aligned}\]
In this expression, $K$ is a tunable coefficient, $u_i$ is the ion velocity, $W = n_e k_B T_e$ is the wave energy density, $m_e$ is the electron mass, $n_e$ is the electron number density, $c_s$ is the ion sound speed, and $v_{de}$ is the electron azimuthal drift speed. Let's say we want to save the wave energy density in addition to the anomalous collision frequency. We begin by defining the model:
struct LafleurModel <: AnomalousTransportModel
K::Float64
end
Next, we add a method to the num_anom_variables
function. Since we want to save the wave energy density, we need 1 additional anomalous transport variable.
num_anom_variables(::LafleurModel) = 1
Now, we define the behavior of the model in a function. Since the model is based on assuming the wave energy convects with the ions, we will use upwind differencing for the gradient.
function (model::LafleurModel)(νan, params)
(;config, cache) = params
mi = config.propellant.m
K = model.K
e = HallThruster.e
me = HallThruster.me
(;ne, Tev, ue, ui, νe, anom_variables) = cache
ncells = length(νan)
for i in 2:ncells-1
W = e * ne[i] * Tev[i]
Hall_param = e * B[i] / me / νe[i]
vde = Hall_param * ue[i]
cs = sqrt(e * Tev[i] / mi)
# Upwind differencing of gradient term
if ui > 0
dz = params.z_cell[i] - params.z_cell[i-1]
W_left = e * cache.ne[i-1] * cache.Te[i-1]
grad_ui_W = (ui[1, i] * W[i] - cache.ui[1, i-1] * W_left) / dz
else
dz = params.z_cell[i+1] - params.z_cell[i]
W_right = e * cache.ne[i+1] * cache.Te[i+1]
grad_ui_W = (cache.ui[1, i+1] * W_right - ui[i] * W[i]) / dz
end
# Save W to cache.anom_variables[1]
anom_variables[1][i] = W
# Return anomalous collision frequency
νan[i] = abs(K * grad_ui_W / (me * cs * vde * ne))
end
# Neumann BC anomalous transport
anom_variables[1][1] = anom_variables[1][2]
anom_variables[1][end] = anom_variables[1][end-1]
νan[1] = νan[2]
νan[end] = νan[end-1]
return νan
end
The saved value of the wave energy density can then be recovered as
solution.savevals[frame].anom_variables[1]
where solution
is the Solution
object resulting from a call to run_simulation
Solving arbitrary PDEs using the AnomalousTransportModel interface
We can use this basic interface to solve PDEs within HallThruster.jl. We demonstrate this by solving the scalar advection equation using first-order upwind differencing in space and forward Euler integration in time, with periodic boundary conditions. The scalar advection equation is given by:
\[ \begin{aligned} \frac{\partial u}{\partial t} + a \frac{\partial u}{\partial x} = 0 \end{aligned}\]
To begin, we define the model struct and the number of variables we need.
using HallThruster, Plots
struct ScalarAdvection{F} <: HallThruster.AnomalousTransportModel
advection_velocity::Float64 # Advection advection_velocity
initializer::F # Initialization function
end
# Save two auxilliary variables
# 1) Advected quantity u
# 2) gradient of advected quantity (du/dz)
HallThruster.num_anom_variables(::ScalarAdvection) = 2
Next, we define the model function:
function (model::ScalarAdvection)(νan, params)
ncells = length(νan)
# Extract variables from params
cache = params.cache
z = params.z_cell
u = cache.anom_variables[1]
du_dz = cache.anom_variables[2]
a = model.advection_velocity
dt = params.dt
if params.iteration[] < 1
# Initialize
model.initializer(u, z)
else
for i in eachindex(νan)
# Setup for periodic boundary conditions
if i == 1
i_minus_1 = ncells
dz_minus = z[2] - z[1]
else
i_minus_1 = i - 1
dz_minus = z[i] - z[i-1]
end
if i == ncells
i_plus_1 = 1
dz_plus = z[2] - z[1]
else
i_plus_1 = i + 1
dz_plus = z[i+1] - z[i]
end
# Update gradient
if a > 0
du_dz[i] = (u[i] - u[i_minus_1]) / dz_minus
else
du_dz[i] = (u[i_plus_1] - u[i]) / dz_plus
end
end
# Update advected quantity
for i in eachindex(νan)
u[i] -= a * du_dz[i] * dt
end
end
# Return a two-zone bohm anomalous transport result,
# since we don't really care about the anomalous transport.
return HallThruster.TwoZoneBohm(1/160, 1/16)(νan, params)
end
And that's it! Now all there is to do is define our simulation parameters and run!
# Set up config
advection_velocity = 1e5
L = 0.08
config = HallThruster.Config(
domain = (0.0, L),
anom_model = ScalarAdvection(advection_velocity, initializer),
thruster = HallThruster.SPT_100,
discharge_voltage = 300.0,
anode_mass_flow_rate = 5e-6
)
# Define dt such that CFL condition is obeyed
ncells = 200
dx = L / ncells
CFL = 0.9
dt = min(1e-8, dx * CFL / advection_velocity)
nsteps = 1000
# Run simulation
solution = HallThruster.run_simulation(
config;
ncells,
dt,
duration = nsteps * dt,
nsave = nsteps
)
# Extract variables from solution
z = solution.params.z_cell
u = [saveval.anom_variables[1] for saveval in solution.savevals]
We can now visualize the results to make sure everything worked well.
using Plots
# Time needed to transit the domain
t_transit = L / advection_velocity
# Number of periods
num_periods = floor(Int, nsteps * dt / t_transit)
# Plot results
p = plot(; framestyle = :box, xlabel = "x", ylabel = "u", title = "First order upwind for scalar advection")
for i in 0:num_periods
index = round(Int, i * t_transit / dt) + 1
plot!(
p, z, u[index], label = "After $i periods",
linecolor = cgrad(:turbo, num_periods + 1, categorical = true)[i+1]
)
end
display(p)
This looks correct! In this case, we haven't coupled our PDE solution to the anomalous transport, but one could easily do this. In the same way, systems of two, three, or more coupled PDEs can be solved and related to the anomalous collision frequency.
The full script is reproduced below:
using HallThruster
struct ScalarAdvection{F} <: HallThruster.AnomalousTransportModel
advection_velocity::Float64 # Advection advection_velocity
initializer::F # Initialization function
end
# Save two auxilliary variables
# 1) Advected quantity u
# 2) gradient of advected quantity (du/dz)
HallThruster.num_anom_variables(::ScalarAdvection) = 2
function (model::ScalarAdvection)(νan, params)
ncells = length(νan)
# Extract variables from params
cache = params.cache
z = params.z_cell
u = cache.anom_variables[1]
du_dz = cache.anom_variables[2]
a = model.advection_velocity
dt = params.dt
if params.iteration[] < 1
# Initialize
model.initializer(u, z)
else
for i in eachindex(νan)
# Setup for periodic boundary conditions
if i == 1
i_minus_1 = ncells
dz_minus = z[2] - z[1]
else
i_minus_1 = i - 1
dz_minus = z[i] - z[i-1]
end
if i == ncells
i_plus_1 = 1
dz_plus = z[2] - z[1]
else
i_plus_1 = i + 1
dz_plus = z[i+1] - z[i]
end
# Update gradient
if a > 0
du_dz[i] = (u[i] - u[i_minus_1]) / dz_minus
else
du_dz[i] = (u[i_plus_1] - u[i]) / dz_plus
end
end
# Update advected quantity
for i in eachindex(νan)
u[i] -= a * du_dz[i] * dt
end
end
# Return a two-zone bohm anomalous transport result,
# since we don't really care about the anomalous transport.
return HallThruster.TwoZoneBohm(1/160, 1/16)(νan, params)
end
# Define initializer function, which is a step function
# between z = 0.01 and z = 0.02
function initializer(u, z)
for i in eachindex(u)
if 0.01 < z[i] < 0.02
u[i] = 1.0
else
u[i] = 0.0
end
end
return u
end
# Set up config
advection_velocity = 1e5
L = 0.08
config = HallThruster.Config(
domain = (0.0, L),
anom_model = ScalarAdvection(advection_velocity, initializer),
thruster = HallThruster.SPT_100,
discharge_voltage = 300.0,
anode_mass_flow_rate = 5e-6
)
# Define dt such that CFL condition is obeyed
ncells = 200
dx = L / ncells
CFL = 0.9
dt = min(1e-8, dx * CFL / advection_velocity)
nsteps = 1000
# Run simulation
solution = HallThruster.run_simulation(
config;
ncells,
dt,
duration = nsteps * dt,
nsave = nsteps
)
# Extract variables from solution
z = solution.params.z_cell
u = [saveval.anom_variables[1] for saveval in solution.savevals]
using Plots
# Time needed to transit the domain
t_transit = L / advection_velocity
# Number of periods
num_periods = floor(Int, nsteps * dt / t_transit)
# Plot results
p = plot(; framestyle = :box, xlabel = "x", ylabel = "u", title = "First order upwind for scalar advection")
for i in 0:num_periods
index = round(Int, i * t_transit / dt) + 1
plot!(
p, z, u[index], label = "After $i periods",
linecolor = cgrad(:turbo, num_periods + 1, categorical = true)[i+1]
)
end
display(p)