Skip to content

Commit 8b1ed0c

Browse files
Added multi-level SDC for Firedrake and Gusto coupling (#520)
* Implemented MLSDC for Firedrake and Gusto coupling * Removed unused `equation` argument from `pySDC_integrator`
1 parent 251b5ed commit 8b1ed0c

File tree

8 files changed

+425
-28
lines changed

8 files changed

+425
-28
lines changed

pySDC/helpers/pySDC_as_gusto_time_discretization.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ class pySDC_integrator(TimeDiscretisation):
3838

3939
def __init__(
4040
self,
41-
equation,
4241
description,
4342
controller_params,
4443
domain,
@@ -52,7 +51,6 @@ def __init__(
5251
Initialization
5352
5453
Args:
55-
equation (:class:`PrognosticEquation`): the prognostic equation.
5654
description (dict): pySDC description
5755
controller_params (dict): pySDC controller params
5856
domain (:class:`Domain`): the model's domain object, containing the
@@ -96,6 +94,7 @@ def setup(self, equation, apply_bcs=True, *active_labels):
9694
'equation': equation,
9795
'solver_parameters': self.solver_parameters,
9896
'residual': self._residual,
97+
**self.description['problem_params'],
9998
}
10099
self.description['level_params']['dt'] = float(self.domain.dt)
101100

pySDC/implementations/problem_classes/HeatFiredrake.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,30 @@ class Heat1DForcedFiredrake(Problem):
5353
dtype_u = firedrake_mesh
5454
dtype_f = IMEX_firedrake_mesh
5555

56-
def __init__(self, n=30, nu=0.1, c=0.0, LHS_cache_size=12, comm=None):
56+
def __init__(self, n=30, nu=0.1, c=0.0, order=4, LHS_cache_size=12, mesh=None, comm=None):
5757
"""
5858
Initialization
5959
6060
Args:
6161
n (int): Number of degrees of freedom
6262
nu (float): Diffusion parameter
6363
c (float): Boundary condition constant
64+
order (int): Order of finite elements
6465
LHS_cache_size (int): Size of the cache for solvers
65-
comm (mpi4pi.Intracomm): MPI communicator for spatial parallelism
66+
mesh (Firedrake mesh, optional): Give a custom mesh, for instance from a hierarchy
67+
comm (mpi4pi.Intracomm, optional): MPI communicator for spatial parallelism
6668
"""
6769
comm = MPI.COMM_WORLD if comm is None else comm
6870

6971
# prepare Firedrake mesh and function space
70-
self.mesh = fd.UnitIntervalMesh(n, comm=comm)
71-
self.V = fd.FunctionSpace(self.mesh, "CG", 4)
72+
self.mesh = fd.UnitIntervalMesh(n, comm=comm) if mesh is None else mesh
73+
self.V = fd.FunctionSpace(self.mesh, "CG", order)
7274

7375
# prepare pySDC problem class infrastructure by passing the function space to super init
7476
super().__init__(self.V)
75-
self._makeAttributeAndRegister('n', 'nu', 'c', 'LHS_cache_size', 'comm', localVars=locals(), readOnly=True)
77+
self._makeAttributeAndRegister(
78+
'n', 'nu', 'c', 'order', 'LHS_cache_size', 'comm', localVars=locals(), readOnly=True
79+
)
7680

7781
# prepare caches and IO variables for solvers
7882
self.solvers = {}
@@ -191,6 +195,10 @@ def solve_system(self, rhs, factor, *args, **kwargs):
191195
self.work_counters['solves']()
192196
return me
193197

198+
@fd.utils.cached_property
199+
def x(self):
200+
return fd.SpatialCoordinate(self.mesh)
201+
194202
def u_exact(self, t):
195203
r"""
196204
Routine to compute the exact solution at time :math:`t`.
@@ -206,6 +214,5 @@ def u_exact(self, t):
206214
Exact solution.
207215
"""
208216
me = self.u_init
209-
x = fd.SpatialCoordinate(self.mesh)
210-
me.interpolate(np.cos(t) * fd.sin(np.pi * x[0]) + self.c)
217+
me.interpolate(np.cos(t) * fd.sin(np.pi * self.x[0]) + self.c)
211218
return me
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from firedrake import assemble, prolong, inject
2+
from firedrake.__future__ import interpolate
3+
4+
from pySDC.core.errors import TransferError
5+
from pySDC.core.space_transfer import SpaceTransfer
6+
from pySDC.implementations.datatype_classes.firedrake_mesh import firedrake_mesh, IMEX_firedrake_mesh
7+
8+
9+
class MeshToMeshFiredrake(SpaceTransfer):
10+
"""
11+
This implementation can restrict and prolong between Firedrake meshes
12+
"""
13+
14+
def restrict(self, F):
15+
"""
16+
Restrict from fine to coarse grid
17+
18+
Args:
19+
F: the fine level data
20+
21+
Returns:
22+
Coarse level data
23+
"""
24+
if isinstance(F, firedrake_mesh):
25+
u_coarse = self.coarse_prob.dtype_u(assemble(interpolate(F.functionspace, self.coarse_prob.init)))
26+
elif isinstance(F, IMEX_firedrake_mesh):
27+
u_coarse = IMEX_firedrake_mesh(self.coarse_prob.init)
28+
u_coarse.impl.functionspace.assign(assemble(interpolate(F.impl.functionspace, self.coarse_prob.init)))
29+
u_coarse.expl.functionspace.assign(assemble(interpolate(F.expl.functionspace, self.coarse_prob.init)))
30+
else:
31+
raise TransferError('Unknown type of fine data, got %s' % type(F))
32+
33+
return u_coarse
34+
35+
def prolong(self, G):
36+
"""
37+
Prolongate from coarse to fine grid
38+
39+
Args:
40+
G: the coarse level data
41+
42+
Returns:
43+
fine level data
44+
"""
45+
if isinstance(G, firedrake_mesh):
46+
u_fine = self.fine_prob.dtype_u(assemble(interpolate(G.functionspace, self.fine_prob.init)))
47+
elif isinstance(G, IMEX_firedrake_mesh):
48+
u_fine = IMEX_firedrake_mesh(self.fine_prob.init)
49+
u_fine.impl.functionspace.assign(assemble(interpolate(G.impl.functionspace, self.fine_prob.init)))
50+
u_fine.expl.functionspace.assign(assemble(interpolate(G.expl.functionspace, self.fine_prob.init)))
51+
else:
52+
raise TransferError('Unknown type of coarse data, got %s' % type(G))
53+
54+
return u_fine
55+
56+
57+
class MeshToMeshFiredrakeHierarchy(SpaceTransfer):
58+
"""
59+
This implementation can restrict and prolong between Firedrake meshes that are generated from a hierarchy.
60+
Example:
61+
62+
```
63+
from firedrake import *
64+
65+
mesh = UnitSquareMesh(8, 8)
66+
hierarchy = MeshHierarchy(mesh, 4)
67+
68+
mesh = hierarchy[-1]
69+
```
70+
"""
71+
72+
@staticmethod
73+
def _restrict(u_fine, u_coarse):
74+
"""Perform restriction in Firedrake"""
75+
inject(u_fine.functionspace, u_coarse.functionspace)
76+
77+
@staticmethod
78+
def _prolong(u_coarse, u_fine):
79+
"""Perform prolongation in Firedrake"""
80+
prolong(u_coarse.functionspace, u_fine.functionspace)
81+
82+
def restrict(self, F):
83+
"""
84+
Restrict from fine to coarse grid
85+
86+
Args:
87+
F: the fine level data
88+
89+
Returns:
90+
Coarse level data
91+
"""
92+
if isinstance(F, firedrake_mesh):
93+
G = self.coarse_prob.u_init
94+
self._restrict(u_fine=F, u_coarse=G)
95+
elif isinstance(F, IMEX_firedrake_mesh):
96+
G = IMEX_firedrake_mesh(self.coarse_prob.init)
97+
self._restrict(u_fine=F.impl, u_coarse=G.impl)
98+
self._restrict(u_fine=F.expl, u_coarse=G.expl)
99+
else:
100+
raise TransferError('Unknown type of fine data, got %s' % type(F))
101+
102+
return G
103+
104+
def prolong(self, G):
105+
"""
106+
Prolongate from coarse to fine grid
107+
108+
Args:
109+
G: the coarse level data
110+
111+
Returns:
112+
fine level data
113+
"""
114+
if isinstance(G, firedrake_mesh):
115+
F = self.fine_prob.u_init
116+
self._prolong(u_coarse=G, u_fine=F)
117+
elif isinstance(G, IMEX_firedrake_mesh):
118+
F = self.fine_prob.f_init
119+
self._prolong(u_coarse=G.impl, u_fine=F.impl)
120+
self._prolong(u_coarse=G.expl, u_fine=F.expl)
121+
else:
122+
raise TransferError('Unknown type of coarse data, got %s' % type(G))
123+
124+
return F

pySDC/tests/test_helpers/test_gusto_coupling.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,6 @@ def test_pySDC_integrator_RK(use_transport_scheme, method, setup):
291291
stepper_pySDC = get_gusto_stepper(
292292
eqns,
293293
pySDC_integrator(
294-
eqns,
295294
description,
296295
controller_params,
297296
domain,
@@ -415,7 +414,6 @@ def test_pySDC_integrator(use_transport_scheme, imex, setup):
415414
stepper_pySDC = get_gusto_stepper(
416415
eqns,
417416
pySDC_integrator(
418-
eqns,
419417
description,
420418
controller_params,
421419
domain,
@@ -550,7 +548,6 @@ def test_pySDC_integrator_with_adaptivity(dt_initial, setup):
550548
stepper_pySDC = get_gusto_stepper(
551549
eqns,
552550
pySDC_integrator(
553-
eqns,
554551
description,
555552
controller_params,
556553
domain,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import pytest
2+
3+
4+
@pytest.mark.firedrake
5+
def test_Firedrake_transfer():
6+
import firedrake as fd
7+
from firedrake.__future__ import interpolate
8+
from pySDC.implementations.problem_classes.HeatFiredrake import Heat1DForcedFiredrake
9+
from pySDC.implementations.transfer_classes.TransferFiredrakeMesh import MeshToMeshFiredrake
10+
import numpy as np
11+
12+
# prepare fine and coarse problems
13+
resolutions = [2, 4]
14+
probs = [Heat1DForcedFiredrake(n=n) for n in resolutions]
15+
16+
# prepare data that we can interpolate exactly in this discretization
17+
functions = [fd.Function(me.V).interpolate(me.x**2) for me in probs]
18+
data = [me.u_init for me in probs]
19+
[data[i].assign(functions[i]) for i in range(len(functions))]
20+
rhs = [me.f_init for me in probs]
21+
[rhs[i].impl.assign(functions[i]) for i in range(len(functions))]
22+
[rhs[i].expl.assign(functions[i]) for i in range(len(functions))]
23+
24+
# setup transfer class
25+
transfer = MeshToMeshFiredrake(*probs[::-1], {})
26+
27+
# test that restriction and interpolation were indeed exact on the mesh
28+
restriction = transfer.restrict(data[1])
29+
error = abs(restriction - data[0])
30+
assert np.isclose(error, 0), f'Got unexpectedly large {error=} during restriction!'
31+
32+
interpolation = transfer.prolong(data[0])
33+
error = abs(interpolation - data[1])
34+
assert np.isclose(error, 0), f'Got unexpectedly large {error=} during interpolation!'
35+
36+
# test that restriction and interpolation were indeed exact on the IMEX mesh
37+
restriction = transfer.restrict(rhs[1])
38+
error = max([abs(restriction.impl - rhs[0].impl), abs(restriction.expl - rhs[0].expl)])
39+
assert np.isclose(error, 0), f'Got unexpectedly large {error=} during restriction!'
40+
41+
interpolation = transfer.prolong(rhs[0])
42+
error = max([abs(interpolation.impl - rhs[1].impl), abs(interpolation.expl - rhs[1].expl)])
43+
assert np.isclose(error, 0), f'Got unexpectedly large {error=} during interpolation!'
44+
45+
46+
@pytest.mark.firedrake
47+
def test_Firedrake_transfer_hierarchy():
48+
import firedrake as fd
49+
from firedrake.__future__ import interpolate
50+
from pySDC.implementations.problem_classes.HeatFiredrake import Heat1DForcedFiredrake
51+
from pySDC.implementations.transfer_classes.TransferFiredrakeMesh import MeshToMeshFiredrakeHierarchy
52+
import numpy as np
53+
54+
# prepare fine and coarse problems with the hierarchy
55+
_prob = Heat1DForcedFiredrake(n=2)
56+
hierarchy = fd.MeshHierarchy(_prob.mesh, 1)
57+
probs = [Heat1DForcedFiredrake(mesh=mesh) for mesh in hierarchy]
58+
59+
# prepare data that we can interpolate exactly in this discretization
60+
functions = [fd.Function(me.V).interpolate(me.x**2) for me in probs]
61+
data = [me.u_init for me in probs]
62+
[data[i].assign(functions[i]) for i in range(len(functions))]
63+
rhs = [me.f_init for me in probs]
64+
[rhs[i].impl.assign(functions[i]) for i in range(len(functions))]
65+
[rhs[i].expl.assign(functions[i]) for i in range(len(functions))]
66+
67+
# setup transfer class
68+
transfer = MeshToMeshFiredrakeHierarchy(*probs[::-1], {})
69+
70+
# test that restriction and interpolation were indeed exact on the mesh
71+
restriction = transfer.restrict(data[1])
72+
error = abs(restriction - data[0])
73+
assert np.isclose(error, 0), f'Got unexpectedly large {error=} during restriction!'
74+
75+
interpolation = transfer.prolong(data[0])
76+
error = abs(interpolation - data[1])
77+
assert np.isclose(error, 0), f'Got unexpectedly large {error=} during interpolation!'
78+
79+
# test that restriction and interpolation were indeed exact on the IMEX mesh
80+
restriction = transfer.restrict(rhs[1])
81+
error = max([abs(restriction.impl - rhs[0].impl), abs(restriction.expl - rhs[0].expl)])
82+
assert np.isclose(error, 0), f'Got unexpectedly large {error=} during restriction!'
83+
84+
interpolation = transfer.prolong(rhs[0])
85+
error = max([abs(interpolation.impl - rhs[1].impl), abs(interpolation.expl - rhs[1].expl)])
86+
assert np.isclose(error, 0), f'Got unexpectedly large {error=} during interpolation!'
87+
88+
89+
if __name__ == '__main__':
90+
test_Firedrake_transfer_hierarchy()

pySDC/tests/test_tutorials/test_step_7.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,11 @@ def test_D():
130130

131131

132132
@pytest.mark.firedrake
133-
def test_E():
133+
@pytest.mark.parametrize('ML', [True, False])
134+
def test_E(ML):
134135
from pySDC.tutorial.step_7.E_pySDC_with_Firedrake import runHeatFiredrake
135136

136-
runHeatFiredrake(useMPIsweeper=False)
137+
runHeatFiredrake(useMPIsweeper=False, ML=ML)
137138

138139

139140
@pytest.mark.firedrake
@@ -142,7 +143,7 @@ def test_E_MPI():
142143
my_env['COVERAGE_PROCESS_START'] = 'pyproject.toml'
143144
cwd = '.'
144145
num_procs = 3
145-
cmd = f'mpiexec -np {num_procs} python pySDC/tutorial/step_7/E_pySDC_with_Firedrake.py'.split()
146+
cmd = f'mpiexec -np {num_procs} python pySDC/tutorial/step_7/E_pySDC_with_Firedrake.py --useMPIsweeper'.split()
146147

147148
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=my_env, cwd=cwd)
148149
p.wait()
@@ -179,3 +180,32 @@ def test_F():
179180
assert (
180181
error < 1e-8
181182
), f'Unexpectedly large difference of {error} between pySDC and Gusto SDC implementations in Williamson 5 test case'
183+
184+
185+
@pytest.mark.firedrake
186+
def test_F_ML():
187+
"""
188+
Test that the Gusto coupling with multiple levels in space converges
189+
"""
190+
from pySDC.tutorial.step_7.F_pySDC_with_Gusto import williamson_5
191+
from pySDC.helpers.stats_helper import get_sorted, filter_stats
192+
import sys
193+
194+
if '--running-tests' not in sys.argv:
195+
sys.argv += ['--running-tests']
196+
197+
params = {'use_pySDC': True, 'dt': 1000, 'tmax': 1000, 'use_adaptivity': False, 'M': 2, 'kmax': 4, 'QI': 'LU'}
198+
stepper_ML, _ = williamson_5(Nlevels=2, **params)
199+
stepper_SL, _ = williamson_5(Nlevels=1, **params)
200+
201+
stats = stepper_ML.scheme.stats
202+
residual_fine = get_sorted(stats, type='residual_post_sweep', level=0, sortby='iter')
203+
residual_coarse = get_sorted(stats, type='residual_post_sweep', level=1, sortby='iter')
204+
assert residual_fine[0][1] / residual_fine[-1][1] > 3e2, 'Fine residual did not converge as expected'
205+
assert residual_coarse[0][1] / residual_coarse[-1][1] > 3e2, 'Coarse residual did not converge as expected'
206+
207+
stats_SL = stepper_SL.scheme.stats
208+
residual_SL = get_sorted(stats_SL, type='residual_post_sweep', sortby='iter')
209+
assert all(
210+
res_SL > res_ML for res_SL, res_ML in zip(residual_SL, residual_fine)
211+
), 'Single level SDC converged faster than multi-level!'

0 commit comments

Comments
 (0)