Coverage for pySDC/implementations/problem_classes/AllenCahn_Temp_MPIFFT.py: 100%

126 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 12:30 +0000

1import numpy as np 

2from pySDC.helpers.fft_helper import PFFT 

3 

4from pySDC.core.errors import ProblemError 

5from pySDC.core.problem import Problem, WorkCounter 

6from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh 

7 

8from mpi4py import MPI 

9from mpi4py_fft import newDistArray 

10 

11 

12class allencahn_temp_imex(Problem): 

13 r""" 

14 Periodic Allen-Cahn equation coupled to a temperature equation, mpi4py-fft FFTs, IMEX with both Laplacians implicit. 

15 

16 This class implements the :math:`N`-dimensional Allen-Cahn equation with periodic boundary conditions, with the two 

17 phases at :math:`u = 0` and :math:`u = 1` 

18 

19 .. math:: 

20 \frac{\partial u}{\partial t} = \Delta u - \frac{2}{\varepsilon^2} u (1 - u) (1 - 2u) 

21 - 6 d_w \frac{T - T_M}{T_M}u (1 - u), 

22 

23 .. math:: 

24 \frac{\partial T}{\partial t} = D \Delta T + \frac{\partial u}{\partial t} 

25 

26 for the phase field :math:`u` and the temperature :math:`T` on a spatial domain 

27 :math:`[-\frac{L}{2}, \frac{L}{2}]^N`, with driving force :math:`d_w`, and :math:`N=2,3`. :math:`D` and 

28 :math:`T_M` are fixed parameters. Different initial conditions can be used, for example, circles of the form 

29 

30 .. math:: 

31 u({\bf x}, 0) = \frac{1}{2}\left(1 + \tanh\left(\frac{r - \sqrt{x_i^2 + y_j^2}} 

32 {\sqrt{2}\varepsilon}\right)\right), 

33 

34 for :math:`i, j=0,..,N-1`, where :math:`N` is the number of spatial grid points. For time-stepping, the problem is treated 

35 *semi-implicitly*, i.e., the diffusion of both components is treated implicitly and solved by Fast Fourier Transform 

36 (FFT) using ``mpi4py-fft`` [1]_, and the reaction terms are treated explicitly. 

37 

38 Parameters 

39 ---------- 

40 nvars : tuple of int, optional 

41 Number of unknowns in each spatial direction, e.g. ``nvars=(128, 128)``. Has to be a tuple of at least two 

42 entries. 

43 eps : float, optional 

44 Scaling parameter :math:`\varepsilon`. For ``eps <= 0``, the reaction terms are dropped. 

45 radius : float, optional 

46 Radius of the circle for ``init_type='circle'``. 

47 spectral : bool, optional 

48 If True, the solution is computed in spectral space. 

49 TM : float, optional 

50 Reference temperature :math:`T_M` of the driving force, which vanishes where the temperature equals :math:`T_M`. 

51 D : float, optional 

52 Diffusion coefficient :math:`D` of the temperature, the second component of the solution. The phase field 

53 :math:`u` diffuses with coefficient one, and its right-hand side is the source of the temperature equation. 

54 dw : float, optional 

55 Driving force :math:`d_w`. 

56 L : float, optional 

57 Length of the periodic domain :math:`[-L/2, L/2]` in each direction. 

58 init_type : str, optional 

59 Initial condition of the phase field, either ``'circle'`` or ``'circle_rand'`` (``int(L)**2`` circles with 

60 random radii, 2D only). The temperature starts at one in both cases. 

61 comm : MPI.Intracomm, optional 

62 Communicator for ``mpi4py-fft``. 

63 

64 Attributes 

65 ---------- 

66 fft : fft object 

67 Object for FFT. 

68 X : np.ogrid 

69 Grid coordinates in real space. 

70 K2 : np.ndarray 

71 Laplace operator in spectral space. 

72 dx : float 

73 Mesh width in x direction. 

74 dy : float 

75 Mesh width in y direction. 

76 

77 References 

78 ---------- 

79 .. [1] https://mpi4py-fft.readthedocs.io/en/latest/ 

80 """ 

81 

82 dtype_u = mesh 

83 dtype_f = imex_mesh 

84 

85 def __init__( 

86 self, 

87 nvars=None, 

88 eps=0.04, 

89 radius=0.25, 

90 spectral=False, 

91 TM=1.0, 

92 D=10.0, 

93 dw=0.0, 

94 L=1.0, 

95 init_type='circle', 

96 comm=MPI.COMM_WORLD, 

97 ): 

98 """Initialization routine""" 

99 

100 if nvars is None: 

101 nvars = (128, 128) 

102 

103 if not (isinstance(nvars, tuple) and len(nvars) > 1): 

104 raise ProblemError('Need at least two dimensions') 

105 

106 # creating FFT structure 

107 ndim = len(nvars) 

108 axes = tuple(range(ndim)) 

109 self.fft = PFFT(comm, list(nvars), axes=axes, dtype=np.float64, collapse=True) 

110 

111 # get test data to figure out type and dimensions 

112 tmp_u = newDistArray(self.fft, spectral) 

113 

114 # add two components to contain field and temperature 

115 self.ncomp = 2 

116 sizes = tmp_u.shape + (self.ncomp,) 

117 

118 # invoke super init, passing the communicator and the local dimensions as init 

119 super().__init__(init=(sizes, comm, tmp_u.dtype)) 

120 self._makeAttributeAndRegister( 

121 'nvars', 

122 'eps', 

123 'radius', 

124 'spectral', 

125 'TM', 

126 'D', 

127 'dw', 

128 'L', 

129 'init_type', 

130 'comm', 

131 localVars=locals(), 

132 readOnly=True, 

133 ) 

134 

135 L = np.array([self.L] * ndim, dtype=float) 

136 

137 # get local mesh 

138 X = list(np.ogrid[self.fft.local_slice(False)]) 

139 N = self.fft.global_shape() 

140 for i in range(len(N)): 

141 X[i] = X[i] * L[i] / N[i] - L[i] / 2.0 

142 self.X = [np.broadcast_to(x, self.fft.shape(False)) for x in X] 

143 

144 # get local wavenumbers and Laplace operator 

145 s = self.fft.local_slice() 

146 N = self.fft.global_shape() 

147 k = [np.fft.fftfreq(n, 1.0 / n).astype(int) for n in N[:-1]] 

148 k.append(np.fft.rfftfreq(N[-1], 1.0 / N[-1]).astype(int)) 

149 K = [ki[si] for ki, si in zip(k, s, strict=True)] 

150 Ks = list(np.meshgrid(*K, indexing='ij', sparse=True)) 

151 Lp = 2 * np.pi / L 

152 for i in range(ndim): 

153 Ks[i] = (Ks[i] * Lp[i]).astype(float) 

154 K = [np.broadcast_to(k, self.fft.shape(True)) for k in Ks] 

155 K = np.array(K).astype(float) 

156 self.K2 = np.sum(K * K, 0, dtype=float) 

157 

158 # Need this for diagnostics 

159 self.dx = self.L / nvars[0] 

160 self.dy = self.L / nvars[1] 

161 

162 self.work_counters['rhs'] = WorkCounter() 

163 

164 def eval_f(self, u, t): 

165 """ 

166 Routine to evaluate the right-hand side of the problem. 

167 

168 Parameters 

169 ---------- 

170 u : dtype_u 

171 Current values of the numerical solution. 

172 t : float 

173 Current time of the numerical solution is computed. 

174 

175 Returns 

176 ------- 

177 f : dtype_f 

178 The right-hand side of the problem. 

179 """ 

180 

181 f = self.dtype_f(self.init) 

182 

183 if self.spectral: 

184 f.impl[..., 0] = -self.K2 * u[..., 0] 

185 

186 if self.eps > 0: 

187 tmp_u = newDistArray(self.fft, False) 

188 tmp_T = newDistArray(self.fft, False) 

189 tmp_u = self.fft.backward(u[..., 0], tmp_u) 

190 tmp_T = self.fft.backward(u[..., 1], tmp_T) 

191 tmpf = -2.0 / self.eps**2 * tmp_u * (1.0 - tmp_u) * (1.0 - 2.0 * tmp_u) - 6.0 * self.dw * ( 

192 tmp_T - self.TM 

193 ) / self.TM * tmp_u * (1.0 - tmp_u) 

194 f.expl[..., 0] = self.fft.forward(tmpf) 

195 

196 f.impl[..., 1] = -self.D * self.K2 * u[..., 1] 

197 f.expl[..., 1] = f.impl[..., 0] + f.expl[..., 0] 

198 

199 else: 

200 u_hat = self.fft.forward(u[..., 0]) 

201 lap_u_hat = -self.K2 * u_hat 

202 f.impl[..., 0] = self.fft.backward(lap_u_hat, f.impl[..., 0]) 

203 

204 if self.eps > 0: 

205 f.expl[..., 0] = -2.0 / self.eps**2 * u[..., 0] * (1.0 - u[..., 0]) * (1.0 - 2.0 * u[..., 0]) 

206 f.expl[..., 0] -= 6.0 * self.dw * (u[..., 1] - self.TM) / self.TM * u[..., 0] * (1.0 - u[..., 0]) 

207 

208 u_hat = self.fft.forward(u[..., 1]) 

209 lap_u_hat = -self.D * self.K2 * u_hat 

210 f.impl[..., 1] = self.fft.backward(lap_u_hat, f.impl[..., 1]) 

211 f.expl[..., 1] = f.impl[..., 0] + f.expl[..., 0] 

212 

213 self.work_counters['rhs']() 

214 return f 

215 

216 def solve_system(self, rhs, factor, u0, t): 

217 """ 

218 Simple FFT solver for the diffusion part. 

219 

220 Parameters 

221 ---------- 

222 rhs : dtype_f 

223 Right-hand side for the linear system. 

224 factor : float 

225 Abbrev. for the node-to-node stepsize (or any other factor required). 

226 u0 : dtype_u 

227 Initial guess for the iterative solver (not used here so far). 

228 t : float 

229 Current time (e.g. for time-dependent BCs). 

230 

231 Returns 

232 ------- 

233 me : dtype_u 

234 The solution as mesh. 

235 """ 

236 

237 if self.spectral: 

238 me = self.dtype_u(self.init) 

239 me[..., 0] = rhs[..., 0] / (1.0 + factor * self.K2) 

240 me[..., 1] = rhs[..., 1] / (1.0 + factor * self.D * self.K2) 

241 

242 else: 

243 me = self.dtype_u(self.init) 

244 rhs_hat = self.fft.forward(rhs[..., 0]) 

245 rhs_hat /= 1.0 + factor * self.K2 

246 me[..., 0] = self.fft.backward(rhs_hat) 

247 rhs_hat = self.fft.forward(rhs[..., 1]) 

248 rhs_hat /= 1.0 + factor * self.D * self.K2 

249 me[..., 1] = self.fft.backward(rhs_hat) 

250 

251 return me 

252 

253 def u_exact(self, t): 

254 r""" 

255 Routine to compute the exact solution at time :math:`t`. 

256 

257 Parameters 

258 ---------- 

259 t : float 

260 Time of the exact solution. 

261 

262 Returns 

263 ------- 

264 me : dtype_u 

265 The exact solution. 

266 """ 

267 

268 def circle(): 

269 tmp_me = newDistArray(self.fft, self.spectral) 

270 r2 = self.X[0] ** 2 + self.X[1] ** 2 

271 if self.spectral: 

272 tmp = 0.5 * (1.0 + np.tanh((self.radius - np.sqrt(r2)) / (np.sqrt(2) * self.eps))) 

273 tmp_me[:] = self.fft.forward(tmp) 

274 else: 

275 tmp_me[:] = 0.5 * (1.0 + np.tanh((self.radius - np.sqrt(r2)) / (np.sqrt(2) * self.eps))) 

276 return tmp_me 

277 

278 def circle_rand(): 

279 tmp_me = newDistArray(self.fft, self.spectral) 

280 ndim = len(tmp_me.shape) 

281 L = int(self.L) 

282 # get random radii for circles/spheres 

283 np.random.seed(1) 

284 lbound = 3.0 * self.eps 

285 ubound = 0.5 - self.eps 

286 rand_radii = (ubound - lbound) * np.random.random_sample(size=tuple([L] * ndim)) + lbound 

287 # distribute circles/spheres 

288 tmp = newDistArray(self.fft, False) 

289 if ndim == 2: 

290 for i in range(0, L): 

291 for j in range(0, L): 

292 # build radius 

293 r2 = (self.X[0] + i - L / 2 + 0.5) ** 2 + (self.X[1] + j - L / 2 + 0.5) ** 2 

294 # add this blob, shifted by 1 to avoid issues with adding up negative contributions 

295 tmp += np.tanh((rand_radii[i, j] - np.sqrt(r2)) / (np.sqrt(2) * self.eps)) + 1 

296 # normalize to [0,1] 

297 tmp *= 0.5 

298 assert np.all(tmp <= 1.0) 

299 if self.spectral: 

300 tmp_me[:] = self.fft.forward(tmp) 

301 else: 

302 tmp_me[:] = tmp[:] 

303 return tmp_me 

304 

305 def sine(): 

306 tmp_me = newDistArray(self.fft, self.spectral) 

307 if self.spectral: 

308 tmp = 0.5 * (2.0 + 0.0 * np.sin(2 * np.pi * self.X[0]) * np.sin(2 * np.pi * self.X[1])) 

309 tmp_me[:] = self.fft.forward(tmp) 

310 else: 

311 tmp_me[:] = 0.5 * (2.0 + 0.0 * np.sin(2 * np.pi * self.X[0]) * np.sin(2 * np.pi * self.X[1])) 

312 return tmp_me 

313 

314 assert t == 0, 'ERROR: u_exact only valid for t=0' 

315 me = self.dtype_u(self.init, val=0.0) 

316 if self.init_type == 'circle': 

317 me[..., 0] = circle() 

318 me[..., 1] = sine() 

319 elif self.init_type == 'circle_rand': 

320 me[..., 0] = circle_rand() 

321 me[..., 1] = sine() 

322 else: 

323 raise NotImplementedError('type of initial value not implemented, got %s' % self.init_type) 

324 

325 return me 

326 

327 

328# class allencahn_temp_imex_timeforcing(allencahn_temp_imex): 

329# """ 

330# Example implementing Allen-Cahn equation in 2-3D using mpi4py-fft for solving linear parts, IMEX time-stepping, 

331# time-dependent forcing 

332# """ 

333# 

334# def eval_f(self, u, t): 

335# """ 

336# Routine to evaluate the RHS 

337# 

338# Args: 

339# u (dtype_u): current values 

340# t (float): current time 

341# 

342# Returns: 

343# dtype_f: the RHS 

344# """ 

345# 

346# f = self.dtype_f(self.init) 

347# 

348# if self.spectral: 

349# 

350# f.impl = -self.K2 * u 

351# 

352# tmp = newDistArray(self.fft, False) 

353# tmp[:] = self.fft.backward(u, tmp) 

354# 

355# if self.eps > 0: 

356# tmpf = -2.0 / self.eps ** 2 * tmp * (1.0 - tmp) * (1.0 - 2.0 * tmp) 

357# else: 

358# tmpf = self.dtype_f(self.init, val=0.0) 

359# 

360# # build sum over RHS without driving force 

361# Rt_local = float(np.sum(self.fft.backward(f.impl) + tmpf)) 

362# if self.comm is not None: 

363# Rt_global = self.comm.allreduce(sendobj=Rt_local, op=MPI.SUM) 

364# else: 

365# Rt_global = Rt_local 

366# 

367# # build sum over driving force term 

368# Ht_local = float(np.sum(6.0 * tmp * (1.0 - tmp))) 

369# if self.comm is not None: 

370# Ht_global = self.comm.allreduce(sendobj=Ht_local, op=MPI.SUM) 

371# else: 

372# Ht_global = Rt_local 

373# 

374# # add/substract time-dependent driving force 

375# if Ht_global != 0.0: 

376# dw = Rt_global / Ht_global 

377# else: 

378# dw = 0.0 

379# 

380# tmpf -= 6.0 * dw * tmp * (1.0 - tmp) 

381# f.expl[:] = self.fft.forward(tmpf) 

382# 

383# else: 

384# 

385# u_hat = self.fft.forward(u) 

386# lap_u_hat = -self.K2 * u_hat 

387# f.impl[:] = self.fft.backward(lap_u_hat, f.impl) 

388# 

389# if self.eps > 0: 

390# f.expl = -2.0 / self.eps ** 2 * u * (1.0 - u) * (1.0 - 2.0 * u) 

391# 

392# # build sum over RHS without driving force 

393# Rt_local = float(np.sum(f.impl + f.expl)) 

394# if self.comm is not None: 

395# Rt_global = self.comm.allreduce(sendobj=Rt_local, op=MPI.SUM) 

396# else: 

397# Rt_global = Rt_local 

398# 

399# # build sum over driving force term 

400# Ht_local = float(np.sum(6.0 * u * (1.0 - u))) 

401# if self.comm is not None: 

402# Ht_global = self.comm.allreduce(sendobj=Ht_local, op=MPI.SUM) 

403# else: 

404# Ht_global = Rt_local 

405# 

406# # add/substract time-dependent driving force 

407# if Ht_global != 0.0: 

408# dw = Rt_global / Ht_global 

409# else: 

410# dw = 0.0 

411# 

412# f.expl -= 6.0 * dw * u * (1.0 - u) 

413# 

414# return f