Skip to content

Commit 23ef9d3

Browse files
Serial implementation for ParaDiag with collocation methods (#530)
* Added serial implementation for ParaDiag for collocation methods * Removed some old comments * Added test that ParaDiag converges within the bounds for Dahlquist problem * Fix test on older Python versions * Added smaller values of alpha to the tests * Implemented separate Jacobian inversion to fix bug pointed out by @JHopeCollins * Update pySDC/tutorial/step_9/B_paradiag_for_nonlinear_problems.py Co-authored-by: Josh Hope-Collins <[email protected]> * Keep spatially extended residual in the levels after computation * Refactored increment formulation --------- Co-authored-by: Josh Hope-Collins <[email protected]>
1 parent 0eaff39 commit 23ef9d3

File tree

21 files changed

+2023
-12
lines changed

21 files changed

+2023
-12
lines changed

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Tutorial
2525
tutorial/step_6.rst
2626
tutorial/step_7.rst
2727
tutorial/step_8.rst
28+
tutorial/step_9.rst
2829

2930
Playgrounds
3031
-----------
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Full code: `pySDC/tutorial/step_9/A_paradiag_for_linear_problems.py <https://github.com/Parallel-in-Time/pySDC/blob/master/pySDC/tutorial/step_9/A_paradiag_for_linear_problems.py>`_
2+
3+
.. literalinclude:: ../../../pySDC/tutorial/step_9/A_paradiag_for_linear_problems.py
4+
5+
Results:
6+
7+
.. literalinclude:: ../../../data/step_9_A_out.txt
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Full code: `pySDC/tutorial/step_9/B_paradiag_for_nonlinear_problems.py <https://github.com/Parallel-in-Time/pySDC/blob/master/pySDC/tutorial/step_9/B_paradiag_for_nonlinear_problems.py>`_
2+
3+
.. literalinclude:: ../../../pySDC/tutorial/step_9/B_paradiag_for_nonlinear_problems.py
4+
5+
Results:
6+
7+
.. literalinclude:: ../../../data/step_9_B_out.txt
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Full code: `pySDC/tutorial/step_9/C_paradiag_in_pySDC.py <https://github.com/Parallel-in-Time/pySDC/blob/master/pySDC/tutorial/step_9/C_paradiag_in_pySDC.py>`_
2+
3+
.. literalinclude:: ../../../pySDC/tutorial/step_9/C_paradiag_in_pySDC.py
4+
5+
Results:
6+
7+
.. literalinclude:: ../../../data/step_9_C_out.txt

docs/source/tutorial/step_9.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.. include:: /../../pySDC/tutorial/step_9/README.rst

pySDC/core/controller.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(self, controller_params, description, useMPI=None):
4141
controller_params (dict): parameter set for the controller and the steps
4242
"""
4343
self.useMPI = useMPI
44+
self.description = description
4445

4546
# check if we have a hook on this list. If not, use default class.
4647
self.__hooks = []
@@ -341,3 +342,65 @@ def return_stats(self):
341342
for hook in self.hooks:
342343
stats = {**stats, **hook.return_stats()}
343344
return stats
345+
346+
347+
class ParaDiagController(Controller):
348+
349+
def __init__(self, controller_params, description, n_steps, useMPI=None):
350+
"""
351+
Initialization routine for ParaDiag controllers
352+
353+
Args:
354+
num_procs: number of parallel time steps (still serial, though), can be 1
355+
controller_params: parameter set for the controller and the steps
356+
description: all the parameters to set up the rest (levels, problems, transfer, ...)
357+
n_steps (int): Number of parallel steps
358+
alpha (float): alpha parameter for ParaDiag
359+
"""
360+
from pySDC.implementations.sweeper_classes.ParaDiagSweepers import QDiagonalization
361+
362+
if QDiagonalization in description['sweeper_class'].__mro__:
363+
description['sweeper_params']['ignore_ic'] = True
364+
description['sweeper_params']['update_f_evals'] = False
365+
else:
366+
logging.getLogger('controller').warning(
367+
f'Warning: Your sweeper class {description["sweeper_class"]} is not derived from {QDiagonalization}. You probably want to use another sweeper class.'
368+
)
369+
370+
if controller_params.get('all_to_done', False):
371+
raise NotImplementedError('ParaDiag only implemented with option `all_to_done=True`')
372+
if 'alpha' not in controller_params.keys():
373+
from pySDC.core.errors import ParameterError
374+
375+
raise ParameterError('Please supply alpha as a parameter to the ParaDiag controller!')
376+
controller_params['average_jacobian'] = controller_params.get('average_jacobian', True)
377+
378+
controller_params['all_to_done'] = True
379+
super().__init__(controller_params=controller_params, description=description, useMPI=useMPI)
380+
381+
self.n_steps = n_steps
382+
383+
def FFT_in_time(self, quantity):
384+
"""
385+
Compute weighted forward FFT in time. The weighting is determined by the alpha parameter in ParaDiag
386+
387+
Note: The implementation via matrix-vector multiplication may be inefficient and less stable compared to an FFT
388+
with transposes!
389+
"""
390+
if not hasattr(self, '__FFT_matrix'):
391+
from pySDC.helpers.ParaDiagHelper import get_weighted_FFT_matrix
392+
393+
self.__FFT_matrix = get_weighted_FFT_matrix(self.n_steps, self.params.alpha)
394+
395+
self.apply_matrix(self.__FFT_matrix, quantity)
396+
397+
def iFFT_in_time(self, quantity):
398+
"""
399+
Compute weighted backward FFT in time. The weighting is determined by the alpha parameter in ParaDiag
400+
"""
401+
if not hasattr(self, '__iFFT_matrix'):
402+
from pySDC.helpers.ParaDiagHelper import get_weighted_iFFT_matrix
403+
404+
self.__iFFT_matrix = get_weighted_iFFT_matrix(self.n_steps, self.params.alpha)
405+
406+
self.apply_matrix(self.__iFFT_matrix, quantity)

pySDC/core/level.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ def __init__(self, problem_class, problem_params, sweeper_class, sweeper_params,
8282
self.uend = None
8383
self.u = [None] * (self.sweep.coll.num_nodes + 1)
8484
self.uold = [None] * (self.sweep.coll.num_nodes + 1)
85+
self.u_avg = [None] * self.sweep.coll.num_nodes
86+
self.residual = [None] * self.sweep.coll.num_nodes
87+
self.increment = [None] * self.sweep.coll.num_nodes
8588
self.f = [None] * (self.sweep.coll.num_nodes + 1)
8689
self.fold = [None] * (self.sweep.coll.num_nodes + 1)
8790

pySDC/core/problem.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,35 @@ def plot(self, u, t=None, fig=None):
176176
None
177177
"""
178178
raise NotImplementedError
179+
180+
def solve_system(self, rhs, dt, u0, t):
181+
"""
182+
Perform an Euler step.
183+
184+
Args:
185+
rhs: Right hand side for the Euler step
186+
dt (float): Step size for the Euler step
187+
u0: Initial guess
188+
t (float): Current time
189+
190+
Returns:
191+
solution to the Euler step
192+
"""
193+
raise NotImplementedError
194+
195+
def solve_jacobian(self, rhs, dt, u=None, u0=None, t=0, **kwargs):
196+
"""
197+
Solve the Jacobian for an Euler step, linearized around u.
198+
This defaults to an Euler step to accommodate linear problems.
199+
200+
Args:
201+
rhs: Right hand side for the Euler step
202+
dt (float): Step size for the Euler step
203+
u: Solution to linearize around
204+
u0: Initial guess
205+
t (float): Current time
206+
207+
Returns:
208+
Solution
209+
"""
210+
return self.solve_system(rhs, dt, u0=u, t=t, **kwargs)

pySDC/core/sweeper.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,14 @@ def compute_residual(self, stage=''):
192192

193193
# build QF(u)
194194
res_norm = []
195-
res = self.integrate()
195+
L.residual = self.integrate()
196196
for m in range(self.coll.num_nodes):
197-
res[m] += L.u[0] - L.u[m + 1]
197+
L.residual[m] += L.u[0] - L.u[m + 1]
198198
# add tau if associated
199199
if L.tau[m] is not None:
200-
res[m] += L.tau[m]
200+
L.residual[m] += L.tau[m]
201201
# use abs function from data type here
202-
res_norm.append(abs(res[m]))
202+
res_norm.append(abs(L.residual[m]))
203203

204204
# find maximal residual over the nodes
205205
if L.params.residual_type == 'full_abs':

pySDC/helpers/ParaDiagHelper.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import numpy as np
2+
import scipy.sparse as sp
3+
4+
5+
def get_FFT_matrix(N):
6+
"""
7+
Get matrix for computing FFT of size N. Normalization is like "ortho" in numpy.
8+
Compute inverse FFT by multiplying by the complex conjugate (numpy.conjugate) of this matrix
9+
10+
Args:
11+
N (int): Size of the data to be transformed
12+
13+
Returns:
14+
numpy.ndarray: Dense square matrix to compute forward transform
15+
"""
16+
idx_1d = np.arange(N, dtype=complex)
17+
i1, i2 = np.meshgrid(idx_1d, idx_1d)
18+
19+
return np.exp(-2 * np.pi * 1j * i1 * i2 / N) / np.sqrt(N)
20+
21+
22+
def get_E_matrix(N, alpha=0):
23+
"""
24+
Get NxN matrix with -1 on the lower subdiagonal, -alpha in the top right and 0 elsewhere
25+
26+
Args:
27+
N (int): Size of the matrix
28+
alpha (float): Negative of value in the top right
29+
30+
Returns:
31+
sparse E matrix
32+
"""
33+
E = sp.diags(
34+
[
35+
-1.0,
36+
]
37+
* (N - 1),
38+
offsets=-1,
39+
).tolil()
40+
E[0, -1] = -alpha
41+
return E
42+
43+
44+
def get_J_matrix(N, alpha):
45+
"""
46+
Get matrix for weights in the weighted inverse FFT
47+
48+
Args:
49+
N (int): Size of the matrix
50+
alpha (float): alpha parameter in ParaDiag
51+
52+
Returns:
53+
sparse J matrix
54+
"""
55+
gamma = alpha ** (-np.arange(N) / N)
56+
return sp.diags(gamma)
57+
58+
59+
def get_J_inv_matrix(N, alpha):
60+
"""
61+
Get matrix for weights in the weighted FFT
62+
63+
Args:
64+
N (int): Size of the matrix
65+
alpha (float): alpha parameter in ParaDiag
66+
67+
Returns:
68+
sparse J_inv matrix
69+
"""
70+
gamma = alpha ** (-np.arange(N) / N)
71+
return sp.diags(1 / gamma)
72+
73+
74+
def get_weighted_FFT_matrix(N, alpha):
75+
"""
76+
Get matrix for the weighted FFT
77+
78+
Args:
79+
N (int): Size of the matrix
80+
alpha (float): alpha parameter in ParaDiag
81+
82+
Returns:
83+
Dense weighted FFT matrix
84+
"""
85+
return get_FFT_matrix(N) @ get_J_inv_matrix(N, alpha)
86+
87+
88+
def get_weighted_iFFT_matrix(N, alpha):
89+
"""
90+
Get matrix for the weighted inverse FFT
91+
92+
Args:
93+
N (int): Size of the matrix
94+
alpha (float): alpha parameter in ParaDiag
95+
96+
Returns:
97+
Dense weighted FFT matrix
98+
"""
99+
return get_J_matrix(N, alpha) @ np.conjugate(get_FFT_matrix(N))
100+
101+
102+
def get_H_matrix(N, sweeper_params):
103+
"""
104+
Get sparse matrix for computing the collocation update. Requires not to do a collocation update!
105+
106+
Args:
107+
N (int): Number of collocation nodes
108+
sweeper_params (dict): Parameters for the sweeper
109+
110+
Returns:
111+
Sparse matrix for collocation update
112+
"""
113+
assert sweeper_params['quad_type'] == 'RADAU-RIGHT'
114+
H = sp.eye(N).tolil() * 0
115+
H[:, -1] = 1
116+
return H
117+
118+
119+
def get_G_inv_matrix(l, L, alpha, sweeper_params):
120+
M = sweeper_params['num_nodes']
121+
I_M = sp.eye(M)
122+
E_alpha = get_E_matrix(L, alpha)
123+
H = get_H_matrix(M, sweeper_params)
124+
125+
gamma = alpha ** (-np.arange(L) / L)
126+
diags = np.fft.fft(1 / gamma * E_alpha[:, 0].toarray().flatten(), norm='backward')
127+
G = (diags[l] * H + I_M).tocsc()
128+
if M > 1:
129+
return sp.linalg.inv(G).toarray()
130+
else:
131+
return 1 / G.toarray()

0 commit comments

Comments
 (0)