Skip to content

Commit 0c3407a

Browse files
Add nutils solver for heat conduction problem (#153)
* Strip case of everything besides simple heat conduction setup. * Adds precice-config.xml supporting parallel runs. * Add documentation and nutils case. * Add complex case. Based on ae77ae5. * Cleanup and Update to newest version of python bindings. * Use scalar-valued flux. * Apply new structure to complex case. * Remove subcycling. * Simplify heat nutils to one mesh, make config consistent * Fix names. * BCs working. * Minor restructuring * Fix initial condition and output. * Mark nutils case as under construction. See #152. * Add nutils prototype from 679fe8d. * Add instructions on running nutils case to README * Cleanup * Fix geometry. * Add convenience scripts and improve output. * Make output more consistent. * Update convenience scripts. * Fix sign error. * Add monolithic solver for comparison. * Scale flux by element size. * Use identical mesh for FEniCS and Nutils. * Add argument for setting error tolerance. * Update README.md * Remove monolithic case. * Smooth readme of partitioned heat case * More information on error. * Provide cleaning scripts according to #169. * Streamline run and clean scripts. * Cleanup. * Align Nutils vtk output format with Paraview * Make explanation of fenics-nutils error more precise * Comment, format, and rewrite nutils heat code Co-authored-by: uekerman <[email protected]> Co-authored-by: Benjamin Uekermann <[email protected]>
1 parent e7149c0 commit 0c3407a

File tree

11 files changed

+244
-25
lines changed

11 files changed

+244
-25
lines changed

partitioned-heat-conduction/README.md

+18-8
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
title: Partitioned heat conduction
33
permalink: tutorials-partitioned-heat-conduction.html
44
keywords: FEniCS, Nutils, Heat conduction
5-
summary: In this tutorial we solve a simple heat equation. The domain is partitioned and the coupling is established in a Dirichlet-Neumann fashion.
5+
summary: We solve a simple heat equation. The domain is partitioned and the coupling is established in a Dirichlet-Neumann fashion.
66
---
77

88
{% include important.html content="We have not yet ported the documentation of the preCICE tutorials from the preCICE wiki to here. Please go to the [preCICE wiki](https://github.com/precice/precice/wiki#2-getting-started---tutorials)" %}
99

1010
## Setup
1111

12-
In this tutorial we solve a partitioned heat equation. For information on the non-partitioned case, please refer to [1, p.37ff]. In this tutorial the computational domain is partitioned and coupled via preCICE. The coupling roughly follows the approach described in [2].
12+
We solve a partitioned heat equation. For information on the non-partitioned case, please refer to [1, p.37ff]. In this tutorial the computational domain is partitioned and coupled via preCICE. The coupling roughly follows the approach described in [2].
1313

1414
![Case setup of partitioned-heat-conduction case](images/tutorials-partitioned-heat-conduction-setup.png)
1515

@@ -26,38 +26,48 @@ You can either couple a solver with itself or different solvers with each other.
2626
* `fenics`, requires you to 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].
2727

2828

29-
* :construction: This case is still under construction. See https://github.com/precice/tutorials/issues/152. :construction: `nutils`, requires you to install [Nutils](http://www.nutils.org/en/latest/).
29+
* `nutils`, requires you to install [Nutils](http://www.nutils.org/en/latest/).
3030

3131
## Running the simulation
3232

33+
This tutorial is for FEniCS and Nutils. You can find the corresponding `run.sh` script in the folders `fenics` and `nutils`.
34+
3335
For choosing whether you want to run the Dirichlet-kind and a Neumann-kind participant, please provide the following commandline input:
3436

3537
* `-d` flag will enforce Dirichlet boundary conditions on the coupling interface.
3638
* `-n` flag will enforce Neumann boundary conditions on the coupling interface.
3739

38-
For running the case, open two terminals and:
40+
For running the case, open two terminals run:
3941

4042
```
4143
cd fenics
42-
python3 heat.py -d
44+
python3 ./run.sh -d
4345
```
4446

4547
and
4648

4749
```
4850
cd fenics
49-
python3 heat.py -n
51+
python3 ./run.sh -n
5052
```
5153

52-
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, simply execute
54+
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
5355

5456
```
5557
mpirun -n <N_PROC> python3 heat.py -d
5658
```
5759

60+
### Note on the combination of Nutils & FEniCS
61+
62+
You can mix the Nutils and FEniCS solver, if you like. Note that the error for a pure FEniCS simulation is lower than for a mixed one. We did not yet study the origin of this error, but assume that this is due to the fact that Nutils uses Gauss points as coupling mesh and therefore entails extrapolation in the data mapping at the top and bottom corners.
63+
5864
## Visualization
5965

60-
Output is written into the folder `fenics/out`. 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).
66+
Output is written into the folders `fenics/out` and `nutils`.
67+
68+
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).
69+
70+
For Nutils, please use the files `Dirichlet-*.vtk` or `Neumann-*.vtk`. Please note that these files contain the temperature as well as the reference solution.
6171

6272
![Animation of the partitioned heat equation](images/tutorials-partitioned-heat-conduction-FEniCS-movie.gif)
6373

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_tutorial .

partitioned-heat-conduction/fenics/Allclean

-11
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
set -e -u
3+
4+
. ../../tools/cleaning-tools.sh
5+
6+
rm -rf out
7+
rm -f *.log
8+
rm -f *-events.json

partitioned-heat-conduction/fenics/heat.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@ def determine_gradient(V_g, u, flux):
6161
action="store_true")
6262
command_group.add_argument("-n", "--neumann", help="create a neumann problem",
6363
dest="neumann", action="store_true")
64+
parser.add_argument("-e", "--error-tol", help="set error tolerance",
65+
type=float, default=10**-6,)
6466

6567
args = parser.parse_args()
6668

6769
fenics_dt = .1 # time step size
6870
# Error is bounded by coupling accuracy. In theory we would obtain the analytical solution.
69-
error_tol = 10 ** -6
71+
error_tol = args.error_tol
7072

7173
alpha = 3 # parameter alpha
7274
beta = 1.3 # parameter beta

partitioned-heat-conduction/fenics/problem_setup.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,21 @@ def inside(self, x, on_boundary):
3535

3636

3737
def get_geometry(domain_part):
38-
nx = 5
39-
ny = 10
38+
nx = ny = 9
4039
low_resolution = 5
4140
high_resolution = 5
4241
n_vertices = 20
4342

4443
if domain_part is DomainPart.LEFT:
45-
nx = nx*3
4644
p0 = Point(x_left, y_bottom)
4745
p1 = Point(x_coupling, y_top)
4846
elif domain_part is DomainPart.RIGHT:
49-
ny = ny*2
5047
p0 = Point(x_coupling, y_bottom)
5148
p1 = Point(x_right, y_top)
5249
else:
5350
raise Exception("invalid domain_part: {}".format(domain_part))
5451

55-
mesh = RectangleMesh(p0, p1, nx, ny)
52+
mesh = RectangleMesh(p0, p1, nx, ny, diagonal="left")
5653
coupling_boundary = StraightBoundary()
5754
remaining_boundary = ExcludeStraightBoundary()
5855

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
while getopts ":dn" opt; do
2+
case ${opt} in
3+
d ) python3 heat.py -d --error-tol 10e-3
4+
;;
5+
n ) python3 heat.py -n --error-tol 10e-3
6+
;;
7+
\? ) echo "Usage: cmd [-d] [-n]"
8+
;;
9+
esac
10+
done
11+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Neumann-*.vtk
2+
Dirichlet-*.vtk
3+
*.log
4+
precice-*-events.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/sh
2+
set -e -u
3+
4+
. ../../tools/cleaning-tools.sh
5+
6+
rm -f Dirichlet-*.vtk
7+
rm -f Neumann-*.vtk
8+
rm -f *.log
9+
rm -f *-events.json
+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#! /usr/bin/env python3
2+
3+
import nutils, treelog
4+
import numpy as np
5+
import precice
6+
7+
8+
def main(side='Dirichlet'):
9+
10+
print("Running nutils")
11+
12+
# domain size
13+
y_bottom, y_top = 0, 1
14+
x_left, x_right = 0, 2
15+
x_coupling = 1 # x coordinate of coupling interface
16+
17+
n = 10 # number of mesh vertices per dimension
18+
19+
if side == 'Dirichlet':
20+
x_grid = np.linspace(x_left, x_coupling, n)
21+
elif side == 'Neumann':
22+
x_grid = np.linspace(x_coupling, x_right, n)
23+
else:
24+
raise Exception('invalid side {!r}'.format(side))
25+
26+
y_grid = np.linspace(y_bottom, y_top, n)
27+
28+
# define the Nutils mesh
29+
domain, geom = nutils.mesh.rectilinear([x_grid, y_grid])
30+
31+
# Nutils namespace
32+
ns = nutils.function.Namespace()
33+
ns.x = geom
34+
35+
degree = 1 # linear finite elements
36+
ns.basis = domain.basis('std', degree=degree)
37+
ns.alpha = 3 # parameter of problem
38+
ns.beta = 1.3 # parameter of problem
39+
ns.u = 'basis_n ?lhs_n' # solution
40+
ns.dudt = 'basis_n (?lhs_n - ?lhs0_n) / ?dt' # time derivative
41+
ns.flux = 'basis_n ?fluxdofs_n' # heat flux
42+
ns.f = 'beta - 2 - 2 alpha' # rhs
43+
ns.uexact = '1 + x_0 x_0 + alpha x_1 x_1 + beta ?t' # analytical solution
44+
45+
# define the weak form
46+
res0 = domain.integral('(basis_n dudt - basis_n f + basis_n,i u_,i) d:x' @ ns, degree=degree * 2)
47+
48+
# set boundary conditions at non-coupling boundaries
49+
# top and bottom boundary are non-coupling for both sides
50+
sqr0 = domain.boundary['top'].integral('(u - 1 - x_0 x_0 - alpha - beta ?t)^2 d:x' @ ns, degree=degree * 2)
51+
sqr0 += domain.boundary['bottom'].integral('(u - 1 - x_0 x_0 - beta ?t)^2 d:x' @ ns, degree=degree * 2)
52+
if side == 'Dirichlet': # left boundary is non-coupling
53+
sqr0 += domain.boundary['left'].integral('(u - 1 - alpha x_1 x_1 - beta ?t)^2 d:x' @ ns, degree=degree * 2)
54+
elif side == 'Neumann': # right boundary is non-coupling
55+
sqr0 += domain.boundary['right'].integral('(u - 1 - x_0 x_0 - alpha x_1 x_1 - beta ?t)^2 d:x' @ ns, degree=degree * 2)
56+
57+
58+
# preCICE setup
59+
interface = precice.Interface(side, "../precice-config.xml", 0, 1)
60+
61+
# define coupling mesh
62+
mesh_name = side + "-Mesh"
63+
mesh_id = interface.get_mesh_id(mesh_name)
64+
coupling_boundary = domain.boundary['right' if side == 'Dirichlet' else 'left']
65+
coupling_sample = coupling_boundary.sample('gauss', degree=degree * 2)
66+
vertices = coupling_sample.eval(ns.x)
67+
vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
68+
69+
# coupling data
70+
write_data = "Temperature" if side == "Neumann" else "Flux"
71+
read_data = "Flux" if side == "Neumann" else "Temperature"
72+
write_data_id = interface.get_data_id(write_data, mesh_id)
73+
read_data_id = interface.get_data_id(read_data, mesh_id)
74+
75+
# helper functions to project heat flux to coupling boundary
76+
projection_matrix = coupling_boundary.integrate(ns.eval_nm('basis_n basis_m d:x'), degree=degree * 2)
77+
projection_cons = np.zeros(res0.shape)
78+
projection_cons[projection_matrix.rowsupp(1e-15)] = np.nan
79+
fluxdofs = lambda v: projection_matrix.solve(v, constrain=projection_cons)
80+
81+
# helper data structures to integrate heat flux correctly
82+
dx = coupling_sample.eval('d:x' @ ns)
83+
dx_function = coupling_sample.asfunction(dx)
84+
85+
precice_dt = interface.initialize()
86+
87+
# write initial data
88+
if interface.is_action_required(precice.action_write_initial_data()):
89+
write_data = np.zeros(len(vertex_ids))
90+
interface.write_block_scalar_data(write_data_id, vertex_ids, write_data)
91+
interface.mark_action_fulfilled(precice.action_write_initial_data())
92+
93+
interface.initialize_data()
94+
95+
t = 0
96+
97+
# initial condition
98+
sqr = domain.integral('(u - uexact)^2' @ ns, degree=degree*2)
99+
lhs0 = nutils.solver.optimize('lhs', sqr, droptol=1e-15, arguments=dict(t=t))
100+
bezier = domain.sample('bezier', degree * 2)
101+
x, u, uexact = bezier.eval(['x_i', 'u', 'uexact'] @ ns, lhs=lhs0, t=t)
102+
with treelog.add(treelog.DataLog()):
103+
nutils.export.vtk(side + '-0', bezier.tri, x, Temperature=u, reference=uexact)
104+
105+
t += precice_dt
106+
timestep = 1
107+
dt = 0.1
108+
109+
while interface.is_coupling_ongoing():
110+
111+
# update (time-dependent) boundary condition
112+
cons0 = nutils.solver.optimize('lhs', sqr0, droptol=1e-15, arguments=dict(t=t))
113+
114+
# read data from interface
115+
if interface.is_read_data_available():
116+
read_data = interface.read_block_scalar_data(read_data_id, vertex_ids)
117+
read_function = coupling_sample.asfunction(read_data)
118+
119+
if side == 'Dirichlet':
120+
sqr = coupling_sample.integral((ns.u - read_function)**2)
121+
cons = nutils.solver.optimize('lhs', sqr, droptol=1e-15, constrain=cons0, arguments=dict(t=t))
122+
res = res0
123+
else:
124+
cons = cons0
125+
res = res0 + coupling_sample.integral(ns.basis * read_function * dx_function)
126+
127+
# save checkpoint
128+
if interface.is_action_required(precice.action_write_iteration_checkpoint()):
129+
lhs_checkpoint = lhs0
130+
interface.mark_action_fulfilled(precice.action_write_iteration_checkpoint())
131+
132+
# potentially adjust non-matching timestep sizes
133+
dt = min(dt, precice_dt)
134+
135+
# solve nutils timestep
136+
lhs = nutils.solver.solve_linear('lhs', res, constrain=cons, arguments=dict(lhs0=lhs0, dt=dt, t=t))
137+
138+
# write data to interface
139+
if interface.is_write_data_required(dt):
140+
if side == 'Dirichlet':
141+
flux_function = res.eval(lhs0=lhs0, lhs=lhs, dt=dt, t=t)
142+
write_data = coupling_sample.eval('flux' @ ns, fluxdofs=fluxdofs(flux_function))
143+
else:
144+
write_data = coupling_sample.eval('u' @ ns, lhs=lhs)
145+
146+
interface.write_block_scalar_data(write_data_id, vertex_ids, write_data)
147+
148+
# do the coupling
149+
precice_dt = interface.advance(dt)
150+
151+
# read checkpoint if required
152+
if interface.is_action_required(precice.action_read_iteration_checkpoint()):
153+
lhs0 = lhs_checkpoint
154+
interface.mark_action_fulfilled(precice.action_read_iteration_checkpoint())
155+
else: # go to next timestep
156+
bezier = domain.sample('bezier', degree * 2)
157+
x, u, uexact = bezier.eval(['x_i', 'u', 'uexact'] @ ns, lhs=lhs, t=t)
158+
159+
with treelog.add(treelog.DataLog()):
160+
nutils.export.vtk(side + "-" + str(timestep), bezier.tri, x, Temperature=u, reference=uexact)
161+
162+
t += dt
163+
timestep += 1
164+
lhs0 = lhs
165+
166+
interface.finalize()
167+
168+
169+
if __name__ == '__main__':
170+
nutils.cli.run(main)
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
while getopts ":dn" opt; do
2+
case ${opt} in
3+
d ) rm -rf Dirichlet-*.vtk
4+
python3 heat.py --side=Dirichlet
5+
;;
6+
n ) rm -rf Neumann-*.vtk
7+
python3 heat.py --side=Neumann
8+
;;
9+
\? ) echo "Usage: cmd [-d] [-n]"
10+
;;
11+
esac
12+
done
13+

0 commit comments

Comments
 (0)