Skip to content

Commit 73967a7

Browse files
Add dDAE BCs for Nystrom (#131)
* Add dDAE BCs for Nystrom * Adding tests for implicit BC_types * Add more informative error message * Add explicit wave demo
1 parent 1e432a9 commit 73967a7

File tree

4 files changed

+202
-5
lines changed

4 files changed

+202
-5
lines changed
+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
Solving the Wave Equation with Irksome and an explicit Nystrom method
2+
=====================================================================
3+
4+
Let's start with the simple wave equation on :math:`\Omega = [0,1]
5+
\times [0,1]`, with boundary :math:`\Gamma`:
6+
7+
.. math::
8+
9+
u_{tt} - \Delta u &= 0
10+
11+
u & = 0 \quad \textrm{on}\ \Gamma
12+
13+
At each time :math:`t`, the solution
14+
to this equation will be some function :math:`u\in V`, for a suitable function
15+
space :math:`V`.
16+
17+
We transform this into weak form by multiplying by an arbitrary test function
18+
:math:`v\in V` and integrating over :math:`\Omega`. We know have the
19+
variational problem of finding :math:`u:[0,T]\rightarrow V` such
20+
that
21+
22+
.. math::
23+
24+
(u_{tt}, v) + (\nabla u, \nabla v) = 0
25+
26+
As usual, we need to import firedrake::
27+
28+
from firedrake import *
29+
30+
We will also need to import certain items from irksome::
31+
32+
from irksome import Dt, MeshConstant, StageDerivativeNystromTimeStepper, ClassicNystrom4Tableau
33+
34+
Here, we will use the "classic" Nystrom method, a 4-stage explicit time-stepper::
35+
36+
nystrom_tableau = ClassicNystrom4Tableau()
37+
38+
Now we define the mesh and piecewise linear approximating space in
39+
standard Firedrake fashion::
40+
41+
N = 32
42+
43+
msh = UnitSquareMesh(N, N)
44+
V = FunctionSpace(msh, "CG", 2)
45+
46+
We define variables to store the time step and current time value, noting that an explicit scheme requires a small timestep for stability::
47+
48+
dt = Constant(0.2 / N)
49+
t = Constant(0.0)
50+
51+
We define the initial condition for the fully discrete problem, which
52+
will get overwritten at each time step. For a second-order problem,
53+
we need an initial condition for the solution and its time derivative
54+
(in this case, taken to be zero)::
55+
56+
x, y = SpatialCoordinate(msh)
57+
uinit = sin(pi * x) * cos(pi * y)
58+
u = Function(V)
59+
u.interpolate(uinit)
60+
ut = Function(V)
61+
62+
Now, we will define the semidiscrete variational problem using
63+
standard UFL notation, augmented by the ``Dt`` operator from Irksome.
64+
Here, the optional second argument indicates the number of derivatives,
65+
(defaulting to 1)::
66+
67+
v = TestFunction(V)
68+
F = inner(Dt(u, 2), v)*dx + inner(grad(u), grad(v))*dx
69+
bc = DirichletBC(V, 0, "on_boundary")
70+
71+
We're using an explicit scheme, so we only need to solve mass matrices in the update system. So, some good solver parameters here are to use a fieldsplit (so we only invert the diagonal blocks of the stage-coupled system) with incomplete Cholesky as a preconditioner for the mass matrices on the diagonal blocks::
72+
73+
mass_params = {"snes_type": "ksponly",
74+
"mat_type": "aij",
75+
"ksp_type": "preonly",
76+
"pc_type": "fieldsplit",
77+
"pc_fieldsplit_type": "multiplicative"}
78+
79+
per_field = {"ksp_type": "cg",
80+
"pc_type": "icc"}
81+
82+
for s in range(nystrom_tableau.num_stages):
83+
mass_params["fieldsplit_%s" % (s,)] = per_field
84+
85+
Most of Irksome's magic happens in the
86+
:class:`.StageDerivativeNystromTimeStepper`. It takes our semidiscrete
87+
form `F` and the tableau and produces the variational form for
88+
computing the stage unknowns. Then, it sets up a variational problem to be
89+
solved for the stages at each time step. Here, we use `dDAE` style boundary conditions, which impose boundary conditions on the stages to match those of :math:`u_t` on the boundary, consistent with the underlying partitioned scheme::
90+
91+
stepper = StageDerivativeNystromTimeStepper(
92+
F, nystrom_tableau, t, dt, u, ut, bcs=bc, bc_type="dDAE",
93+
solver_parameters=mass_params)
94+
95+
The system energy is an important quantity for the wave equation. It
96+
won't be conserved with our choice of time-stepping method, but serves as a good diagnostic::
97+
98+
E = 0.5 * (inner(ut, ut) * dx + inner(grad(u), grad(u)) * dx)
99+
print(f"Initial energy: {assemble(E)}")
100+
101+
Then, we can loop over time steps, much like with 1st order systems::
102+
103+
while (float(t) < 1.0):
104+
if (float(t) + float(dt) > 1.0):
105+
dt.assign(1.0 - float(t))
106+
stepper.advance()
107+
t.assign(float(t) + float(dt))
108+
print(f"Time: {float(t)}, Energy: {assemble(E)}")
109+

docs/source/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ and for Nystrom methods for second-order-in-time equations:
103103
:maxdepth: 1
104104

105105
demos/demo_wave.py
106+
demos/demo_wave_explicit.py
106107
demos/demo_wave_mg.py
107108

108109
and for bounds constraints:

irksome/nystrom_stepper.py

+30
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ def bc2gcur(bc, i):
117117
return replace(gfoo, {t: t + c[i] * dt})
118118

119119
elif bc_type == "DAE":
120+
if tableau.is_explicit:
121+
raise NotImplementedError("Can't impose DAE BCs for an explicit Nystrom tableau. You should probably try bc_type = 'dDAE' instead")
120122
try:
121123
bA1inv = numpy.linalg.inv(tableau.Abar)
122124
A1inv = vecconst(bA1inv)
@@ -130,6 +132,34 @@ def bc2gcur(bc, i):
130132
gcur = (1/dt**2) * sum((replace(gorig, {t: t + c[j]*dt}) - ucur - utcur * (dt * c[j])) * A1inv[i, j]
131133
for j in range(num_stages))
132134
return gcur
135+
136+
elif bc_type == "dDAE":
137+
if tableau.is_explicit:
138+
try:
139+
AAb = numpy.vstack((tableau.A, tableau.b))
140+
AAb = AAb[1:]
141+
bA1inv = numpy.linalg.inv(AAb)
142+
A1inv = vecconst(bA1inv)
143+
c_one = numpy.append(tableau.c, 1.0)
144+
c_ddae = vecconst(c_one[1:])
145+
except numpy.linalg.LinAlgError:
146+
raise NotImplementedError("Can't have derivative DAE BC's for this method")
147+
else:
148+
try:
149+
bA1inv = numpy.linalg.inv(tableau.A)
150+
A1inv = vecconst(bA1inv)
151+
c_ddae = vecconst(tableau.c)
152+
except numpy.linalg.LinAlgError:
153+
raise NotImplementedError("Can't have derivative DAE BC's for this method")
154+
155+
def bc2gcur(bc, i):
156+
gorig = as_ufl(bc._original_arg)
157+
gfoo = expand_time_derivatives(Dt(gorig, 1), t=t, timedep_coeffs=(u0,))
158+
utcur = bc2space(bc, ut0)
159+
gcur = (1/dt) * sum((replace(gfoo, {t: t + c_ddae[j]*dt}) - utcur) * A1inv[i, j]
160+
for j in range(num_stages))
161+
return gcur
162+
133163
else:
134164
raise ValueError(f"Unrecognized BC type: {bc_type}")
135165

tests/test_nystrom.py

+62-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import pytest
12
from firedrake import (Constant, DirichletBC, Function, FunctionSpace, SpatialCoordinate,
23
TestFunction, UnitIntervalMesh, VectorFunctionSpace, assemble, div, dx,
34
norm, grad, inner, pi, project, sin, split)
4-
from irksome import Dt, GaussLegendre, StageDerivativeNystromTimeStepper
5+
from irksome import Dt, GaussLegendre, StageDerivativeNystromTimeStepper, ClassicNystrom4Tableau
56

67

7-
def wave(n, deg, time_stages):
8+
def wave(n, deg, time_stages, bc_type):
89
N = 2**n
910
msh = UnitIntervalMesh(N)
1011

@@ -36,7 +37,7 @@ def wave(n, deg, time_stages):
3637
E = 0.5 * inner(ut, ut) * dx + 0.5 * inner(grad(u), grad(u)) * dx
3738

3839
stepper = StageDerivativeNystromTimeStepper(
39-
F, butcher_tableau, t, dt, u, ut, bcs=bc, solver_parameters=params)
40+
F, butcher_tableau, t, dt, u, ut, bcs=bc, solver_parameters=params, bc_type=bc_type)
4041

4142
E0 = assemble(E)
4243
tf = 1
@@ -49,12 +50,13 @@ def wave(n, deg, time_stages):
4950
return assemble(E) / E0, norm(u + u0)
5051

5152

52-
def test_wave_eq():
53+
@pytest.mark.parametrize("bc_type", ["ODE", "DAE", "dDAE"])
54+
def test_wave_eq(bc_type):
5355
# number of refinements
5456
n = 5
5557
deg = 2
5658
stage_count = 2
57-
Erat, diff = wave(n, deg, stage_count)
59+
Erat, diff = wave(n, deg, stage_count, bc_type)
5860
print(Erat, diff)
5961
assert abs(Erat - 1) < 1.e-8
6062
assert diff < 3.e-5
@@ -120,3 +122,58 @@ def test_mixed_wave_eq():
120122
print(Erat, diff)
121123
assert abs(Erat - 1) < 1.e-8
122124
assert diff < 3.e-5
125+
126+
127+
def explicit_wave(n, deg):
128+
N = 2**n
129+
msh = UnitIntervalMesh(N)
130+
131+
params = {"snes_type": "ksponly",
132+
"ksp_type": "preonly",
133+
"mat_type": "aij",
134+
"pc_type": "lu"}
135+
136+
V = FunctionSpace(msh, "CG", deg)
137+
x, = SpatialCoordinate(msh)
138+
139+
t = Constant(0.0)
140+
dt = Constant(0.1 / N)
141+
142+
uinit = sin(pi * x)
143+
144+
nystrom_tableau = ClassicNystrom4Tableau()
145+
146+
u0 = project(uinit, V)
147+
u = Function(u0) # copy
148+
ut = Function(V)
149+
150+
v = TestFunction(V)
151+
152+
F = inner(Dt(u, 2), v) * dx + inner(grad(u), grad(v)) * dx
153+
154+
bc = DirichletBC(V, 0, "on_boundary")
155+
156+
E = 0.5 * inner(ut, ut) * dx + 0.5 * inner(grad(u), grad(u)) * dx
157+
158+
stepper = StageDerivativeNystromTimeStepper(
159+
F, nystrom_tableau, t, dt, u, ut, bcs=bc, solver_parameters=params, bc_type="dDAE")
160+
161+
E0 = assemble(E)
162+
tf = 1
163+
while (float(t) < tf):
164+
if (float(t) + float(dt) > tf):
165+
dt.assign(tf - float(t))
166+
stepper.advance()
167+
t.assign(float(t) + float(dt))
168+
169+
return assemble(E) / E0, norm(u + u0)
170+
171+
172+
def test_explicit_wave_eq():
173+
# Mesh size and polynomial degree
174+
n = 5
175+
deg = 2
176+
Erat, diff = explicit_wave(n, deg)
177+
print(Erat, diff)
178+
assert abs(Erat - 1) < 1.e-6
179+
assert diff < 3.e-5

0 commit comments

Comments
 (0)