Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MHD Example #280

Merged
merged 4 commits into from
Dec 17, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 278 additions & 0 deletions examples/mhd.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
# The model in this file is copied from the stream function formulation introduced here:
# https://algebraicjulia.github.io/Decapodes.jl/dev/navier_stokes/ns/
# , but updated with terms representing β being advected by the fluid. The initial conditions
# setup code is copied outright.

# The main reference used for developing this model is:
# "Magnetohydrodynamics Simulation via Discrete Exterior Calculus", Gillespie, M.
# https://markjgillespie.com/Research/MHD/MHD_Simulation_with_DEC.pdf
# Note that the Gillespie paper does not use a stream function-vorticity formulation.

@info "Loading Dependencies"

#=
# Dependencies can be installed with the following command:
using Pkg
Pkg.add(["ACSets", "CairoMakie", "CombinatorialSpaces", "ComponentArrays",
"CoordRefSystems", "DiagrammaticEquations", "GeometryBasics", "JLD2",
"LinearAlgebra", "Logging", "LoggingExtras", "OrdinaryDiffEq", "SparseArrays",
"StaticArrays", "TerminalLoggers"])
=#

# Saving
using JLD2

# other dependencies
using MLStyle
using Statistics: mean

begin # Dependencies
# AlgebraicJulia
using ACSets
using CombinatorialSpaces
using Decapodes
using DiagrammaticEquations

# Meshing
using CoordRefSystems
using GeometryBasics: Point3
Point3D = Point3{Float64};

# Visualization
using CairoMakie

# Simulation
using ComponentArrays
using LinearAlgebra
using LinearAlgebra: factorize
using Logging: global_logger
using LoggingExtras
using OrdinaryDiffEq
using SparseArrays
using StaticArrays
using TerminalLoggers: TerminalLogger
global_logger(TerminalLogger())

# Saving
using JLD2

# other dependencies
using MLStyle
using Statistics: mean
end # Dependencies

@info "Defining models"
# Beta is out-of-plane:
mhd_out_of_plane = @decapode begin
ψ::Form0
η::DualForm1
(dη,β)::DualForm2
# δ = ⋆d⋆
∂ₜ(dη) == -1*(∘(⋆₁, dual_d₁)((⋆(dη) ∧₀₁ ♭♯(η)) + (⋆(β) ∧₀₁ ♭♯(∘(⋆, d, ⋆)(β)))))
∂ₜ(β) == -1*(∘(⋆₁, dual_d₁)(⋆(β) ∧₀₁ ♭♯(η)))
# solve for stream function
ψ == ∘(⋆, Δ⁻¹)(dη)
η == ⋆(d(ψ))
end;

# Beta lies in-plane:
mhd = @decapode begin
ψ::Form0
(η,β)::DualForm1
dη::DualForm2

∂ₜ(dη) == -1*(∘(⋆₁, dual_d₁)((⋆(dη) ∧₀₁ ♭♯(η)) + (♭♯(⋆(β)) ∧ᵈᵈ₁₀ ∘(⋆, d, ⋆)(β))))
∂ₜ(β) == -1*(∘(⋆₂, dual_d₀)(⋆(β) ∧ᵖᵈ₁₁ η))

ψ == ∘(⋆, Δ⁻¹)(dη)
η == ⋆(d(ψ))
end;

@info "Allocating Mesh and Operators"
const RADIUS = 1.0;
sphere = :ICO7;
s = @match sphere begin
:ICO5 => loadmesh(Icosphere(4, RADIUS));
:ICO6 => loadmesh(Icosphere(6, RADIUS));
:ICO7 => loadmesh(Icosphere(7, RADIUS));
:ICO8 => loadmesh(Icosphere(8, RADIUS));
:flat => triangulated_grid(10, 10, 0.2, 0.2, Point3D)
:UV => begin
s, _, _ = makeSphere(0, 180, 2.5, 0, 360, 2.5, RADIUS);
s;
end
end;
sd = EmbeddedDeltaDualComplex2D{Bool,Float64,Point3D}(s);
subdivide_duals!(sd, Circumcenter());

Δ0 = Δ(0,sd);
fΔ0 = factorize(Δ0);
d0 = dec_differential(0,sd);
d1 = dec_differential(1,sd);
dd0 = dec_dual_derivative(0,sd);
dd1 = dec_dual_derivative(1,sd);
δ1 = δ(1,sd);
s0 = dec_hodge_star(0,sd,GeometricHodge());
s1 = dec_hodge_star(1,sd,GeometricHodge());
s2 = dec_hodge_star(2, sd);
s0inv = dec_inv_hodge_star(0,sd,GeometricHodge());
♭♯_m = ♭♯_mat(sd);
@info " Differential operators allocated"

function generate(s, my_symbol; hodge=GeometricHodge())
op = @match my_symbol begin
:Δ⁻¹ => x -> begin
y = fΔ0 \ x
y .- minimum(y)
end
:♭♯ => x -> ♭♯_m * x
_ => default_dec_matrix_generate(s, my_symbol, hodge)
end
return (args...) -> op(args...)
end;

sim = gensim(mhd);
open("mhd_sim.jl", "w") do f
write(f, string(gensim(mhd)))
end
sim = include("mhd_sim.jl")
f = sim(sd, generate);

constants_and_parameters = (
μ = 0.001,)

begin # ICs
@info "Setting Initial Conditions"

""" function great_circle_dist(pnt,G,a,cntr)
Compute the length of the shortest path along a sphere, given Cartesian coordinates.
"""
function great_circle_dist(pnt1::Point3D, pnt2::Point3D)
RADIUS * acos(dot(pnt1,pnt2))
end

abstract type AbstractVortexParams end

struct TaylorVortexParams <: AbstractVortexParams
G::Real
a::Real
end

struct PointVortexParams <: AbstractVortexParams
τ::Real
a::Real
end

""" function taylor_vortex(pnt::Point3D, cntr::Point3D, p::TaylorVortexParams)
Compute the value of a Taylor vortex at the given point.
"""
function taylor_vortex(pnt::Point3D, cntr::Point3D, p::TaylorVortexParams)
gcd = great_circle_dist(pnt,cntr)
(p.G/p.a) * (2 - (gcd/p.a)^2) * exp(0.5 * (1 - (gcd/p.a)^2))
end

""" function point_vortex(pnt::Point3D, cntr::Point3D, p::PointVortexParams)
Compute the value of a smoothed point vortex at the given point.
"""
function point_vortex(pnt::Point3D, cntr::Point3D, p::PointVortexParams)
gcd = great_circle_dist(pnt,cntr)
p.τ / (cosh(3gcd/p.a)^2)
end

taylor_vortex(sd::HasDeltaSet, cntr::Point3D, p::TaylorVortexParams) =
map(x -> taylor_vortex(x, cntr, p), point(sd))
point_vortex(sd::HasDeltaSet, cntr::Point3D, p::PointVortexParams) =
map(x -> point_vortex(x, cntr, p), point(sd))

""" function ring_centers(lat, n)
Find n equispaced points at the given latitude.
"""
function ring_centers(lat, n)
ϕs = range(0.0, 2π; length=n+1)[1:n]
map(ϕs) do ϕ
v_sph = Spherical(RADIUS, lat, ϕ)
v_crt = convert(Cartesian, v_sph)
Point3D(v_crt.x.val, v_crt.y.val, v_crt.z.val)
end
end

""" function vort_ring(lat, n_vorts, p::T, formula) where {T<:AbstractVortexParams}
Compute vorticity as primal 0-forms for a ring of vortices.
Specify the latitude, number of vortices, and a formula for computing vortex strength centered at a point.
"""
function vort_ring(lat, n_vorts, p::T, formula) where {T<:AbstractVortexParams}
sum(map(x -> formula(sd, x, p), ring_centers(lat, n_vorts)))
end

""" function vort_ring(lat, n_vorts, p::PointVortexParams, formula)
Compute vorticity as primal 0-forms for a ring of vortices.
Specify the latitude, number of vortices, and a formula for computing vortex strength centered at a point.
Additionally, place a counter-balance vortex at the South Pole such that the integral of vorticity is 0.
"""
function vort_ring(lat, n_vorts, p::PointVortexParams, formula)
Xs = sum(map(x -> formula(sd, x, p), ring_centers(lat, n_vorts)))
Xsp = point_vortex(sd, Point3D(0.0, 0.0, -1.0), PointVortexParams(-1*n_vorts*p.τ, p.a))
Xs + Xsp
end

X = # Six equidistant points at latitude θ=0.4.
# "... an additional vortex, with strength τ=-18 and a radius a=0.15, is
# placed at the south pole (θ=π)."
vort_ring(0.4, 6, PointVortexParams(3.0, 0.15), point_vortex)

""" function solve_poisson(vort::VForm)
Compute the stream function by solving the Poisson equation.
"""
function solve_poisson(vort::VForm)
ψ = fΔ0 \ vort.data
ψ = ψ .- minimum(ψ)
end
solve_poisson(vort::DualForm{2}) =
solve_poisson(VForm(s0inv * vort.data))

ψ = solve_poisson(VForm(X))

# Compute velocity as curl (⋆d) of the stream function.
curl_stream(ψ) = s1 * d0 * ψ
divergence(u) = s2 * d1 * (s1 \ u)
RMS(x) = √(mean(x' * x))

integral_of_curl(curl::DualForm{2}) = sum(curl.data)
# Recall that s0 effectively multiplies each entry by a solid angle.
# i.e. (sum ∘ ⋆₀) computes a Riemann sum.
integral_of_curl(curl::VForm) = integral_of_curl(DualForm{2}(s0*curl.data))

u₀ = ComponentArray(dη = s0*X, β = zeros(ne(sd)))

constants_and_parameters = (
μ = 0.0,)

@info "RMS of divergence of initial velocity: $(∘(RMS, divergence, curl_stream)(ψ))"
@info "Integral of initial curl: $(integral_of_curl(VForm(X)))"
end # ICs

@info("Solving")
tₑ = 1.0;

prob = ODEProblem(f, u₀, (0, tₑ), constants_and_parameters)
soln = solve(prob,
Tsit5(),
dtmax = 1e-3,
dense=false,
progress=true, progress_steps=1);

function visualize_dynamics(file_name, soln)
time = Observable(0.0)
fig = Figure()
Label(fig[1, 1, Top()], @lift("...at $($time)"), padding = (0, 0, 5, 0))
ax = CairoMakie.Axis(fig[1,1])
msh = CairoMakie.mesh!(ax, s,
color=@lift(s0inv*soln($time).dη),
colormap=Reverse(:redsblues))
Colorbar(fig[1,2], msh)
record(fig, file_name, soln.t[1:10:end]; framerate = 10) do t
time[] = t
end
end
visualize_dynamics("mhd.mp4", soln)

Loading