|
| 1 | +Linear Shallow Water Equations on an Extruded Mesh using a Strang timestepping scheme |
| 2 | +===================================================================================== |
| 3 | + |
| 4 | +This demo solves the linear shallow water equations on an extruded mesh |
| 5 | +using a Strang timestepping scheme. |
| 6 | + |
| 7 | +The equations are solved in a domain :math:`\Omega` utilizing a 2D base mesh |
| 8 | +that is extruded vertically to form a 3D volume. |
| 9 | + |
| 10 | +As usual, we start by importing Firedrake: :: |
| 11 | + |
| 12 | + from firedrake import * |
| 13 | + |
| 14 | +Mesh Generation |
| 15 | +---------------- |
| 16 | + |
| 17 | +We use an *extruded* mesh, where the base mesh is a :math:`2^5 \times 2^5` unit square |
| 18 | +with 5 evenly-spaced vertical layers. This results in a 3D volume composed of |
| 19 | +prisms. :: |
| 20 | + |
| 21 | + power = 5 |
| 22 | + m = UnitSquareMesh(2 ** power, 2 ** power) |
| 23 | + layers = 5 |
| 24 | + |
| 25 | + mesh = ExtrudedMesh(m, layers, layer_height=0.25) |
| 26 | + |
| 27 | +Function Spaces |
| 28 | +---------------- |
| 29 | +For the velocity field, we use an :math:`H(\mathrm{div})`-conforming function space |
| 30 | +constructed as the outer product of a 2D BDM space and a 1D DG space. This ensures |
| 31 | +that the normal component of the velocity is continuous across element boundaries |
| 32 | +in the horizontal directions, which is important for accurately capturing fluxes. :: |
| 33 | + |
| 34 | + horiz = FiniteElement("BDM", "triangle", 1) |
| 35 | + vert = FiniteElement("DG", "interval", 0) |
| 36 | + prod = HDiv(TensorProductElement(horiz, vert)) |
| 37 | + W = FunctionSpace(mesh, prod) |
| 38 | + |
| 39 | +We also define a pressure space :math:`X` using piecewise constant discontinuous |
| 40 | +Galerkin elements, and a plotting space :math:`X_{\text{plot}}` using continuous |
| 41 | +Galerkin elements for better visualization. :: |
| 42 | + |
| 43 | + X = FunctionSpace(mesh, "DG", 0, vfamily="DG", vdegree=0) |
| 44 | + Xplot = FunctionSpace(mesh, "CG", 1, vfamily="Lagrange", vdegree=1) |
| 45 | + |
| 46 | +Initial Conditions |
| 47 | +------------------- |
| 48 | + |
| 49 | +We define our functions for velocity and pressure fields. The initial pressure field |
| 50 | +is set to a prescribed sine function to create a wave-like disturbance. :: |
| 51 | + |
| 52 | + # Define starting field |
| 53 | + u_0 = Function(W) |
| 54 | + u_h = Function(W) |
| 55 | + u_1 = Function(W) |
| 56 | + p_0 = Function(X) |
| 57 | + p_1 = Function(X) |
| 58 | + p_plot = Function(Xplot) |
| 59 | + x, y, z = SpatialCoordinate(mesh) |
| 60 | + p_0.interpolate(sin(4*pi*x)*sin(2*pi*x)) |
| 61 | + |
| 62 | + T = 0.5 |
| 63 | + t = 0 |
| 64 | + dt = 0.0025 |
| 65 | + |
| 66 | + file = VTKFile("lsw3d.pvd") |
| 67 | + |
| 68 | +Before starting the time-stepping loop, we project the initial pressure field |
| 69 | +into the plotting space for visualization. :: |
| 70 | + |
| 71 | + p_trial = TrialFunction(Xplot) |
| 72 | + p_test = TestFunction(Xplot) |
| 73 | + solve(p_trial * p_test * dx == p_0 * p_test * dx, p_plot) |
| 74 | + file.write(p_plot, time=t) |
| 75 | + |
| 76 | + E_0 = assemble(0.5 * p_0 * p_0 * dx + 0.5 * dot(u_0, u_0) * dx) |
| 77 | + |
| 78 | +Time-Stepping Loop |
| 79 | +-------------------- |
| 80 | + |
| 81 | +We evolve the system in time using a Strang splitting method. In each time step, |
| 82 | +we perform a half-step update of the velocity field, a full-step update of the pressure field, |
| 83 | +and then another half-step update of the velocity field. This approach helps to |
| 84 | +maintain stability and accuracy. |
| 85 | + |
| 86 | +**Step 1: Velocity Half-Step** |
| 87 | +First, we solve for an intermediate velocity :math:`u_h` using the pressure |
| 88 | +from the start of the step :math:`p_0`. Mathematically, we find :math:`u_h \in W` such that: |
| 89 | + |
| 90 | +.. math:: |
| 91 | +
|
| 92 | + \int_{\Omega} w \cdot u_h \, dx = \int_{\Omega} w \cdot u_0 \, dx + \frac{\Delta t}{2} \int_{\Omega} (\nabla \cdot w) p_0 \, dx \quad \forall w \in W |
| 93 | +
|
| 94 | +.. code-block:: python |
| 95 | +
|
| 96 | + a_1 = dot(w, u) * dx |
| 97 | + L_1 = dot(w, u_0) * dx + 0.5 * dt * div(w) * p_0 * dx |
| 98 | + solve(a_1 == L_1, u_h) |
| 99 | +
|
| 100 | +**Step 2: Pressure Full-Step** |
| 101 | +Next, we update the pressure to :math:`p_1` using the divergence of the |
| 102 | +intermediate velocity :math:`u_h`. We find :math:`p_1 \in X` such that: |
| 103 | + |
| 104 | +.. math:: |
| 105 | +
|
| 106 | + \int_{\Omega} \phi \, p_1 \, dx = \int_{\Omega} \phi \, p_0 \, dx - \Delta t \int_{\Omega} \phi (\nabla \cdot u_h) \, dx \quad \forall \phi \in X |
| 107 | +
|
| 108 | +.. code-block:: python |
| 109 | +
|
| 110 | + a_2 = phi * p * dx |
| 111 | + L_2 = phi * p_0 * dx - dt * phi * div(u_h) * dx |
| 112 | + solve(a_2 == L_2, p_1) |
| 113 | +
|
| 114 | +**Step 3: Velocity Final Half-Step** |
| 115 | +Finally, we compute the velocity at the end of the time step :math:`u_1` using |
| 116 | +the updated pressure :math:`p_1`. We find :math:`u_1 \in W` such that: |
| 117 | + |
| 118 | +.. math:: |
| 119 | +
|
| 120 | + \int_{\Omega} w \cdot u_1 \, dx = \int_{\Omega} w \cdot u_h \, dx + \frac{\Delta t}{2} \int_{\Omega} (\nabla \cdot w) p_1 \, dx \quad \forall w \in W |
| 121 | +
|
| 122 | +.. code-block:: python |
| 123 | +
|
| 124 | + a_3 = dot(w, u) * dx |
| 125 | + L_3 = dot(w, u_h) * dx + 0.5 * dt * div(w) * p_1 * dx |
| 126 | + solve(a_3 == L_3, u_1) |
| 127 | +
|
| 128 | +Implementation in the Simulation Loop |
| 129 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 130 | + |
| 131 | +Here follows the complete implementation of the time-stepping loop, including the updates of the velocity and pressure fields, |
| 132 | +as well as the projection of the pressure field for visualization at each time step. |
| 133 | +We also print the current simulation time at each step for tracking progress. |
| 134 | + |
| 135 | +.. code-block:: python |
| 136 | +
|
| 137 | + while t < T: |
| 138 | + u = TrialFunction(W) |
| 139 | + w = TestFunction(W) |
| 140 | + a_1 = dot(w, u) * dx |
| 141 | + L_1 = dot(w, u_0) * dx + 0.5 * dt * div(w) * p_0 * dx |
| 142 | + solve(a_1 == L_1, u_h) |
| 143 | +
|
| 144 | + p = TrialFunction(X) |
| 145 | + phi = TestFunction(X) |
| 146 | + a_2 = phi * p * dx |
| 147 | + L_2 = phi * p_0 * dx - dt * phi * div(u_h) * dx |
| 148 | + solve(a_2 == L_2, p_1) |
| 149 | +
|
| 150 | + u = TrialFunction(W) |
| 151 | + w = TestFunction(W) |
| 152 | + a_3 = dot(w, u) * dx |
| 153 | + L_3 = dot(w, u_h) * dx + 0.5 * dt * div(w) * p_1 * dx |
| 154 | + solve(a_3 == L_3, u_1) |
| 155 | +
|
| 156 | + u_0.assign(u_1) |
| 157 | + p_0.assign(p_1) |
| 158 | + t += dt |
| 159 | +
|
| 160 | + # project into P1 x P1 for plotting |
| 161 | + p_trial = TrialFunction(Xplot) |
| 162 | + p_test = TestFunction(Xplot) |
| 163 | + solve(p_trial * p_test * dx == p_0 * p_test * dx, p_plot) |
| 164 | + file.write(p_plot, time=t) |
| 165 | + print(t) |
| 166 | +
|
| 167 | +Energy Calculation |
| 168 | +------------------ |
| 169 | + |
| 170 | +Finally, we compute and print the total energy of the system at the end of the simulation |
| 171 | +and compare it to the initial energy to assess conservation properties. :: |
| 172 | + |
| 173 | + E_1 = assemble(0.5 * p_0 * p_0 * dx + 0.5 * dot(u_0, u_0) * dx) |
| 174 | + print('Initial energy', E_0) |
| 175 | + print('Final energy', E_1) |
| 176 | + |
| 177 | +This demo can be found as a script in |
| 178 | +:demo:`extruded_shallow_water.py <extruded_shallow_water.py>`. |
0 commit comments