Steady-State Euler Equations

CombinatorialSpaces provides meshes and discrete operators amenable for solving many types of physics and multi-physics problems. For example, CombinatorialSpaces powers the Decapodes library provides a DSL for generating initial-value problem simulations, such as co-rotating vortices on a sphere governed by Navier-Stokes.

On this page, we will use CombinatorialSpaces directly to solve a steady-state problem.

Euler Equations

The Euler equations are a concise model of fluid flow, but this model still demonstrates some interesting differential operators:

\[\frac{\partial \textbf{u}^\flat}{\partial t} + \pounds_u \textbf{u}^\flat - \frac{1}{2} \textbf{d}(\textbf{u}^\flat(\textbf{u})) = - \frac{1}{\rho} \textbf{d} p + \textbf{b}^\flat.\]

See Marsden, Ratiu, and Abraham's "Manifolds, Tensor Analysis, and Applications" for an overview in the exterior calculus.

Here, we see an exterior derivative, d, a Lie derivative, , and an interior product, interior_product.

Discretizing

Let's examine some particular cases of these equations. For both, we need a mesh and some discrete differential operators.

using CairoMakie, CombinatorialSpaces, StaticArrays
using CombinatorialSpaces.DiscreteExteriorCalculus: eval_constant_primal_form
using GeometryBasics: Point3d
using LinearAlgebra: norm

s = triangulated_grid(100,100,5,5,Point3d);
sd = EmbeddedDeltaDualComplex2D{Bool,Float64,Point3d}(s);
subdivide_duals!(sd, Barycenter());

f = Figure()
ax = CairoMakie.Axis(f[1,1])
wireframe!(ax, s)

f
Example block output

Now that we have our mesh, let's allocate our discrete differential operators:

d0 = dec_dual_derivative(0, sd)
d1 = dec_differential(1, sd);
s1 = dec_hodge_star(1, sd);
s2 = dec_hodge_star(2, sd);
ι1 = interior_product_dd(Tuple{1,1}, sd)
ι2 = interior_product_dd(Tuple{1,2}, sd)
ℒ1 = ℒ_dd(Tuple{1,1}, sd);
#36 (generic function with 1 method)

First Case

In this first case, we will explicitly provide initial values for u. We will solve for pressure and the time derivative of u and check that they are what we expect. Note that we will set the mass budget, b, to 0.

Let's provide a flow field of unit magnitude, static throughout the domain. We want to store this as a 1-form. We can create a 1-form by "flattening" a vector field, performing many line integrals to store values on the edges of the mesh. Since we want to store our flow as a "dual" 1-form (on the edges of the dual mesh), we can use the Hodge star operator to convert from a primal 1-form to a dual 1-form. Since the values of a 1-form can be unintuitive, we will "sharpen" the 1-form back to a vector field when visualizing.

X♯ = SVector{3,Float64}(1/√2,1/√2,0)
u = s1 * eval_constant_primal_form(sd, X♯)

plot_dvf(sd, u, title="Flow")
Example block output

Let's look at the self-advection term, in which we take the lie derivative of u along itself, and subtract half of the gradient of its inner product. (See Marsden, Ratiu, and Abraham for a derivation.) Recall that our flow u is static throughout the domain, so we should expect this term to be 0 throughout the interior of the domain, where it is not affected by boundary conditions.

The Lie derivative encodes how a differential form changes along a vector field. For our case of many parallel streamlines, and in which the magnitude is identical everywhere, we expect such a quantity to be 0. However, when discretizing, we have to make some assumptions about what is happening "outside" of the domain, and these assumptions have implications on the data stored on the boundary of the domain. In our discretization, we assume the flow outside the domain is 0. Thus, our Lie derivative along the boundary points inward:

lie_u_u = ℒ1(u,u)

plot_dvf(sd, lie_u_u, title="Lie Derivative of Flow with Itself")
Example block output
selfadv = ℒ1(u,u) - 0.5*d0*ι1(u,u)

plot_dvf(sd, selfadv, title="Self-Advection")
Example block output

Now, let's solve for pressure. We can set up a Poisson problem on the divergence of the self-advection term we computed. Recall that divergence can be computed as $\star d \star$, and the Laplacian as $d \star d \star$. To solve a Poisson problem, we reverse the order of the operations, and take advantage of the fact that solving the inverse hodge star is equivalent to multiplying by the hodge star.

div(x) = s2 * d1 * (s1 \ x);
solveΔ(x) = float.(d0) \ (s1 * (float.(d1) \ (s2 \ x)))

p = (solveΔ ∘ div)(selfadv)

plot_dual0form(sd, p, title="Pressure")
Example block output

We see that we have a nonzero pressure of exactly 2 across the interior of the domain.

dp = d0*p

plot_dvf(sd, dp, title="Pressure Gradient")
Example block output

Based on our initial conditions and the way that we computed pressure, we expect that the time derivative of u should be 0 on the interior of the domain, where it is not affected by boundary conditions.

∂ₜu = -selfadv - dp;

plot_dvf(sd, ∂ₜu, title="Time Derivative")
Example block output

We see that we do indeed find zero-vectors throughout the interior of the domain as expected.

Second Case

For this second case, we will specify that the time derivative of u is 0. We will assume a constant pressure, and then analyze the steady-states of u. We will again ignore any mass budget, b, and recall the gradient of a constant function (here, pressure) is 0. Recall our formula:

\[\frac{\partial \textbf{u}^\flat}{\partial t} + \pounds_u \textbf{u}^\flat - \frac{1}{2} \textbf{d}(\textbf{u}^\flat(\textbf{u})) = - \frac{1}{\rho} \textbf{d} p + \textbf{b}^\flat.\]

Setting appropriate terms to 0, we have:

\[\pounds_u \textbf{u}^\flat - \frac{1}{2} \textbf{d}(\textbf{u}^\flat(\textbf{u})) = 0.\]

We already allocated our discrete differential operators. Let us solve.

using NLsolve

steady_flow(u) = ℒ1(u,u) - 0.5*d0*ι1(u,u)

starting_state = s1 * eval_constant_primal_form(sd, X♯)
sol = nlsolve(steady_flow, starting_state)

plot_dvf(sd, sol.zero, title="Steady State")
Example block output

We note that this steady flow of all zero-vectors does indeed satisfy the constraints that we set.

Third Case

For this third case, we will again solve for u. However, we will set a Gaussian bubble of pressure at the center of the domain, and use Euler's method to solve Euler's equations.

\[\frac{\partial \textbf{u}^\flat}{\partial t} = - \pounds_u \textbf{u}^\flat + \frac{1}{2} \textbf{d}(\textbf{u}^\flat(\textbf{u})) - \frac{1}{\rho} \textbf{d} p.\]

Case 3.1: Euler's method

center = [50.0, 50.0, 0.0]
gauss(pnt) = 2 + 50/(√(2*π*10))*ℯ^(-(norm(center-pnt)^2)/(2*10))
p = gauss.(sd[sd[:tri_center], :dual_point])

u = s1 * eval_constant_primal_form(sd, X♯)
du = copy(u)

function euler_equation!(du,u,p)
  du .= - ℒ1(u,u) + 0.5*d0*ι1(u,u) - d0*p
end

dt = 1e-3
function eulers_method()
  for _ in 0:dt:1
    euler_equation!(du,u,p)
    u .+= du * dt
  end
  u
end

eulers_method()

plot_dvf(sd, u, title="Flow")
Example block output

Case 3.2: Euler's method with Projection

In Case 3.1, we solved Euler's equation directly using the method of lines. However, we assume that our flow, u, is incompressible. That is, $\delta u = 0$. In our finite updates, we did not check that the self-advection term is divergence free! One way to resolve this discrepancy is the "Projection method", and this is intimately related to the Hodge decomposition of the flow. (See the Wikipedia entry on the projection method, for example.) Let's employ this method here.

u = s1 * eval_constant_primal_form(sd, X♯)
du = copy(u)

dt = 1e-3
u_int = zeros(ne(sd))
p_next = zeros(ntriangles(sd))

function euler_equation_with_projection!(u)
  u_int .= u .+ (- ℒ1(u,u) + 0.5*d0*ι1(u,u))*dt
  p_next .= (solveΔ ∘ div)(u_int/dt)
  u .= u_int - dt*(d0*p_next)
end

function eulers_method()
  for _ in 0:dt:1
    euler_equation_with_projection!(u)
  end
  u
end

eulers_method()

plot_dvf(sd, u, title="Flow, with Projection Method")
Example block output