Skip to content

Commit b02bd43

Browse files
authored
Improvements to Navier-Stokes demo (#99)
* Change from quads to triangles. Solve full problem. Next up: Speed of pressure evaluation * Switch to semi implicit time stepping scheme to improve CFL condition. Various improvements to plotting and meshing
1 parent 334c7fe commit b02bd43

File tree

3 files changed

+159
-131
lines changed

3 files changed

+159
-131
lines changed

.github/workflows/build-publish.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ jobs:
7474
mpirun -n 3 python3 convergence.py
7575
mpirun -n 3 python3 compiler_parameters.py
7676
77+
- name: Upload Navier-Stokes DFG 2D 3 plots
78+
uses: actions/upload-artifact@v3
79+
with:
80+
name: DFG2D-3
81+
path: chapter2/figures
82+
retention-days: 2
83+
if-no-files-found: error
84+
85+
7786
build-book:
7887
# The type of runner that the job will run on
7988
runs-on: ubuntu-latest

chapter2/ns_code2.ipynb

Lines changed: 96 additions & 80 deletions
Large diffs are not rendered by default.

chapter2/ns_code2.py

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
# To be able to solve this problem efficiently and ensure numerical stability, we will subsitute our first order backward difference scheme with a second order backward difference approximation, and use an explicit Adams-Bashforth approximation of the non-linear term.
2222
#
2323
# ```{admonition} Computationally demanding demo
24-
# This demo is computationally demanding, with a run-time up to 25 minutes, as it is using parameters from the DFG 2D-3 benchmark, which consists of 12800 time steps. It is adviced to download this demo and not run it in a browser. Please also see the last part of the tutorial on how to use `mpirun` to speedup the run-time of the program.
24+
# This demo is computationally demanding, with a run-time up to 15 minutes, as it is using parameters from the DFG 2D-3 benchmark, which consists of 12800 time steps. It is adviced to download this demo and not run it in a browser. This runtime of the demo can be increased by using 2 or 3 mpi processes.
2525
# ```
2626
#
2727
# The computational geometry we would like to use is
@@ -42,19 +42,22 @@
4242

4343
# +
4444
import gmsh
45+
import os
4546
import numpy as np
4647
import matplotlib.pyplot as plt
47-
import tqdm.notebook
48+
import tqdm.autonotebook
4849

4950
from mpi4py import MPI
5051
from petsc4py import PETSc
5152

5253
from dolfinx.cpp.mesh import to_type, cell_entity_type
53-
from dolfinx.fem import Constant, Function, FunctionSpace, assemble_scalar, dirichletbc, form, locate_dofs_topological, set_bc
54-
from dolfinx.fem.petsc import apply_lifting, assemble_matrix, assemble_vector, create_vector, set_bc
54+
from dolfinx.fem import (Constant, Function, FunctionSpace,
55+
assemble_scalar, dirichletbc, form, locate_dofs_topological, set_bc)
56+
from dolfinx.fem.petsc import (apply_lifting, assemble_matrix, assemble_vector,
57+
create_vector, create_matrix, set_bc)
5558
from dolfinx.graph import create_adjacencylist
5659
from dolfinx.geometry import BoundingBoxTree, compute_collisions, compute_colliding_cells
57-
from dolfinx.io import (XDMFFile, distribute_entity_data, gmshio)
60+
from dolfinx.io import (VTXWriter, distribute_entity_data, gmshio)
5861
from dolfinx.mesh import create_mesh, meshtags_from_entities
5962

6063
from ufl import (FacetNormal, FiniteElement, Identity, Measure, TestFunction, TrialFunction, VectorElement,
@@ -123,31 +126,30 @@
123126
# LcMin -o---------/
124127
# | | |
125128
# Point DistMin DistMax
126-
res_min = r / 3.7
127-
res_max = 1.5 * r
129+
res_min = r / 3
128130
if mesh_comm.rank == model_rank:
129-
gmsh.model.mesh.field.add("Distance", 1)
130-
gmsh.model.mesh.field.setNumbers(1, "EdgesList", obstacle)
131-
gmsh.model.mesh.field.add("Threshold", 2)
132-
gmsh.model.mesh.field.setNumber(2, "IField", 1)
133-
gmsh.model.mesh.field.setNumber(2, "LcMin", res_min)
134-
gmsh.model.mesh.field.setNumber(2, "LcMax", res_max)
135-
gmsh.model.mesh.field.setNumber(2, "DistMin", 4*r)
136-
gmsh.model.mesh.field.setNumber(2, "DistMax", 8*r)
137-
138-
# We take the minimum of the two fields as the mesh size
139-
gmsh.model.mesh.field.add("Min", 5)
140-
gmsh.model.mesh.field.setNumbers(5, "FieldsList", [2])
141-
gmsh.model.mesh.field.setAsBackgroundMesh(5)
131+
distance_field = gmsh.model.mesh.field.add("Distance")
132+
gmsh.model.mesh.field.setNumbers(distance_field, "EdgesList", obstacle)
133+
threshold_field = gmsh.model.mesh.field.add("Threshold")
134+
gmsh.model.mesh.field.setNumber(threshold_field, "IField", distance_field)
135+
gmsh.model.mesh.field.setNumber(threshold_field, "LcMin", res_min)
136+
gmsh.model.mesh.field.setNumber(threshold_field, "LcMax", 0.25 * H)
137+
gmsh.model.mesh.field.setNumber(threshold_field, "DistMin", r)
138+
gmsh.model.mesh.field.setNumber(threshold_field, "DistMax", 2 * H)
139+
min_field = gmsh.model.mesh.field.add("Min")
140+
gmsh.model.mesh.field.setNumbers(min_field, "FieldsList", [threshold_field])
141+
gmsh.model.mesh.field.setAsBackgroundMesh(min_field)
142142

143143
# ## Generating the mesh
144-
# We are now ready to generate the mesh. However, we have to decide if our mesh should consist of triangles or quadrilaterals. In this demo, to match the DFG 2D-3 benchmark, we use quadrilateral elements. This is done by recombining the mesh, setting three gmsh options.
144+
# We are now ready to generate the mesh. However, we have to decide if our mesh should consist of triangles or quadrilaterals. In this demo, to match the DFG 2D-3 benchmark, we use second order quadrilateral elements.
145145

146146
if mesh_comm.rank == model_rank:
147-
gmsh.option.setNumber("Mesh.RecombinationAlgorithm", 8)
148-
gmsh.option.setNumber("Mesh.RecombineAll", 2)
147+
gmsh.option.setNumber("Mesh.Algorithm", 8)
148+
gmsh.option.setNumber("Mesh.RecombinationAlgorithm", 2)
149+
gmsh.option.setNumber("Mesh.RecombineAll", 1)
149150
gmsh.option.setNumber("Mesh.SubdivisionAlgorithm", 1)
150151
gmsh.model.mesh.generate(gdim)
152+
gmsh.model.mesh.setOrder(2)
151153
gmsh.model.mesh.optimize("Netgen")
152154

153155
# ## Loading mesh and boundary markers
@@ -161,7 +163,7 @@
161163
# Following the DGF-2 benchmark, we define our problem specific parameters
162164

163165
t = 0
164-
T = 1 #8 # Final time
166+
T = 8 # Final time
165167
dt = 1/1600 # Time step size
166168
num_steps = int(T/dt)
167169
k = Constant(mesh, PETSc.ScalarType(dt))
@@ -210,11 +212,11 @@ def __call__(self, x):
210212
# -
211213

212214
# ## Variational form
213-
# As opposed to [Pouseille flow](./ns_code1.ipynb), we will use a Crank-Nicolson discretization, and an explicit Adams-Bashforth approximation.
215+
# As opposed to [Pouseille flow](./ns_code1.ipynb), we will use a Crank-Nicolson discretization, and an semi-implicit Adams-Bashforth approximation.
214216
# The first step can be written as
215217
#
216218
# $$
217-
# \rho\left(\frac{u^*- u^n}{\delta t} + \frac{3}{2} u^n \cdot \nabla u^n - \frac{1}{2} u^{n-1} \cdot \nabla u^{n-1}\right) - \frac{1}{2}\mu \Delta( u^*+ u^n )+ \nabla p^{n-1/2} = f^{n+\frac{1}{2}} \qquad \text{ in } \Omega
219+
# \rho\left(\frac{u^*- u^n}{\delta t} + \left(\frac{3}{2}u^{n} - \frac{1}{2} u^{n-1}\right)\cdot \frac{1}{2}\nabla (u^*+u^n) \right) - \frac{1}{2}\mu \Delta( u^*+ u^n )+ \nabla p^{n-1/2} = f^{n+\frac{1}{2}} \qquad \text{ in } \Omega
218220
# $$
219221
#
220222
# $$
@@ -263,13 +265,12 @@ def __call__(self, x):
263265

264266
f = Constant(mesh, PETSc.ScalarType((0,0)))
265267
F1 = rho / k * dot(u - u_n, v) * dx
266-
F1 += inner(1.5 * dot(u_n, nabla_grad(u_n)) - 0.5 * dot(u_n1, nabla_grad(u_n1)), v) * dx
267-
F1 += 0.5 * mu * inner(grad(u+u_n), grad(v))*dx - dot(p_, div(v))*dx
268+
F1 += inner(dot(1.5 * u_n - 0.5 * u_n1, 0.5 * nabla_grad(u + u_n)), v) * dx
269+
F1 += 0.5 * mu * inner(grad(u + u_n), grad(v))*dx - dot(p_, div(v))*dx
268270
F1 += dot(f, v) * dx
269271
a1 = form(lhs(F1))
270272
L1 = form(rhs(F1))
271-
A1 = assemble_matrix(a1, bcs=bcu)
272-
A1.assemble()
273+
A1 = create_matrix(a1)
273274
b1 = create_vector(L1)
274275

275276
# Next we define the second step
@@ -351,18 +352,17 @@ def __call__(self, x):
351352
# ## Solving the time-dependent problem
352353
# ```{admonition} Stability of the Navier-Stokes equation
353354
# Note that the current splitting scheme has to fullfil the a [Courant–Friedrichs–Lewy condition](https://en.wikipedia.org/wiki/Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition). This limits the spatial discretization with respect to the inlet velocity and temporal discretization.
354-
# Other temporal discretization schemes such as the second order backward difference discretization or Crank-Nicholson discretization with Adams-Bashforth linearization are better behaved than our simple backward differnce scheme.```
355+
# Other temporal discretization schemes such as the second order backward difference discretization or Crank-Nicholson discretization with Adams-Bashforth linearization are better behaved than our simple backward difference scheme.
356+
# ```
357+
#
355358
# As in the previous example, we create output files for the velocity and pressure and solve the time-dependent problem. As we are solving a time dependent problem with many time steps, we use the `tqdm`-package to visualize the progress. This package can be install with `pip3`.
356359

357360
# + tags=[]
358-
xdmf = XDMFFile(MPI.COMM_WORLD, "dfg2D-3.xdmf", "w")
359-
xdmf.write_mesh(mesh)
360-
xdmf.write_function(u_, t)
361-
xdmf.write_function(p_, t)
362-
363-
progress = tqdm.notebook.tqdm(desc="Solving PDE", total=num_steps)
364-
# If running this as a python script, you should use the Progressbar command below
365-
# progress = tqdm.tqdm(desc="Solving PDE", total=num_steps)
361+
vtx_u = VTXWriter(mesh.comm, "dfg2D-3-u.bp", [u_])
362+
vtx_p = VTXWriter(mesh.comm, "dfg2D-3-p.bp", [p_])
363+
vtx_u.write(t)
364+
vtx_p.write(t)
365+
progress = tqdm.autonotebook.tqdm(desc="Solving PDE", total=num_steps)
366366
for i in range(num_steps):
367367
progress.update(1)
368368
# Update current time step
@@ -371,7 +371,10 @@ def __call__(self, x):
371371
inlet_velocity.t = t
372372
u_inlet.interpolate(inlet_velocity)
373373

374-
# Step 1: Tentative veolcity step
374+
# Step 1: Tentative velocity step
375+
A1.zeroEntries()
376+
assemble_matrix(A1, a1, bcs=bcu)
377+
A1.assemble()
375378
with b1.localForm() as loc:
376379
loc.set(0)
377380
assemble_vector(b1, L1)
@@ -403,8 +406,8 @@ def __call__(self, x):
403406
u_.x.scatter_forward()
404407

405408
# Write solutions to file
406-
xdmf.write_function(u_, t)
407-
xdmf.write_function(p_, t)
409+
vtx_u.write(t)
410+
vtx_p.write(t)
408411

409412
# Update variable with solution form this time step
410413
with u_.vector.localForm() as loc_, u_n.vector.localForm() as loc_n, u_n1.vector.localForm() as loc_n1:
@@ -426,7 +429,7 @@ def __call__(self, x):
426429
p_back = mesh.comm.gather(p_back, root=0)
427430
if mesh.comm.rank == 0:
428431
t_u[i] = t
429-
t_p[i] = t-dt/2
432+
t_p[i] = t - dt / 2
430433
C_D[i] = sum(drag_coeff)
431434
C_L[i] = sum(lift_coeff)
432435
# Choose first pressure that is found from the different processors
@@ -438,16 +441,17 @@ def __call__(self, x):
438441
if pressure is not None:
439442
p_diff[i] -= pressure[0]
440443
break
441-
# Close xmdf file
442-
xdmf.close()
444+
vtx_u.close()
445+
vtx_p.close()
443446
# -
444447

445448
# ## Verification using data from FEATFLOW
446449
# As FEATFLOW has provided data for different discretization levels, we compare our numerical data with the data provided using `matplotlib`.
447-
#
448450

449451
# + tags=[]
450452
if mesh.comm.rank == 0:
453+
if not os.path.exists("figures"):
454+
os.mkdir("figures")
451455
num_velocity_dofs = V.dofmap.index_map_bs * V.dofmap.index_map.size_global
452456
num_pressure_dofs = Q.dofmap.index_map_bs * V.dofmap.index_map.size_global
453457

@@ -460,7 +464,7 @@ def __call__(self, x):
460464
plt.title("Drag coefficient")
461465
plt.grid()
462466
plt.legend()
463-
plt.savefig("drag_comparison.png")
467+
plt.savefig("figures/drag_comparison.png")
464468

465469
fig = plt.figure(figsize=(25,8))
466470
l1 = plt.plot(t_u, C_L, label=r"FEniCSx ({0:d} dofs)".format(
@@ -470,7 +474,7 @@ def __call__(self, x):
470474
plt.title("Lift coefficient")
471475
plt.grid()
472476
plt.legend()
473-
plt.savefig("lift_comparison.png")
477+
plt.savefig("figures/lift_comparison.png")
474478

475479
fig = plt.figure(figsize=(25,8))
476480
l1 = plt.plot(t_p, p_diff, label=r"FEniCSx ({0:d} dofs)".format(num_velocity_dofs+num_pressure_dofs),linewidth=2)
@@ -479,8 +483,7 @@ def __call__(self, x):
479483
plt.title("Pressure difference")
480484
plt.grid()
481485
plt.legend()
482-
plt.savefig("pressure_comparison.png")
483-
486+
plt.savefig("figures/pressure_comparison.png")
484487
# -
485488

486-
# We observe an offset in amplitude. This is due to the reduced number of degrees of freedom compared to FEATFLOW. If we change the parameters `res_min` to `r/5`, and `res_max` to `r`, we can obtain a result closer to the FEATFLOW benchmark. It is recommended to convert the notebook to a python-script using [nbconvert](https://nbconvert.readthedocs.io/en/latest/) and using `mpirun -n 4 python3 ns_code2.py` to run the python program distributed over 4 processors.
489+

0 commit comments

Comments
 (0)