Skip to content

Commit d1a041a

Browse files
authoredJan 23, 2023
Adding FEniCSx solver to partitioned-heat-equation tutorial (#317)
* adding fenicsx solver to partitioned-heat-equation tutorial * Delete printStats.py * cleanup work * changed README.md * output reference solution * Changed output folder
1 parent 71e6b5c commit d1a041a

9 files changed

+380
-5
lines changed
 

‎partitioned-heat-conduction/README.md

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Partitioned heat conduction
33
permalink: tutorials-partitioned-heat-conduction.html
4-
keywords: FEniCS, Nutils, Heat conduction
4+
keywords: FEniCSx, FEniCS, Nutils, Heat conduction
55
summary: We solve a simple heat equation. The domain is partitioned and the coupling is established in a Dirichlet-Neumann fashion.
66
---
77

@@ -25,6 +25,8 @@ This simple case allows us to compare the solution for the partitioned case to a
2525

2626
You can either couple a solver with itself or different solvers with each other. In any case you will need to have preCICE and the python bindings installed on your system.
2727

28+
* FEniCSx. Install [FEniCSx](https://fenicsproject.org/download/) and the [FEniCSx-adapter](https://github.com/precice/fenicsx-adapter). The code is largely based on this [fenics-tutorial](https://github.com/hplgit/fenics-tutorial/blob/master/pub/python/vol1/ft03_heat.py) from [1] and has been adapted to FEniCSx.
29+
2830
* FEniCS. Install [FEniCS](https://fenicsproject.org/download/) and the [FEniCS-adapter](https://github.com/precice/fenics-adapter). The code is largely based on this [fenics-tutorial](https://github.com/hplgit/fenics-tutorial/blob/master/pub/python/vol1/ft03_heat.py) from [1].
2931

3032
* Nutils. Install [Nutils](https://nutils.org/install-nutils.html).
@@ -43,18 +45,18 @@ For choosing whether you want to run the Dirichlet-kind and a Neumann-kind parti
4345
For running the case, open two terminals run:
4446

4547
```bash
46-
cd fenics
48+
cd fenicsx
4749
./run.sh -d
4850
```
4951

5052
and
5153

5254
```bash
53-
cd fenics
55+
cd fenicsx
5456
./run.sh -n
5557
```
5658

57-
If you want to use Nutils for one or both sides of the setup, just `cd nutils`. The FEniCS case also supports parallel runs. Here, you cannot use the `run.sh` script, but must simply execute
59+
If you want to use FEniCS or Nutils, use `cd fenics`/ `cd nutils` instead of `cd fenicsx`. The FEniCS case also supports parallel runs. Here, you cannot use the `run.sh` script, but must simply execute
5860

5961
```bash
6062
mpirun -n <N_PROC> heat.py -d
@@ -66,7 +68,9 @@ You can mix the Nutils and FEniCS solver, if you like. Note that the error for a
6668

6769
## Visualization
6870

69-
Output is written into the folders `fenics/out` and `nutils`.
71+
Output is written into the folders `fenicsx/out`, `fenics/out` and `nutils`.
72+
73+
For FEniCSx you can visualize the content with paraview by opening the `*.xdmf` files. The files `Dirichlet.xdmf` and `Neumann.xdmf` correspond to the numerical solution of the Dirichlet, respectively Neumann, problem, while the files with the prefix `ref` correspond to the analytical reference solution.
7074

7175
For FEniCS you can visualize the content with paraview by opening the `*.pvd` files. The files `Dirichlet.pvd` and `Neumann.pvd` correspond to the numerical solution of the Dirichlet, respectively Neumann, problem, while the files with the prefix `ref` correspond to the analytical reference solution, the files with `error` show the error and the files with `ranks` the ranks of the solvers (if executed in parallel).
7276

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
set -e -u
3+
4+
. ../../tools/cleaning-tools.sh
5+
6+
clean_fenics .
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from ufl import dx
2+
from dolfinx.fem import assemble_scalar, form
3+
import numpy as np
4+
from mpi4py import MPI
5+
6+
7+
def compute_errors(u_approx, u_ref, total_error_tol=10 ** -4):
8+
mesh = u_ref.function_space.mesh
9+
10+
# compute total L2 error between reference and calculated solution
11+
error_pointwise = form(((u_approx - u_ref) / u_ref) ** 2 * dx)
12+
error_total = np.sqrt(mesh.comm.allreduce(assemble_scalar(error_pointwise), MPI.SUM))
13+
14+
assert (error_total < total_error_tol)
15+
16+
return error_total
+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"""
2+
The basic example is taken from "Langtangen, Hans Petter, and Anders Logg. Solving PDEs in Python: The FEniCS
3+
Tutorial I. Springer International Publishing, 2016."
4+
5+
The example code has been extended with preCICE API calls and mixed boundary conditions to allow for a Dirichlet-Neumann
6+
coupling of two separate heat equations. It also has been adapted to be compatible with FEniCSx.
7+
8+
The original source code can be found on https://jsdokken.com/dolfinx-tutorial/chapter2/heat_code.html.
9+
10+
Heat equation with Dirichlet conditions. (Dirichlet problem)
11+
u'= Laplace(u) + f in the unit square [0,1] x [0,1]
12+
u = u_C on the coupling boundary at x = 1
13+
u = u_D on the remaining boundary
14+
u = u_0 at t = 0
15+
u = 1 + x^2 + alpha*y^2 + \beta*t
16+
f = beta - 2 - 2*alpha
17+
18+
Heat equation with mixed boundary conditions. (Neumann problem)
19+
u'= Laplace(u) + f in the shifted unit square [1,2] x [0,1]
20+
du/dn = f_N on the coupling boundary at x = 1
21+
u = u_D on the remaining boundary
22+
u = u_0 at t = 0
23+
u = 1 + x^2 + alpha*y^2 + \beta*t
24+
f = beta - 2 - 2*alpha
25+
"""
26+
27+
from __future__ import print_function, division
28+
from mpi4py import MPI
29+
from dolfinx.fem import Function, FunctionSpace, Expression, Constant, dirichletbc, locate_dofs_geometrical
30+
from dolfinx.fem.petsc import LinearProblem
31+
from dolfinx.io import XDMFFile
32+
from ufl import TrialFunction, TestFunction, dx, ds, dot, grad, inner, lhs, rhs, FiniteElement, VectorElement
33+
from fenicsxprecice import Adapter
34+
from errorcomputation import compute_errors
35+
from my_enums import ProblemType, DomainPart
36+
import argparse
37+
import numpy as np
38+
from problem_setup import get_geometry
39+
40+
def determine_gradient(V_g, u):
41+
"""
42+
compute flux following http://hplgit.github.io/INF5620/doc/pub/fenics_tutorial1.1/tu2.html#tut-poisson-gradu
43+
:param V_g: Vector function space
44+
:param u: solution where gradient is to be determined
45+
"""
46+
47+
w = TrialFunction(V_g)
48+
v = TestFunction(V_g)
49+
50+
a = inner(w, v) * dx
51+
L = inner(grad(u), v) * dx
52+
problem = LinearProblem(a, L)
53+
return problem.solve()
54+
55+
parser = argparse.ArgumentParser(description="Solving heat equation for simple or complex interface case")
56+
command_group = parser.add_mutually_exclusive_group(required=True)
57+
command_group.add_argument("-d", "--dirichlet", help="create a dirichlet problem", dest="dirichlet",
58+
action="store_true")
59+
command_group.add_argument("-n", "--neumann", help="create a neumann problem", dest="neumann", action="store_true")
60+
parser.add_argument("-e", "--error-tol", help="set error tolerance", type=float, default=10**-6,)
61+
62+
args = parser.parse_args()
63+
64+
fenics_dt = .1 # time step size
65+
# Error is bounded by coupling accuracy. In theory we would obtain the analytical solution.
66+
error_tol = args.error_tol
67+
68+
alpha = 3 # parameter alpha
69+
beta = 1.3 # parameter beta
70+
71+
if args.dirichlet and not args.neumann:
72+
problem = ProblemType.DIRICHLET
73+
domain_part = DomainPart.LEFT
74+
elif args.neumann and not args.dirichlet:
75+
problem = ProblemType.NEUMANN
76+
domain_part = DomainPart.RIGHT
77+
78+
mesh, coupling_boundary, remaining_boundary = get_geometry(MPI.COMM_WORLD, domain_part)
79+
80+
# Define function space using mesh
81+
scalar_element = FiniteElement("P", mesh.ufl_cell(), 2)
82+
vector_element = VectorElement("P", mesh.ufl_cell(), 1)
83+
V = FunctionSpace(mesh, scalar_element)
84+
V_g = FunctionSpace(mesh, vector_element)
85+
W = V_g.sub(0).collapse()[0]
86+
87+
# Define boundary conditions
88+
89+
90+
class Expression_u_D:
91+
def __init__(self):
92+
self.t = 0.0
93+
94+
def eval(self, x):
95+
return np.full(x.shape[1], 1 + x[0] * x[0] + alpha * x[1] * x[1] + beta * self.t)
96+
97+
98+
u_D = Expression_u_D()
99+
u_D_function = Function(V)
100+
u_D_function.interpolate(u_D.eval)
101+
102+
if problem is ProblemType.DIRICHLET:
103+
# Define flux in x direction
104+
class Expression_f_N:
105+
def __init__(self):
106+
pass
107+
108+
def eval(self, x):
109+
return np.full(x.shape[1], 2 * x[0])
110+
111+
f_N = Expression_f_N()
112+
f_N_function = Function(V)
113+
f_N_function.interpolate(f_N.eval)
114+
115+
# Define initial value
116+
u_n = Function(V, name="Temperature")
117+
u_n.interpolate(u_D.eval)
118+
119+
precice, precice_dt, initial_data = None, 0.0, None
120+
121+
# Initialize the adapter according to the specific participant
122+
if problem is ProblemType.DIRICHLET:
123+
precice = Adapter(MPI.COMM_WORLD, adapter_config_filename="precice-adapter-config-D.json")
124+
precice_dt = precice.initialize(coupling_boundary, read_function_space=V, write_object=f_N_function)
125+
elif problem is ProblemType.NEUMANN:
126+
precice = Adapter(MPI.COMM_WORLD, adapter_config_filename="precice-adapter-config-N.json")
127+
precice_dt = precice.initialize(coupling_boundary, read_function_space=W, write_object=u_D_function)
128+
129+
dt = Constant(mesh, 0.0)
130+
dt.value = np.min([fenics_dt, precice_dt])
131+
132+
# Define variational problem
133+
u = TrialFunction(V)
134+
v = TestFunction(V)
135+
136+
137+
class Expression_f:
138+
def __init__(self):
139+
self.t = 0.0
140+
141+
def eval(self, x):
142+
return np.full(x.shape[1], beta - 2 - 2 * alpha)
143+
144+
145+
f = Expression_f()
146+
f_function = Function(V)
147+
F = u * v / dt * dx + dot(grad(u), grad(v)) * dx - (u_n / dt + f_function) * v * dx
148+
149+
dofs_remaining = locate_dofs_geometrical(V, remaining_boundary)
150+
bcs = [dirichletbc(u_D_function, dofs_remaining)]
151+
152+
coupling_expression = precice.create_coupling_expression()
153+
read_data = precice.read_data()
154+
precice.update_coupling_expression(coupling_expression, read_data)
155+
if problem is ProblemType.DIRICHLET:
156+
# modify Dirichlet boundary condition on coupling interface
157+
dofs_coupling = locate_dofs_geometrical(V, coupling_boundary)
158+
bcs.append(dirichletbc(coupling_expression, dofs_coupling))
159+
if problem is ProblemType.NEUMANN:
160+
# modify Neumann boundary condition on coupling interface, modify weak
161+
# form correspondingly
162+
F += v * coupling_expression * ds
163+
164+
a, L = lhs(F), rhs(F)
165+
166+
# Time-stepping
167+
u_np1 = Function(V, name="Temperature")
168+
t = 0
169+
170+
# reference solution at t=0
171+
u_ref = Function(V, name="reference")
172+
u_ref.interpolate(u_D_function)
173+
174+
# Generating output files
175+
temperature_out = XDMFFile(MPI.COMM_WORLD, f"./output/{precice.get_participant_name()}.xdmf", "w")
176+
temperature_out.write_mesh(mesh)
177+
ref_out = XDMFFile(MPI.COMM_WORLD, f"./output/ref{precice.get_participant_name()}.xdmf", "w")
178+
ref_out.write_mesh(mesh)
179+
180+
# output solution at t=0, n=0
181+
n = 0
182+
183+
temperature_out.write_function(u_n, t)
184+
ref_out.write_function(u_ref, t)
185+
186+
u_D.t = t + dt.value
187+
u_D_function.interpolate(u_D.eval)
188+
f.t = t + dt.value
189+
f_function.interpolate(f.eval)
190+
191+
if problem is ProblemType.DIRICHLET:
192+
flux = Function(V_g, name="Heat-Flux")
193+
194+
while precice.is_coupling_ongoing():
195+
196+
# write checkpoint
197+
if precice.is_action_required(precice.action_write_iteration_checkpoint()):
198+
precice.store_checkpoint(u_n, t, n)
199+
200+
read_data = precice.read_data()
201+
202+
# Update the coupling expression with the new read data
203+
precice.update_coupling_expression(coupling_expression, read_data)
204+
205+
dt.value = np.min([fenics_dt, precice_dt])
206+
207+
linear_problem = LinearProblem(a, L, bcs=bcs)
208+
u_np1 = linear_problem.solve()
209+
210+
# Write data to preCICE according to which problem is being solved
211+
if problem is ProblemType.DIRICHLET:
212+
# Dirichlet problem reads temperature and writes flux on boundary to Neumann problem
213+
flux = determine_gradient(V_g, u_np1)
214+
flux_x = Function(W)
215+
flux_x.interpolate(flux.sub(0))
216+
precice.write_data(flux_x)
217+
elif problem is ProblemType.NEUMANN:
218+
# Neumann problem reads flux and writes temperature on boundary to Dirichlet problem
219+
precice.write_data(u_np1)
220+
221+
precice_dt = precice.advance(dt.value)
222+
223+
# roll back to checkpoint
224+
if precice.is_action_required(precice.action_read_iteration_checkpoint()):
225+
u_cp, t_cp, n_cp = precice.retrieve_checkpoint()
226+
u_n.interpolate(u_cp)
227+
t = t_cp
228+
n = n_cp
229+
else: # update solution
230+
u_n.interpolate(u_np1)
231+
t += dt.value
232+
n += 1
233+
234+
if precice.is_time_window_complete():
235+
u_ref.interpolate(u_D_function)
236+
error = compute_errors(u_n, u_ref, total_error_tol=error_tol)
237+
print('n = %d, t = %.2f: L2 error on domain = %.3g' % (n, t, error))
238+
print('output u^%d and u_ref^%d' % (n, n))
239+
240+
temperature_out.write_function(u_n, t)
241+
ref_out.write_function(u_ref, t)
242+
243+
244+
# Update Dirichlet BC
245+
u_D.t = t + dt.value
246+
u_D_function.interpolate(u_D.eval)
247+
f.t = t + dt.value
248+
f_function.interpolate(f.eval)
249+
250+
251+
# Hold plot
252+
precice.finalize()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from enum import Enum
2+
3+
4+
class ProblemType(Enum):
5+
"""
6+
Enum defines problem type. Details see above.
7+
"""
8+
DIRICHLET = 1 # Dirichlet problem
9+
NEUMANN = 2 # Neumann problem
10+
11+
12+
class DomainPart(Enum):
13+
"""
14+
Enum defines which part of the domain [x_left, x_right] x [y_bottom, y_top] we compute.
15+
"""
16+
LEFT = 1 # left part of domain in simple interface case
17+
RIGHT = 2 # right part of domain in simple interface case
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"participant_name": "Dirichlet",
3+
"config_file_name": "../precice-config.xml",
4+
"interface": {
5+
"coupling_mesh_name": "Dirichlet-Mesh",
6+
"write_data_name": "Heat-Flux",
7+
"read_data_name": "Temperature"
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"participant_name": "Neumann",
3+
"config_file_name": "../precice-config.xml",
4+
"interface": {
5+
"coupling_mesh_name": "Neumann-Mesh",
6+
"write_data_name": "Temperature",
7+
"read_data_name": "Heat-Flux"
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Problem setup for partitioned-heat-conduction/fenicsx tutorial
3+
"""
4+
from dolfinx.mesh import DiagonalType, create_rectangle
5+
from my_enums import DomainPart
6+
import numpy as np
7+
8+
9+
y_bottom, y_top = 0, 1
10+
x_left, x_right = 0, 2
11+
x_coupling = 1.0 # x coordinate of coupling interface
12+
13+
14+
def exclude_straight_boundary(x):
15+
tol = 1E-14
16+
return np.logical_or(
17+
np.logical_or(~np.isclose(x[0], x_coupling, tol), np.isclose(x[1], y_top, tol)),
18+
np.isclose(x[1], y_bottom, tol)
19+
)
20+
21+
22+
def straight_boundary(x):
23+
tol = 1E-14
24+
return np.isclose(x[0], x_coupling, tol)
25+
26+
27+
def get_geometry(mpi_comm, domain_part):
28+
nx = ny = 9
29+
low_resolution = 5
30+
high_resolution = 5
31+
n_vertices = 20
32+
33+
if domain_part is DomainPart.LEFT:
34+
p0 = (x_left, y_bottom)
35+
p1 = (x_coupling, y_top)
36+
elif domain_part is DomainPart.RIGHT:
37+
p0 = (x_coupling, y_bottom)
38+
p1 = (x_right, y_top)
39+
else:
40+
raise Exception("invalid domain_part: {}".format(domain_part))
41+
42+
mesh = create_rectangle(mpi_comm, [p0, p1], [nx, ny], diagonal=DiagonalType.left)
43+
coupling_boundary = straight_boundary
44+
remaining_boundary = exclude_straight_boundary
45+
46+
return mesh, coupling_boundary, remaining_boundary
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/sh
2+
set -e -u
3+
4+
while getopts ":dn" opt; do
5+
case ${opt} in
6+
d)
7+
python3 heat.py -d --error-tol 10e-3
8+
;;
9+
n)
10+
python3 heat.py -n --error-tol 10e-3
11+
;;
12+
\?)
13+
echo "Usage: cmd [-d] [-n]"
14+
;;
15+
esac
16+
done

0 commit comments

Comments
 (0)
Please sign in to comment.