Coverage for pySDC/implementations/problem_classes/generic_spectral.py: 56%
147 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-20 14:51 +0000
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-20 14:51 +0000
1from pySDC.core.problem import Problem, WorkCounter
2from pySDC.helpers.spectral_helper import SpectralHelper
3import numpy as np
4from pySDC.core.errors import ParameterError
7class GenericSpectralLinear(Problem):
8 """
9 Generic class to solve problems of the form M u_t + L u = y, with mass matrix M, linear operator L and some right
10 hand side y using spectral methods.
11 L may contain algebraic conditions, as long as (M + dt L) is invertible.
13 Note that the `__getattr__` method is overloaded to pass requests on to the spectral helper if they are not
14 attributes of this class itself. For instance, you can add a BC by calling `self.spectral.add_BC` or equivalently
15 `self.add_BC`.
17 You can port problems derived from this more or less seamlessly to GPU by using the numerical libraries that are
18 class attributes of the spectral helper. This class will automatically switch the datatype using the `setup_GPU` class method.
20 Attributes:
21 spectral (pySDC.helpers.spectral_helper.SpectralHelper): Spectral helper
22 work_counters (dict): Dictionary for counting work
23 cached_factorizations (dict): Dictionary of cached matrix factorizations for solving
24 L (sparse matrix): Linear operator
25 M (sparse matrix): Mass matrix
26 diff_mask (list): Mask for separating differential and algebraic terms
27 Pl (sparse matrix): Left preconditioner
28 Pr (sparse matrix): Right preconditioner
29 """
31 @classmethod
32 def setup_GPU(cls):
33 """switch to GPU modules"""
34 import cupy as cp
35 from pySDC.implementations.datatype_classes.cupy_mesh import cupy_mesh, imex_cupy_mesh
36 from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh
38 cls.dtype_u = cupy_mesh
40 GPU_versions = {
41 mesh: cupy_mesh,
42 imex_mesh: imex_cupy_mesh,
43 }
45 cls.dtype_f = GPU_versions[cls.dtype_f]
47 def __init__(
48 self,
49 bases,
50 components,
51 comm=None,
52 Dirichlet_recombination=True,
53 left_preconditioner=True,
54 solver_type='cached_direct',
55 solver_args=None,
56 useGPU=False,
57 max_cached_factorizations=12,
58 spectral_space=True,
59 real_spectral_coefficients=False,
60 debug=False,
61 ):
62 """
63 Base class for problems discretized with spectral methods.
65 Args:
66 bases (list of dictionaries): 1D Bases
67 components (list of strings): Components of the equations
68 comm (mpi4py.Intracomm or None): MPI communicator
69 Dirichlet_recombination (bool): Use Dirichlet recombination in the last axis as right preconditioner
70 left_preconditioner (bool): Reverse the Kronecker product if yes
71 solver_type (str): Solver for linear systems
72 solver_args (dict): Arguments for linear solver
73 useGPU (bool): Run on GPU or CPU
74 max_cached_factorizations (int): Number of matrix decompositions to cache before starting eviction
75 spectral_space (bool): If yes, the solution will not be transformed back after solving and evaluating the RHS, and is expected as input in spectral space to these functions
76 real_spectral_coefficients (bool): If yes, allow only real values in spectral space, otherwise, allow complex.
77 debug (bool): Make additional tests at extra computational cost
78 """
79 solver_args = {} if solver_args is None else solver_args
80 self._makeAttributeAndRegister(
81 'max_cached_factorizations',
82 'useGPU',
83 'solver_type',
84 'solver_args',
85 'left_preconditioner',
86 'Dirichlet_recombination',
87 'comm',
88 'spectral_space',
89 'real_spectral_coefficients',
90 'debug',
91 localVars=locals(),
92 )
93 self.spectral = SpectralHelper(comm=comm, useGPU=useGPU, debug=debug)
95 if useGPU:
96 self.setup_GPU()
98 for base in bases:
99 self.spectral.add_axis(**base)
100 self.spectral.add_component(components)
102 self.spectral.setup_fft(real_spectral_coefficients)
104 super().__init__(init=self.spectral.init_forward if spectral_space else self.spectral.init)
106 self.work_counters[solver_type] = WorkCounter()
107 self.work_counters['factorizations'] = WorkCounter()
109 self.setup_preconditioner(Dirichlet_recombination, left_preconditioner)
111 self.cached_factorizations = {}
113 def __getattr__(self, name):
114 """
115 Pass requests on to the helper if they are not directly attributes of this class for convenience.
117 Args:
118 name (str): Name of the attribute you want
120 Returns:
121 request
122 """
123 return getattr(self.spectral, name)
125 def _setup_operator(self, LHS):
126 """
127 Setup a sparse linear operator by adding relationships. See documentation for ``GenericSpectralLinear.setup_L`` to learn more.
129 Args:
130 LHS (dict): Equations to be added to the operator
132 Returns:
133 sparse linear operator
134 """
135 operator = self.spectral.get_empty_operator_matrix()
136 for line, equation in LHS.items():
137 self.spectral.add_equation_lhs(operator, line, equation)
138 return self.spectral.convert_operator_matrix_to_operator(operator)
140 def setup_L(self, LHS):
141 """
142 Setup the left hand side of the linear operator L and store it in ``self.L``.
144 The argument is meant to be a dictionary with the line you want to write the equation in as the key and the relationship between components as another dictionary. For instance, you can add an algebraic condition capturing a first derivative relationship between u and ux as follows:
146 ```
147 Dx = self.get_differentiation_matrix(axes=(0,))
148 I = self.get_Id()
149 LHS = {'ux': {'u': Dx, 'ux': -I}}
150 self.setup_L(LHS)
151 ```
153 If you put zero as right hand side for the solver in the line for ux, ux will contain the x-derivative of u afterwards.
155 Args:
156 LHS (dict): Dictionary containing the equations.
157 """
158 self.L = self._setup_operator(LHS)
160 def setup_M(self, LHS):
161 '''
162 Setup mass matrix, see documentation of ``GenericSpectralLinear.setup_L``.
163 '''
164 diff_index = list(LHS.keys())
165 self.diff_mask = [me in diff_index for me in self.components]
166 self.M = self._setup_operator(LHS)
168 def setup_preconditioner(self, Dirichlet_recombination=True, left_preconditioner=True):
169 """
170 Get left and right preconditioners.
172 Args:
173 Dirichlet_recombination (bool): Basis conversion for right preconditioner. Useful for Chebychev and Ultraspherical methods. 10/10 would recommend.
174 left_preconditioner (bool): If True, it will interleave the variables and reverse the Kronecker product
175 """
176 sp = self.spectral.sparse_lib
177 N = np.prod(self.init[0][1:])
179 Id = sp.eye(N)
180 Pl_lhs = {comp: {comp: Id} for comp in self.components}
181 self.Pl = self._setup_operator(Pl_lhs)
183 if left_preconditioner:
184 # reverse Kronecker product
186 if self.spectral.useGPU:
187 R = self.Pl.get().tolil() * 0
188 else:
189 R = self.Pl.tolil() * 0
191 for j in range(self.ncomponents):
192 for i in range(N):
193 R[i * self.ncomponents + j, j * N + i] = 1.0
195 self.Pl = self.spectral.sparse_lib.csc_matrix(R)
197 if Dirichlet_recombination and type(self.axes[-1]).__name__ in ['ChebychevHelper, Ultraspherical']:
198 _Pr = self.spectral.get_Dirichlet_recombination_matrix(axis=-1)
199 else:
200 _Pr = Id
202 Pr_lhs = {comp: {comp: _Pr} for comp in self.components}
203 self.Pr = self._setup_operator(Pr_lhs) @ self.Pl.T
205 def solve_system(self, rhs, dt, u0=None, *args, skip_itransform=False, **kwargs):
206 """
207 Do an implicit Euler step to solve M u_t + Lu = rhs, with M the mass matrix and L the linear operator as setup by
208 ``GenericSpectralLinear.setup_L`` and ``GenericSpectralLinear.setup_M``.
210 The implicit Euler step is (M - dt L) u = M rhs. Note that M need not be invertible as long as (M + dt*L) is.
211 This means solving with dt=0 to mimic explicit methods does not work for all problems, in particular simple DAEs.
213 Note that by putting M rhs on the right hand side, this function can only solve algebraic conditions equal to
214 zero. If you want something else, it should be easy to overload this function.
215 """
217 sp = self.spectral.sparse_lib
219 if self.spectral_space:
220 rhs_hat = rhs.copy()
221 else:
222 rhs_hat = self.spectral.transform(rhs)
224 rhs_hat = (self.M @ rhs_hat.flatten()).reshape(rhs_hat.shape)
225 rhs_hat = self.spectral.put_BCs_in_rhs_hat(rhs_hat)
226 rhs_hat = self.Pl @ rhs_hat.flatten()
228 if dt not in self.cached_factorizations.keys():
229 A = self.M + dt * self.L
230 A = self.Pl @ self.spectral.put_BCs_in_matrix(A) @ self.Pr
232 # import numpy as np
233 # if A.shape[0] < 200:
234 # import matplotlib.pyplot as plt
236 # # M = self.spectral.put_BCs_in_matrix(self.L.copy())
237 # M = A # self.L
238 # im = plt.imshow((M / abs(M)).real)
239 # # im = plt.imshow(np.log10(abs(A.toarray())).real)
240 # # im = plt.imshow(((A.toarray())).real)
241 # plt.colorbar(im)
242 # plt.show()
244 if self.solver_type.lower() == 'cached_direct':
245 if dt not in self.cached_factorizations.keys():
246 if len(self.cached_factorizations) >= self.max_cached_factorizations:
247 self.cached_factorizations.pop(list(self.cached_factorizations.keys())[0])
248 self.logger.debug(f'Evicted matrix factorization for {dt=:.6f} from cache')
249 self.cached_factorizations[dt] = self.spectral.linalg.factorized(A)
250 self.logger.debug(f'Cached matrix factorization for {dt=:.6f}')
251 self.work_counters['factorizations']()
253 _sol_hat = self.cached_factorizations[dt](rhs_hat)
254 self.logger.debug(f'Used cached matrix factorization for {dt=:.6f}')
256 elif self.solver_type.lower() == 'direct':
257 _sol_hat = sp.linalg.spsolve(A, rhs_hat)
258 elif self.solver_type.lower() == 'gmres':
259 _sol_hat, _ = sp.linalg.gmres(
260 A,
261 rhs_hat,
262 x0=u0.flatten(),
263 **self.solver_args,
264 callback=self.work_counters[self.solver_type],
265 callback_type='legacy',
266 )
267 elif self.solver_type.lower() == 'cg':
268 _sol_hat, _ = sp.linalg.cg(
269 A, rhs_hat, x0=u0.flatten(), **self.solver_args, callback=self.work_counters[self.solver_type]
270 )
271 else:
272 raise NotImplementedError(f'Solver {self.solver_type:!} not implemented in {type(self).__name__}!')
274 sol_hat = self.spectral.u_init_forward
275 sol_hat[...] = (self.Pr @ _sol_hat).reshape(sol_hat.shape)
277 if self.spectral_space:
278 return sol_hat
279 else:
280 sol = self.spectral.u_init
281 sol[:] = self.spectral.itransform(sol_hat).real
283 if self.spectral.debug:
284 self.spectral.check_BCs(sol)
286 return sol
289def compute_residual_DAE(self, stage=''):
290 """
291 Computation of the residual that does not add u_0 - u_m in algebraic equations.
293 Args:
294 stage (str): The current stage of the step the level belongs to
295 """
297 # get current level and problem description
298 L = self.level
300 # Check if we want to skip the residual computation to gain performance
301 # Keep in mind that skipping any residual computation is likely to give incorrect outputs of the residual!
302 if stage in self.params.skip_residual_computation:
303 L.status.residual = 0.0 if L.status.residual is None else L.status.residual
304 return None
306 # check if there are new values (e.g. from a sweep)
307 # assert L.status.updated
309 # compute the residual for each node
311 # build QF(u)
312 res_norm = []
313 res = self.integrate()
314 mask = L.prob.diff_mask
315 for m in range(self.coll.num_nodes):
316 res[m][mask] += L.u[0][mask] - L.u[m + 1][mask]
317 # add tau if associated
318 if L.tau[m] is not None:
319 res[m] += L.tau[m]
320 # use abs function from data type here
321 res_norm.append(abs(res[m]))
322 # print(m, [abs(me) for me in res[m]], [abs(me) for me in L.u[0] - L.u[m + 1]])
324 # find maximal residual over the nodes
325 if L.params.residual_type == 'full_abs':
326 L.status.residual = max(res_norm)
327 elif L.params.residual_type == 'last_abs':
328 L.status.residual = res_norm[-1]
329 elif L.params.residual_type == 'full_rel':
330 L.status.residual = max(res_norm) / abs(L.u[0])
331 elif L.params.residual_type == 'last_rel':
332 L.status.residual = res_norm[-1] / abs(L.u[0])
333 else:
334 raise ParameterError(
335 f'residual_type = {L.params.residual_type} not implemented, choose '
336 f'full_abs, last_abs, full_rel or last_rel instead'
337 )
339 # indicate that the residual has seen the new values
340 L.status.updated = False
342 return None
345def compute_residual_DAE_MPI(self, stage=None):
346 """
347 Computation of the residual using the collocation matrix Q
349 Args:
350 stage (str): The current stage of the step the level belongs to
351 """
352 from mpi4py import MPI
354 L = self.level
356 # Check if we want to skip the residual computation to gain performance
357 # Keep in mind that skipping any residual computation is likely to give incorrect outputs of the residual!
358 if stage in self.params.skip_residual_computation:
359 L.status.residual = 0.0 if L.status.residual is None else L.status.residual
360 return None
362 # compute the residual for each node
364 # build QF(u)
365 res = self.integrate(last_only=L.params.residual_type[:4] == 'last')
366 mask = L.prob.diff_mask
367 res[mask] += L.u[0][mask] - L.u[self.rank + 1][mask]
368 # add tau if associated
369 if L.tau[self.rank] is not None:
370 res += L.tau[self.rank]
371 # use abs function from data type here
372 res_norm = abs(res)
374 # find maximal residual over the nodes
375 if L.params.residual_type == 'full_abs':
376 L.status.residual = self.comm.allreduce(res_norm, op=MPI.MAX)
377 elif L.params.residual_type == 'last_abs':
378 L.status.residual = self.comm.bcast(res_norm, root=self.comm.size - 1)
379 elif L.params.residual_type == 'full_rel':
380 L.status.residual = self.comm.allreduce(res_norm / abs(L.u[0]), op=MPI.MAX)
381 elif L.params.residual_type == 'last_rel':
382 L.status.residual = self.comm.bcast(res_norm / abs(L.u[0]), root=self.comm.size - 1)
383 else:
384 raise NotImplementedError(f'residual type \"{L.params.residual_type}\" not implemented!')
386 # indicate that the residual has seen the new values
387 L.status.updated = False
389 return None
392def get_extrapolated_error_DAE(self, S, **kwargs):
393 """
394 The extrapolation estimate combines values of u and f from multiple steps to extrapolate and compare to the
395 solution obtained by the time marching scheme. This function can be used in `EstimateExtrapolationError`.
397 Args:
398 S (pySDC.Step): The current step
400 Returns:
401 None
402 """
403 u_ex = self.get_extrapolated_solution(S)
404 diff_mask = S.levels[0].prob.diff_mask
405 if u_ex is not None:
406 S.levels[0].status.error_extrapolation_estimate = (
407 abs((u_ex - S.levels[0].u[-1])[diff_mask]) * self.coeff.prefactor
408 )
409 # print([abs(me) for me in (u_ex - S.levels[0].u[-1]) * self.coeff.prefactor])
410 else:
411 S.levels[0].status.error_extrapolation_estimate = None