Coverage for pySDC/tutorial/step_9/B_paradiag_for_nonlinear_problems.py: 99%
83 statements
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-21 06:49 +0000
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-21 06:49 +0000
1"""
2This script introduces ParaDiag for nonlinear problems with the van der Pol oscillator as an example.
4ParaDiag works by diagonalizing the "top layer" of Kronecker products that make up the circularized composite
5collocation problem.
6However, in nonlinear problems, the problem cannot be written as a matrix and therefore we cannot write the composite
7collocation problem as a matrix.
8There are two approaches for dealing with this. We can do IMEX splitting, where we treat only the linear part implicitly.
9The ParaDiag preconditioner is then only made up of the linear implicit part and we can again write this as a matrix and
10do the diagonalization just like for linear problems. The non-linear part then comes in via the residual on the right
11hand side.
12The second approach is to average Jacobians. The non-linear problems are solved with a Newton scheme, where the Jacobian
13matrix is computed based on the current solution and then inverted in each Newton iteration. In order to write the
14ParaDiag preconditioner as a matrix with Kronecker products and then only diagonalize the outermost part, we need to
15have the same Jacobian on all steps.
16The ParaDiag iteration then proceeds as follows:
17 - (1) Compute residual of composite collocation problem
18 - (2) Average the solution across the steps and nodes as preparation for computing the average Jacobian
19 - (3) Weighted FFT in time to diagonalize E_alpha
20 - (4) Solve for the increment by inverting the averaged Jacobian from (2) on the subproblems on the different steps
21 and nodes.
22 - (5) Weighted iFFT in time
23 - (6) Increment solution
24As IMEX ParaDiag is a trivial extension of ParaDiag for linear problems, we focus on the second approach here.
25"""
27import numpy as np
28import scipy.sparse as sp
29import sys
31from pySDC.implementations.sweeper_classes.generic_implicit import generic_implicit as sweeper_class
32from pySDC.implementations.problem_classes.Van_der_Pol_implicit import vanderpol
34# setup output
35out_file = open('data/step_9_B_out.txt', 'w')
38def my_print(*args, **kwargs):
39 for output in [sys.stdout, out_file]:
40 print(*args, **kwargs, file=output)
43# setup parameters
44L = 4
45M = 3
46alpha = 1e-4
47restol = 1e-8
48dt = 0.1
50# setup infrastructure
51prob = vanderpol(newton_maxiter=1, mu=1e0, crash_at_maxiter=False)
52N = prob.init[0]
54# make problem work on complex data
55prob.init = tuple([*prob.init[:2]] + [np.dtype('complex128')])
57# setup global solution array
58u = np.zeros((L, M, N), dtype=complex)
60# setup collocation problem
61sweep = sweeper_class({'num_nodes': M, 'quad_type': 'RADAU-RIGHT'}, None)
63# initial conditions
64u[0, :, :] = prob.u_exact(t=0)
66my_print(
67 f'Running ParaDiag test script for van der Pol with mu={prob.mu} and {L} time steps and {M} collocation nodes.'
68)
71"""
72Setup matrices that make up the composite collocation problem. We do not set up the full composite collocation problem
73here, however. See https://arxiv.org/abs/2103.12571 for the meaning of the matrices.
74"""
75I_M = sp.eye(M)
77H_M = sp.eye(M).tolil() * 0
78H_M[:, -1] = 1
80Q = sweep.coll.Qmat[1:, 1:]
82E_alpha = sp.diags(
83 [
84 -1.0,
85 ]
86 * (L - 1),
87 offsets=-1,
88).tolil()
89E_alpha[0, -1] = -alpha
91gamma = alpha ** (-np.arange(L) / L)
92D_alpha_diag_vals = np.fft.fft(1 / gamma * E_alpha[:, 0].toarray().flatten(), norm='backward')
94J = sp.diags(gamma)
95J_inv = sp.diags(1 / gamma)
97G = [(D_alpha_diag_vals[l] * H_M + I_M).tocsc() for l in range(L)] # MxM
99# prepare diagonalization of QG^{-1}
100w = []
101S = []
102S_inv = []
104for l in range(L):
105 # diagonalize QG^-1 matrix
106 if M > 1:
107 _w, _S = np.linalg.eig(Q @ sp.linalg.inv(G[l]).toarray())
108 else:
109 _w, _S = np.linalg.eig(Q / (G[l].toarray()))
110 _S_inv = np.linalg.inv(_S)
111 w.append(_w)
112 S.append(_S)
113 S_inv.append(_S_inv)
115"""
116Setup functions for computing matrix-vector productions on the steps and for computing the residual of the composite
117collocation problem
118"""
121def mat_vec(mat, vec):
122 """
123 Matrix vector product
125 Args:
126 mat (np.ndarray or scipy.sparse) : Matrix
127 vec (np.ndarray) : vector
129 Returns:
130 np.ndarray: mat @ vec
131 """
132 res = np.zeros_like(vec)
133 for l in range(vec.shape[0]):
134 for k in range(vec.shape[0]):
135 res[l] += mat[l, k] * vec[k]
136 return res
139def residual(_u, u0):
140 """
141 Compute the residual of the composite collocation problem
143 Args:
144 _u (np.ndarray): Current iterate
145 u0 (np.ndarray): Initial conditions
147 Returns:
148 np.ndarray: LMN size array with the residual
149 """
150 res = _u * 0j
151 for l in range(L):
152 # build step local residual
154 # communicate initial conditions for each step
155 if l == 0:
156 res[l, ...] = u0[l, ...]
157 else:
158 res[l, ...] = _u[l - 1, -1, ...]
160 # evaluate and subtract integral over right hand side functions
161 f_evals = np.array([prob.eval_f(_u[l, m], 0) for m in range(M)])
162 Qf = mat_vec(Q, f_evals)
163 res[l, ...] -= _u[l] - dt * Qf
165 return res
168# do ParaDiag
169sol_paradiag = u.copy() * 0j
170u0 = u.copy()
171niter = 0
172res = residual(sol_paradiag, u0)
173while np.max(np.abs(res)) > restol:
174 # compute all-at-once residual
175 res = residual(sol_paradiag, u0)
177 # compute solution averaged across the L steps and M nodes. This is the difference to ParaDiag for linear problems.
178 u_avg = prob.u_init
179 u_avg[:] = np.mean(sol_paradiag, axis=(0, 1))
181 # weighted FFT in time
182 x = np.fft.fft(mat_vec(J_inv.toarray(), res), axis=0)
184 # perform local solves of "collocation problems" on the steps in parallel
185 y = np.empty_like(x)
186 for l in range(L):
188 # perform local solves on the collocation nodes in parallel
189 x1 = S_inv[l] @ x[l]
190 x2 = np.empty_like(x1)
191 for m in range(M):
192 x2[m, :] = prob.solve_jacobian(x1[m], w[l][m] * dt, u=u_avg, t=l * dt)
193 z = S[l] @ x2
194 y[l, ...] = sp.linalg.spsolve(G[l], z)
196 # inverse FFT in time and increment
197 sol_paradiag += mat_vec(J.toarray(), np.fft.ifft(y, axis=0))
199 res = residual(sol_paradiag, u0)
200 niter += 1
201 assert niter < 99, 'ParaDiag did not converge for nonlinear problem!'
202my_print(f'Needed {niter} ParaDiag iterations, stopped at residual {np.max(np.abs(res)):.2e}')