SRMHD Linear Eigenmode Convergence
This example verifies the SRMHD solver's ability to propagate all four MHD wave families: fast magnetosonic, slow magnetosonic, Alfvén, and entropy waves. Each eigenmode is initialised as a small perturbation and evolved; self-convergence (comparing N vs 2N solutions) demonstrates that the numerical error decreases under mesh refinement.
Mathematical Setup
The 1D SRMHD equations linearised about a uniform background state admit four wave families propagating at their characteristic speeds. We test each by perturbing the density/velocity/magnetic field along the corresponding eigenvector direction.
Reference
- Anile, A.M. (1989). Relativistic Fluids and Magneto-Fluids. Cambridge University Press.
- Balsara, D.S. (2001). Total Variation Diminishing Scheme for Adiabatic and Isothermal MHD. J. Comput. Phys., 174, 614-648.
using FiniteVolumeMethod
using StaticArrays
using CairoMakie
gamma = 5.0 / 3.0
eos = IdealGasEOS(gamma)
law = SRMHDEquations{1}(eos)SRMHDEquations{1, IdealGasEOS{Float64}}(IdealGasEOS{Float64}(1.6666666666666667), 1.0e-12, 50)Background State
Uniform background with moderate magnetic field.
rho0 = 1.0
P0 = 1.0
Bx0 = 1.0
amp = 1.0e-4 # small perturbation amplitude0.0001Eigenmode Definitions
Each mode perturbs different variables to excite the corresponding wave family.
eigenmodes = [
(
name = "Fast magnetosonic",
ic = (x -> SVector(rho0 + amp * sin(2 * pi * x), 0.0, 0.0, 0.0, P0 + amp * gamma * P0 / rho0 * sin(2 * pi * x), Bx0, 0.0, 0.0)),
var_idx = 1, # measure density
var_name = "ρ",
),
(
name = "Slow magnetosonic",
ic = (x -> SVector(rho0, 0.0, 0.0, 0.0, P0, Bx0, amp * sin(2 * pi * x), 0.0)),
var_idx = 7, # measure By
var_name = "By",
),
(
name = "Alfvén",
ic = (x -> SVector(rho0, 0.0, 0.0, amp * sin(2 * pi * x), P0, Bx0, 0.0, amp * sin(2 * pi * x))),
var_idx = 8, # measure Bz
var_name = "Bz",
),
(
name = "Entropy",
ic = (x -> SVector(rho0 + amp * sin(2 * pi * x), 0.0, 0.0, 0.0, P0, Bx0, 0.0, 0.0)),
var_idx = 1, # measure density
var_name = "ρ",
),
]4-element Vector{NamedTuple{(:name, :ic, :var_idx, :var_name)}}:
(name = "Fast magnetosonic", ic = Main.var"#5#6"(), var_idx = 1, var_name = "ρ")
(name = "Slow magnetosonic", ic = Main.var"#7#8"(), var_idx = 7, var_name = "By")
(name = "Alfvén", ic = Main.var"#9#10"(), var_idx = 8, var_name = "Bz")
(name = "Entropy", ic = Main.var"#11#12"(), var_idx = 1, var_name = "ρ")Self-Convergence Study
We use self-convergence: run at resolutions N and 2N, then compare the solutions on the coarse grid. The difference decreases under refinement.
t_final = 0.5
resolutions = [32, 64, 128]
all_errors = Dict{String, Vector{Float64}}()
all_rates = Dict{String, Vector{Float64}}()
function convergence_rates(errs)
return [log2(errs[i] / errs[i + 1]) for i in 1:(length(errs) - 1)]
end
function run_mode(law, mode, N, t_final)
mesh = StructuredMesh1D(0.0, 1.0, N)
prob = HyperbolicProblem(
law, mesh, HLLSolver(), CellCenteredMUSCL(MinmodLimiter()),
PeriodicHyperbolicBC(), PeriodicHyperbolicBC(), mode.ic;
final_time = t_final, cfl = 0.3,
)
x, U, t_end = solve_hyperbolic(prob)
w = [conserved_to_primitive(law, U[i])[mode.var_idx] for i in eachindex(x)]
return x, w, t_end
end
for mode in eigenmodes
errors = Float64[]
for N in resolutions
# Run at N and 2N
x_c, w_c, _ = run_mode(law, mode, N, t_final)
_, w_f, _ = run_mode(law, mode, 2 * N, t_final)
# Restrict fine solution to coarse grid (average pairs)
w_f_coarse = [(w_f[2 * i - 1] + w_f[2 * i]) / 2 for i in 1:N]
# L1 difference
err = sum(abs(w_c[i] - w_f_coarse[i]) for i in 1:N) / N
push!(errors, err)
end
all_errors[mode.name] = errors
all_rates[mode.name] = convergence_rates(errors)
endVisualisation — Convergence Comparison
fig = Figure(fontsize = 22, size = (800, 600))
ax = Axis(
fig[1, 1], xlabel = "N", ylabel = L"L^1 \text{ self-convergence error}",
xscale = log2, yscale = log10,
title = "SRMHD Eigenmode Self-Convergence",
)
colors = [:blue, :red, :green, :orange]
markers = [:circle, :utriangle, :diamond, :rect]
for (i, mode) in enumerate(eigenmodes)
scatterlines!(
ax, resolutions, all_errors[mode.name],
color = colors[i], marker = markers[i],
linewidth = 2, markersize = 10,
label = mode.name,
)
end
# Reference slope
e_ref = all_errors[eigenmodes[1].name][1]
N_ref = resolutions[1]
lines!(
ax, resolutions, e_ref .* (N_ref ./ resolutions) .^ 1,
color = :black, linestyle = :dash, linewidth = 1, label = L"O(N^{-1})",
)
axislegend(ax, position = :lb)
resize_to_layout!(fig)
fig
if isdefined(@__MODULE__, :evidence_artifact_path)
save(evidence_artifact_path("srmhd_eigenmode_convergence.png"), fig)
endTest Assertions
Each eigenmode should show self-convergence at rate > 0.5 with MUSCL+HLL.
for mode in eigenmodes
rates = all_rates[mode.name]
end
if isdefined(@__MODULE__, :record_evidence_result)
record_evidence_result(
metrics = Dict(
"mode_errors" => all_errors,
"mode_rates" => all_rates,
"minimum_mode_rate" => minimum(vcat(values(all_rates)...)),
),
artifacts = ["srmhd_eigenmode_convergence.png"],
notes = [
"Relativistic benchmark sweep across fast, slow, Alfven, and entropy SRMHD eigenmodes.",
"This evidence entry is the relativistic benchmark-stage eigenmode propagation case.",
],
summary = Dict(
"resolutions" => resolutions,
"mode_errors" => all_errors,
"mode_rates" => all_rates,
"mode_names" => [mode.name for mode in eigenmodes],
),
)
endJust the code
An uncommented version of this example is given below. You can view the source code for this file here.
using FiniteVolumeMethod
using StaticArrays
using CairoMakie
gamma = 5.0 / 3.0
eos = IdealGasEOS(gamma)
law = SRMHDEquations{1}(eos)
rho0 = 1.0
P0 = 1.0
Bx0 = 1.0
amp = 1.0e-4 # small perturbation amplitude
eigenmodes = [
(
name = "Fast magnetosonic",
ic = (x -> SVector(rho0 + amp * sin(2 * pi * x), 0.0, 0.0, 0.0, P0 + amp * gamma * P0 / rho0 * sin(2 * pi * x), Bx0, 0.0, 0.0)),
var_idx = 1, # measure density
var_name = "ρ",
),
(
name = "Slow magnetosonic",
ic = (x -> SVector(rho0, 0.0, 0.0, 0.0, P0, Bx0, amp * sin(2 * pi * x), 0.0)),
var_idx = 7, # measure By
var_name = "By",
),
(
name = "Alfvén",
ic = (x -> SVector(rho0, 0.0, 0.0, amp * sin(2 * pi * x), P0, Bx0, 0.0, amp * sin(2 * pi * x))),
var_idx = 8, # measure Bz
var_name = "Bz",
),
(
name = "Entropy",
ic = (x -> SVector(rho0 + amp * sin(2 * pi * x), 0.0, 0.0, 0.0, P0, Bx0, 0.0, 0.0)),
var_idx = 1, # measure density
var_name = "ρ",
),
]
t_final = 0.5
resolutions = [32, 64, 128]
all_errors = Dict{String, Vector{Float64}}()
all_rates = Dict{String, Vector{Float64}}()
function convergence_rates(errs)
return [log2(errs[i] / errs[i + 1]) for i in 1:(length(errs) - 1)]
end
function run_mode(law, mode, N, t_final)
mesh = StructuredMesh1D(0.0, 1.0, N)
prob = HyperbolicProblem(
law, mesh, HLLSolver(), CellCenteredMUSCL(MinmodLimiter()),
PeriodicHyperbolicBC(), PeriodicHyperbolicBC(), mode.ic;
final_time = t_final, cfl = 0.3,
)
x, U, t_end = solve_hyperbolic(prob)
w = [conserved_to_primitive(law, U[i])[mode.var_idx] for i in eachindex(x)]
return x, w, t_end
end
for mode in eigenmodes
errors = Float64[]
for N in resolutions
# Run at N and 2N
x_c, w_c, _ = run_mode(law, mode, N, t_final)
_, w_f, _ = run_mode(law, mode, 2 * N, t_final)
# Restrict fine solution to coarse grid (average pairs)
w_f_coarse = [(w_f[2 * i - 1] + w_f[2 * i]) / 2 for i in 1:N]
# L1 difference
err = sum(abs(w_c[i] - w_f_coarse[i]) for i in 1:N) / N
push!(errors, err)
end
all_errors[mode.name] = errors
all_rates[mode.name] = convergence_rates(errors)
end
fig = Figure(fontsize = 22, size = (800, 600))
ax = Axis(
fig[1, 1], xlabel = "N", ylabel = L"L^1 \text{ self-convergence error}",
xscale = log2, yscale = log10,
title = "SRMHD Eigenmode Self-Convergence",
)
colors = [:blue, :red, :green, :orange]
markers = [:circle, :utriangle, :diamond, :rect]
for (i, mode) in enumerate(eigenmodes)
scatterlines!(
ax, resolutions, all_errors[mode.name],
color = colors[i], marker = markers[i],
linewidth = 2, markersize = 10,
label = mode.name,
)
end
# Reference slope
e_ref = all_errors[eigenmodes[1].name][1]
N_ref = resolutions[1]
lines!(
ax, resolutions, e_ref .* (N_ref ./ resolutions) .^ 1,
color = :black, linestyle = :dash, linewidth = 1, label = L"O(N^{-1})",
)
axislegend(ax, position = :lb)
resize_to_layout!(fig)
fig
if isdefined(@__MODULE__, :evidence_artifact_path)
save(evidence_artifact_path("srmhd_eigenmode_convergence.png"), fig)
end
for mode in eigenmodes
rates = all_rates[mode.name]
end
if isdefined(@__MODULE__, :record_evidence_result)
record_evidence_result(
metrics = Dict(
"mode_errors" => all_errors,
"mode_rates" => all_rates,
"minimum_mode_rate" => minimum(vcat(values(all_rates)...)),
),
artifacts = ["srmhd_eigenmode_convergence.png"],
notes = [
"Relativistic benchmark sweep across fast, slow, Alfven, and entropy SRMHD eigenmodes.",
"This evidence entry is the relativistic benchmark-stage eigenmode propagation case.",
],
summary = Dict(
"resolutions" => resolutions,
"mode_errors" => all_errors,
"mode_rates" => all_rates,
"mode_names" => [mode.name for mode in eigenmodes],
),
)
endThis page was generated using Literate.jl.