Coverage for pySDC/implementations/problem_classes/generic_MPIFFT_Laplacian.py: 100%
89 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
1import numpy as np
2from mpi4py import MPI
3from mpi4py_fft import newDistArray
5from pySDC.helpers.fft_helper import PFFT
7from pySDC.core.errors import ProblemError
8from pySDC.core.problem import Problem, WorkCounter
9from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh
12class IMEX_Laplacian_MPIFFT(Problem):
13 r"""
14 Generic base class for IMEX problems using a spectral method to solve the Laplacian implicitly and a possible rest
15 explicitly. The FFTs are done with``mpi4py-fft`` [1]_.
16 Works in two and three dimensions.
18 Parameters
19 ----------
20 nvars : tuple, optional
21 Spatial resolution
22 spectral : bool, optional
23 If True, the solution is computed in spectral space.
24 L : float, optional
25 Denotes the period of the function to be approximated for the Fourier transform.
26 alpha : float, optional
27 Multiplicative factor before the Laplacian
28 comm : MPI.COMM_World
29 Communicator for parallelisation.
30 dtype : numpy.dtype or str, optional
31 Datatype of the solution in real space, e.g. ``'d'`` for real or ``'D'`` for complex problems.
32 useGPU : bool, optional
33 Run on the GPU with CuPy and NCCL instead of on the CPU with NumPy and MPI.
34 x0 : float, optional
35 Coordinate of the left boundary of the domain in each direction.
37 Attributes
38 ----------
39 fft : PFFT
40 Object for parallel FFT transforms.
41 X : mesh-grid
42 Grid coordinates in real space.
43 K2 : matrix
44 Laplace operator in spectral space.
46 References
47 ----------
48 .. [1] Lisandro Dalcin, Mikael Mortensen, David E. Keyes. Fast parallel multidimensional FFT using advanced MPI.
49 Journal of Parallel and Distributed Computing (2019).
50 """
52 dtype_u = mesh
53 dtype_f = imex_mesh
55 xp = np
56 fft_backend = 'fftw'
57 fft_comm_backend = 'MPI'
59 def setup_GPU(self):
60 """switch to GPU modules"""
61 import cupy as cp
62 from pySDC.implementations.datatype_classes.cupy_mesh import cupy_mesh, imex_cupy_mesh
64 self.xp = cp
66 self.dtype_u = cupy_mesh
67 self.dtype_f = imex_cupy_mesh
69 self.fft_backend = 'cupy'
70 self.fft_comm_backend = 'NCCL'
72 def __init__(
73 self, nvars=None, spectral=False, L=2 * np.pi, alpha=1.0, comm=MPI.COMM_WORLD, dtype='d', useGPU=False, x0=0.0
74 ):
75 """Initialization routine"""
76 if useGPU:
77 self.setup_GPU()
79 if nvars is None:
80 nvars = (128, 128)
82 if not (isinstance(nvars, tuple) and len(nvars) > 1):
83 raise ProblemError('Need at least two dimensions for distributed FFTs')
85 # Creating FFT structure
86 self.ndim = len(nvars)
87 axes = tuple(range(self.ndim))
88 self.fft = PFFT(
89 comm,
90 list(nvars),
91 axes=axes,
92 dtype=dtype,
93 collapse=True,
94 backend=self.fft_backend,
95 comm_backend=self.fft_comm_backend,
96 grid=(-1,),
97 )
99 # get test data to figure out type and dimensions
100 tmp_u = newDistArray(self.fft, spectral)
102 L = np.array([L] * self.ndim, dtype=float)
104 # invoke super init, passing the communicator and the local dimensions as init
105 super().__init__(init=(tmp_u.shape, comm, tmp_u.dtype))
106 self._makeAttributeAndRegister(
107 'nvars', 'spectral', 'L', 'alpha', 'comm', 'x0', 'useGPU', localVars=locals(), readOnly=True
108 )
110 self.getLocalGrid()
111 self.getLaplacian()
113 # Need this for diagnostics
114 self.dx = self.L[0] / nvars[0]
115 self.dy = self.L[1] / nvars[1]
117 # work counters
118 self.work_counters['rhs'] = WorkCounter()
120 def getLocalGrid(self):
121 """
122 Compute the coordinates of the local part of the grid in real space and store them in ``self.X``, one array per
123 dimension broadcast to the local shape, with the points :math:`x_0 + i L / N`.
125 Returns
126 -------
127 None
128 """
129 X = list(self.xp.ogrid[self.fft.local_slice(False)])
130 N = self.fft.global_shape()
131 for i in range(len(N)):
132 X[i] = self.x0 + (X[i] * self.L[i] / N[i])
133 self.X = [self.xp.broadcast_to(x, self.fft.shape(False)) for x in X]
135 def getLaplacian(self):
136 r"""
137 Compute the squared wave numbers :math:`|k|^2` of the local part of the spectral grid, with the wave numbers
138 scaled by :math:`2\pi / L`, and store them in ``self.K2``. The Laplacian in spectral space is :math:`-|k|^2`.
140 Returns
141 -------
142 None
143 """
144 s = self.fft.local_slice()
145 N = self.fft.global_shape()
146 k = [self.xp.fft.fftfreq(n, 1.0 / n).astype(int) for n in N]
147 K = [ki[si] for ki, si in zip(k, s, strict=True)]
148 Ks = list(self.xp.meshgrid(*K, indexing='ij', sparse=True))
149 Lp = 2 * np.pi / self.L
150 for i in range(self.ndim):
151 Ks[i] = (Ks[i] * Lp[i]).astype(float)
152 K = [self.xp.broadcast_to(k, self.fft.shape(True)) for k in Ks]
153 K = self.xp.array(K).astype(float)
154 self.K2 = self.xp.sum(K * K, 0, dtype=float)
156 def eval_f(self, u, t):
157 """
158 Routine to evaluate the right-hand side of the problem.
160 Parameters
161 ----------
162 u : dtype_u
163 Current values of the numerical solution.
164 t : float
165 Current time at which the numerical solution is computed.
167 Returns
168 -------
169 f : dtype_f
170 The right-hand side of the problem.
171 """
173 f = self.dtype_f(self.init)
175 f.impl[:] = self._eval_Laplacian(u, f.impl)
177 if self.spectral:
178 tmp = self.fft.backward(u)
179 tmp[:] = self._eval_explicit_part(tmp, t, tmp)
180 f.expl[:] = self.fft.forward(tmp)
182 else:
183 f.expl[:] = self._eval_explicit_part(u, t, f.expl)
185 self.work_counters['rhs']()
186 return f
188 def _eval_Laplacian(self, u, f_impl, alpha=None):
189 alpha = alpha if alpha else self.alpha
190 if self.spectral:
191 f_impl[:] = -alpha * self.K2 * u
192 else:
193 u_hat = self.fft.forward(u)
194 lap_u_hat = -alpha * self.K2 * u_hat
195 f_impl[:] = self.fft.backward(lap_u_hat, f_impl)
196 return f_impl
198 def _eval_explicit_part(self, u, t, f_expl):
199 return f_expl
201 def solve_system(self, rhs, factor, u0, t):
202 """
203 Simple FFT solver for the diffusion part.
205 Parameters
206 ----------
207 rhs : dtype_f
208 Right-hand side for the linear system.
209 factor : float
210 Abbrev. for the node-to-node stepsize (or any other factor required).
211 u0 : dtype_u
212 Initial guess for the iterative solver (not used here so far).
213 t : float
214 Current time (e.g. for time-dependent BCs).
216 Returns
217 -------
218 me : dtype_u
219 The solution as mesh.
220 """
221 me = self.dtype_u(self.init)
222 me[:] = self._invert_Laplacian(me, factor, rhs)
224 return me
226 def _invert_Laplacian(self, me, factor, rhs, alpha=None):
227 alpha = alpha if alpha else self.alpha
228 if self.spectral:
229 me[:] = rhs / (1.0 + factor * alpha * self.K2)
231 else:
232 rhs_hat = self.fft.forward(rhs)
233 rhs_hat /= 1.0 + factor * alpha * self.K2
234 me[:] = self.fft.backward(rhs_hat)
235 return me