HallThruster is an efficient 1D fluid model to simulate Hall Thruster discharges. This interactive tutorial will show you how to get started with HallThruster.jl and will compare our results to the LANDMARK 1-D Hall thruster benchmark. More detailed information about the interface and internals of HallThruster.jl can be found in the Documentation.
To begin, install HallThruster.jl according to the instructions in the README. Then load it:
using Revise, HallThruster
Let's also load Plots for visualization:
using Plots
┌ Warning: backend `GR` is not installed. └ @ Plots C:\Users\thoma\.julia\packages\Plots\HyyIK\src\backends.jl:43
Let's try and replicate the LANDMARK benchmarks.
Let us define the function run_landmark()
as a helper function. The most important arguments are duration; ncells, nsave, dt
. When increasing the amount of cells, the timestep has to be reduced accordingly to satisfy the CFL condition. nsave
specifies the amount of times the solution is saved in constant time intervals.
Here we have the Landmark source terms and energy loss terms given as a standard, and we can select one of the 3 test cases to run. We initialize a new simulation and define the domain as twice the thruster length, as evident by the argument domain = (0.0, 0.05)
. The default flux is the rusanov
flux. Grids may be even or uneven. To generate an even grid with ncells
cells, create an EvenGrid
Uneven grids may be employed by passing an UnevenGrid
object instead of an EvenGrid
object. This allows you to either use a grid you generated yourself, to use an uneven grid generated using the default density function, or use a density function of your own.
function run_landmark(duration = 1e-3; ncells = 200, nsave = 2, dt = 0.7e-8, CFL = 0.799, case = 1)
domain = (0.0, 0.05)
#Landmark cases loss frequencies
αϵ_in, αϵ_out = if case == 1
(1.0, 1.0)
elseif case == 2
(0.5, 1.0)
elseif case == 3
(0.4, 1.0)
end
scheme = HallThruster.HyperbolicScheme(
# We use global_lax_friedrichs here to better handle case 1, as it is very oscillatory and this
# scheme is the most diffusive
# In general, prefer rusanov or HLLE
flux_function = HallThruster.global_lax_friedrichs,
limiter = HallThruster.minmod,
reconstruct = true
)
ϵ_anode = 3.0
ϵ_cathode = 3.0
config = HallThruster.Config(;
ncharge = 1,
scheme,
domain,
anode_Te = 2/3 * ϵ_anode,
cathode_Te = 2/3 * ϵ_cathode,
discharge_voltage = 300.0,
ionization_model = HallThruster.LandmarkIonizationLookup(),
excitation_model = HallThruster.LandmarkExcitationLookup(),
electron_neutral_model = HallThruster.LandmarkElectronNeutral(),
electron_ion_collisions = false,
wall_loss_model = HallThruster.ConstantSheathPotential(20, αϵ_in, αϵ_out),
LANDMARK = true,
neutral_velocity = 150.0,
ion_temperature = 0.0,
thruster = HallThruster.SPT_100,
anode_mass_flow_rate = 5e-6,
transition_length = 2.5e-3,
ion_wall_losses = false,
anom_model = HallThruster.TwoZoneBohm(1/160, 1/16),
anode_boundary_condition = :dirichlet,
conductivity_model = HallThruster.LANDMARK_conductivity(),
)
@time sol = HallThruster.run_simulation(
config; duration, grid = EvenGrid(ncells), nsave,
dt, dtmin = dt / 100, dtmax = dt * 10, adaptive = true, CFL
)
return sol
end;
Now we can use run_landmark()
to perform simulations of our desired cases.
# use low CFL due to this case being highly-oscillatory
sol_case1 = run_landmark(1e-3; ncells=150, nsave=1000, case = 1, CFL = 0.2)
Simulation exited at t = 0.001 with retcode :success in 44.1881557 seconds. 44.190008 seconds (48.62 k allocations: 46.723 MiB, 0.04% gc time)
Hall thruster solution with 1000 saved frames Retcode: success End time: 0.001 seconds
sol_case_1
is a Solution
object, which has the following fields
t
: The times at which the solution was savedu
: A vector of solution matrices of length nsave
. This contains the main state variables: $\rho_n$, $\rho_i$ and $\rho_i u_i$savevals
: A vector of NamedTuples containing some auxilliary variables including $\phi$, $n_e$, $T_{ev}$, $\nu_{AN}$ , $\nu_{en}$, $\nu_{ei}$, and $\mu$retcode
: A short symbol describing how the solution ended. Generally this should be :success
.params
: The params that the simulation was run with.HallThruster has some built-in plotting and postprocessing faculties. First, if we have Plots
loaded, we can type plot(sol, frame)
to plot the solution at the given frame (an Int
from 1:nsave
). Leaving out frame
will plot the last frame by default.
p = plot(sol_case1, label = "HallThruster.jl", legend = :outertop);
display(p)
We can load in the LANDMARK data for comparison:
landmark_1, landmark_2, landmark_hybrid = HallThruster.load_landmark_data(1);
function compare_to_landmark(sol, case)
p = plot(sol, label = "HallThruster.jl", legend = :outertop, lc = :black)
landmark_1, landmark_2, landmark_hybrid = HallThruster.load_landmark_data(case)
plot!(p, landmark_1, ls = :dash, lc = RGB(0.0, 1.0, 0.0), label = "LANDMARK, fluid, δ = 0.5 mm")
plot!(p, landmark_2, ls = :dash, lc = RGB(1.0, 0.0, 0.0), label = "LANDMARK, fluid, δ = 1.0 mm")
plot!(p, landmark_hybrid, ls = :dash, lc = RGB(0.0, 0.0, 1.0), label = "LANDMARK, Hybrid-PIC")
return p
end
p = compare_to_landmark(sol_case1, 1); display(p)
This looks quite different than the benchmark. However, Hall thrusters are strongly oscillatory devices, and we only plotted the last frame. Let's look instead at the time averaged behavior. For this, we use the time_average
function, which returns another Solution
object, and plot that against the benchmark.
avg_case1 = time_average(sol_case1, 500) # average over last 500 saved frames of the simulation
p = compare_to_landmark(avg_case1, 1); display(p)
This looks better. There are discrepancies due to minor differences in physics model and choice of numerics, but overall the agreement is good. We can also check the other LANDMARK cases. Below is the time-averaged behavior of case 2:
sol_case2 = run_landmark(1e-3; ncells=150, nsave=1000, case = 2)
avg_case2 = time_average(sol_case2)
p = compare_to_landmark(avg_case2, 2); display(p)
Simulation exited at t = 0.001 with retcode :success in 11.68718 seconds. 11.689037 seconds (47.95 k allocations: 46.702 MiB)
And case 3:
sol_case3 = run_landmark(1e-3; ncells=150, nsave=1000, case = 3)
avg_case3 = time_average(sol_case3)
p = compare_to_landmark(avg_case3, 3); display(p)
Simulation exited at t = 0.001 with retcode :success in 11.7090664 seconds. 11.711134 seconds (47.95 k allocations: 46.702 MiB, 0.12% gc time)
Here we will show how to extract thrust, current and other useful quantities from a simulation. The discharge current is computed during each timestep of a simulation, and we can extract and plot it as follows
function plot_current(sol)
(;A_ch, index, config) = sol.params
e = HallThruster.e
mi = config.propellant.m
t_ms = sol.t * 1000 # convert from seconds to milliseconds
N = length(t_ms)
current = HallThruster.discharge_current(sol)
ion_current = HallThruster.ion_current(sol)
electron_current = HallThruster.electron_current(sol)
p = plot(t_ms, current, label = "Discharge current", xlabel = "Time (ms)", ylabel = "Current (A)", linewidth = 1.5, ylims = (-10, 25))
plot!(p, t_ms, ion_current, label = "Ion current (cathode)")
plot!(p, t_ms, electron_current, label = "Electron current (cathode)")
return p
end
p = plot_current(sol_case1); display(p)
As suspected, this case was quite oscillatory. We can see both transit-time oscillations (O(100 kHz)) in addition to the normal O(10 kHz) breathing mode oscillations, with intense transient spikes in both ion and electron currents.
p = plot_current(sol_case2); display(p)
This case is more quiescent---we see small breathing oscillations with no transit time oscillations. Additionally, the oscillations seem to damp out as the simulation progresses
p = plot_current(sol_case3); display(p)
In case 3, we find large coherent breathing oscillations emerge, again with no transit time oscillations.
We can also compute the thrust (in Newtons) as a function of time.
thrust_mN = HallThruster.thrust(sol_case3) .* 1000
p = plot(sol_case3.t .* 1000, thrust_mN, xlabel = "Time (ms)", ylabel = "Thrust (mN)", title = "Case 3 thrust", label = "");
display(p)
Solution
object¶Let us take a closer look at the Solution
object and how to extract data. HallThruster.jl provides a convienient interface to access plasma parameters from the solution object. If we index the Solution
by a symbol, HallThruster will pull out a Vector
of Vector
s containing the evolution of that variable over time. To access the neutral density at the last timestep and the z position of the corresponding values we would type
nn_end = sol_case2[:nn][end]
z = sol_case2.params.z_cell
p = plot(z, nn_end, xlabel = "z (m)", ylabel = "Number Density (m^-3)", title = "Neutral density, case 2", label = "");
display(p)
Similarly, we can get the ion velocity profile at the 444th saved frame:
frame = 444
ui = sol_case2[:ui][frame]
p = plot(z, ui, xlabel = "z (m)", ylabel = "Velocity (m/s)", label = "", title = "Ion velocity, frame $(frame), case 2");
display(p)
If we have multiple charge states, we would need to index by [:ui, 1]. The same goes for ion number density. To get the density of the 3rd charge state we would index by [:ni, 3].
To get the potential, we index by :ϕ
.
ϕ = sol_case2[:ϕ][end]
p = plot(z, ϕ, xlabel = "z (m)", ylabel = "Potential (V)", label = "", title = "Potential, case 2.");
display(p)
Calling HallThruster.saved_fields()
gives us a list of the variables that we can access through this interface.
HallThruster.saved_fields()
(:μ, :Tev, :ϕ, :∇ϕ, :ne, :pe, :ue, :∇pe, :νan, :νc, :νen, :νei, :radial_loss_frequency, :νew_momentum, :νiz, :νex, :νe, :Id, :ji, :nn, :anom_multiplier, :ohmic_heating, :wall_losses, :inelastic_losses, :Vs, :channel_area, :inner_radius, :outer_radius, :dA_dz, :tanδ, :anom_variables, :dt, :ni, :ui, :niui)
We can also inspect parameters that the simulation was run with and do not change over time. These are accesssible in sol.params
.
These are:
ncells,
ncharge,
config,
ϕ_L,
ϕ_R,
Te_L,
Te_R,
L_ch,
A_ch,
z_cell,
z_edge,
dt,
dtmin,
dtmax,
index, cache, fluids, fluid_ranges, species_range_dict,
iteration,
ionization_reactions,
ionization_reactant_indices,
ionization_product_indices,
excitation_reactions,
excitation_reactant_indices,
electron_neutral_collisions
Where config
contains even more parameters and settings. You are referred to the Configuration page of the documentation for a detailed description.
As an example, let us extract the anomalous collision frequency model from the Landmark case 1 simulation.
anom_model = sol_case1.params.config.anom_model
HallThruster.TwoZoneBohm((0.00625, 0.0625))
This returns a TwoZoneBohm
object, which defines the anomalous collision frequency used in the Landmark cases. Anomalous collision frequency models in HallThruster.jl are explained here.
Let us plot the anomalous collision frequency vs axial position.
νan = zeros(length(z))
# Note, anom models in HallThruster.jl modify their first argument νan for speed. For convenience, they also return the modified array.
anom_model(νan, sol_case2.params)
152-element Vector{Float64}: 1.246132923765344e6 1.2896431762688495e6 1.380323403861489e6 1.4760237228820696e6 1.5769104178437886e6 1.6831464518877408e6 1.7948906214599628e6 1.912296673403549e6 2.035512386956471e6 2.1646786235450073e6 ⋮ 7.549185851167956e7 7.375178514226599e7 7.202711524937049e7 7.031865749573272e7 6.86271850212136e7 6.695343523763771e7 6.529810968904467e7 6.366187397605952e7 6.285111297539646e7
p = let p = plot(z, νan;
yaxis = :log, label = "Anomalous collision frequency", ylabel = "Frequency (Hz)", xlabel = "z (m)"
)
plot!(p, z, sol_case2[:ωce][end], label = "Cyclotron frequency", legend = :outertop)
end
display(p)
If you want to use other numerical fluxes, change the physics through collisions, boundary conditions, wall sheath losses, define new thrusters and change the domain length, use another propellant or provide your own source terms and ionization models, check out the Configuration section of the official Documentation