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

222 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-17 13:41 +0000

1import numpy as np 

2from scipy import sparse as sp 

3 

4from pySDC.core.problem import Problem 

5from pySDC.implementations.datatype_classes.mesh import mesh 

6from pySDC.implementations.problem_classes.generic_spectral import GenericSpectralLinear 

7 

8 

9class Heat1DChebychev(GenericSpectralLinear): 

10 """ 

11 1D Heat equation with Dirichlet Boundary conditions discretized on (-1, 1) using a Chebychev spectral method. 

12 """ 

13 

14 dtype_u = mesh 

15 dtype_f = mesh 

16 

17 def __init__(self, nvars=128, a=0, b=0, f=1, nu=1.0, mode='T2U', **kwargs): 

18 """ 

19 Constructor. `kwargs` are forwarded to parent class constructor. 

20 

21 Args: 

22 nvars (int): Resolution 

23 a (float): Left BC value 

24 b (float): Right BC value 

25 f (int): Frequency of the solution 

26 nu (float): Diffusion parameter 

27 mode ('T2T' or 'T2U'): Mode for Chebychev method. 

28 

29 """ 

30 self._makeAttributeAndRegister('nvars', 'a', 'b', 'f', 'nu', 'mode', localVars=locals(), readOnly=True) 

31 

32 bases = [{'base': 'chebychev', 'N': nvars}] 

33 components = ['u', 'ux'] 

34 

35 super().__init__(bases, components, real_spectral_coefficients=True, **kwargs) 

36 

37 self.x = self.get_grid()[0] 

38 

39 I = self.get_Id() 

40 Dx = self.get_differentiation_matrix(axes=(0,)) 

41 self.Dx = Dx 

42 

43 self.T2U = self.get_basis_change_matrix(conv=mode) 

44 

45 L_lhs = { 

46 'ux': {'u': -self.T2U @ Dx, 'ux': self.T2U @ I}, 

47 'u': {'ux': -nu * (self.T2U @ Dx)}, 

48 } 

49 self.setup_L(L_lhs) 

50 

51 M_lhs = {'u': {'u': self.T2U @ I}} 

52 self.setup_M(M_lhs) 

53 

54 self.add_BC(component='u', equation='u', axis=0, x=-1, v=a, kind="Dirichlet") 

55 self.add_BC(component='u', equation='ux', axis=0, x=1, v=b, kind="Dirichlet") 

56 self.setup_BCs() 

57 

58 def eval_f(self, u, *args, **kwargs): 

59 """ 

60 Evaluate the right hand side nu u_xx, computed as nu times the Chebychev derivative of the auxiliary component 

61 `ux`. The algebraic `ux` equation gets zero. 

62 

63 Args: 

64 u (dtype_u): Solution, in spectral space if `spectral_space` is set, else in physical space 

65 *args: Not used, the right hand side does not depend on time 

66 **kwargs: Not used, the right hand side does not depend on time 

67 

68 Returns: 

69 Heat1DChebychev.dtype_f: The right hand side, in the same space as `u` 

70 """ 

71 f = self.f_init 

72 iu, iux = self.index(self.components) 

73 

74 if self.spectral_space: 

75 u_hat = u.copy() 

76 else: 

77 u_hat = self.transform(u) 

78 

79 u_hat[iu] = (self.nu * self.Dx @ u_hat[iux].flatten()).reshape(u_hat[iu].shape) 

80 

81 if self.spectral_space: 

82 me = u_hat 

83 else: 

84 me = self.itransform(u_hat).real 

85 

86 f[iu] = me[iu] 

87 return f 

88 

89 def u_exact(self, t, noise=0, seed=666): 

90 """ 

91 Get exact solution at time `t` 

92 

93 Args: 

94 t (float): When you want the exact solution 

95 noise (float): Add noise of this level 

96 seed (int): Random seed for the noise 

97 

98 Returns: 

99 Heat1DChebychev.dtype_u: Exact solution 

100 """ 

101 xp = self.xp 

102 iu, iux = self.index(self.components) 

103 u = self.spectral.u_init 

104 

105 u[iu] = ( 

106 xp.sin(self.f * np.pi * self.x) * xp.exp(-self.nu * (self.f * np.pi) ** 2 * t) 

107 + (self.b - self.a) / 2 * self.x 

108 + (self.b + self.a) / 2 

109 ) 

110 u[iux] = ( 

111 self.f * np.pi * xp.cos(self.f * np.pi * self.x) * xp.exp(-self.nu * (self.f * np.pi) ** 2 * t) 

112 + (self.b - self.a) / 2 

113 ) 

114 

115 if noise > 0: 

116 assert t == 0 

117 _noise = self.u_init 

118 rng = self.xp.random.default_rng(seed=seed) 

119 _noise[iu] = rng.normal(size=u[iu].shape) 

120 noise_hat = self.transform(_noise) 

121 low_pass = self.get_filter_matrix(axis=0, kmax=self.nvars - 2) 

122 noise_hat[iu] = (low_pass @ noise_hat[iu].flatten()).reshape(noise_hat[iu].shape) 

123 _noise[:] = self.itransform(noise_hat) 

124 u += _noise * noise * (self.x - 1) * (self.x + 1) 

125 

126 self.check_BCs(u) 

127 

128 if self.spectral_space: 

129 u_hat = self.u_init 

130 u_hat[...] = self.transform(u) 

131 return u_hat 

132 else: 

133 return u 

134 

135 

136class Heat1DUltraspherical(GenericSpectralLinear): 

137 """ 

138 1D Heat equation with Dirichlet Boundary conditions discretized on (-1, 1) using an ultraspherical spectral method. 

139 """ 

140 

141 dtype_u = mesh 

142 dtype_f = mesh 

143 

144 def __init__(self, nvars=128, a=0, b=0, f=1, nu=1.0, **kwargs): 

145 """ 

146 Constructor. `kwargs` are forwarded to parent class constructor. 

147 

148 Args: 

149 nvars (int): Resolution 

150 a (float): Left BC value 

151 b (float): Right BC value 

152 f (int): Frequency of the solution 

153 nu (float): Diffusion parameter 

154 """ 

155 self._makeAttributeAndRegister('nvars', 'a', 'b', 'f', 'nu', localVars=locals(), readOnly=True) 

156 

157 bases = [{'base': 'ultraspherical', 'N': nvars}] 

158 components = ['u'] 

159 

160 GenericSpectralLinear.__init__(self, bases, components, real_spectral_coefficients=True, **kwargs) 

161 

162 self.x = self.get_grid()[0] 

163 

164 I = self.get_Id() 

165 Dxx = self.get_differentiation_matrix(axes=(0,), p=2) 

166 

167 S2 = self.get_basis_change_matrix(p_in=2, p_out=0) 

168 U2 = self.get_basis_change_matrix(p_in=0, p_out=2) 

169 

170 self.Dxx = S2 @ Dxx 

171 

172 L_lhs = { 

173 'u': {'u': -nu * Dxx}, 

174 } 

175 self.setup_L(L_lhs) 

176 

177 M_lhs = {'u': {'u': U2 @ I}} 

178 self.setup_M(M_lhs) 

179 

180 self.add_BC(component='u', equation='u', axis=0, x=-1, v=a, kind="Dirichlet", line=-1) 

181 self.add_BC(component='u', equation='u', axis=0, x=1, v=b, kind="Dirichlet", line=-2) 

182 self.setup_BCs() 

183 

184 def eval_f(self, u, *args, **kwargs): 

185 """ 

186 Evaluate the right hand side nu u_xx with the ultraspherical second derivative matrix, converted back to the 

187 Chebychev basis. 

188 

189 Args: 

190 u (dtype_u): Solution, in spectral space if `spectral_space` is set, else in physical space 

191 *args: Not used, the right hand side does not depend on time 

192 **kwargs: Not used, the right hand side does not depend on time 

193 

194 Returns: 

195 Heat1DUltraspherical.dtype_f: The right hand side, in the same space as `u` 

196 """ 

197 f = self.f_init 

198 iu = self.index('u') 

199 

200 if self.spectral_space: 

201 u_hat = u.copy() 

202 else: 

203 u_hat = self.transform(u) 

204 

205 u_hat[iu] = (self.nu * (self.Dxx @ u_hat[iu].flatten())).reshape(u_hat[iu].shape) 

206 

207 if self.spectral_space: 

208 me = u_hat 

209 else: 

210 me = self.itransform(u_hat).real 

211 

212 f[iu][...] = me[iu] 

213 return f 

214 

215 def u_exact(self, t, noise=0, seed=666): 

216 """ 

217 Get exact solution at time `t` 

218 

219 Args: 

220 t (float): When you want the exact solution 

221 noise (float): Add noise of this level 

222 seed (int): Random seed for the noise 

223 

224 Returns: 

225 Heat1DUltraspherical.dtype_u: Exact solution 

226 """ 

227 xp = self.xp 

228 iu = self.index('u') 

229 u = self.spectral.u_init 

230 

231 u[iu] = ( 

232 xp.sin(self.f * np.pi * self.x) * xp.exp(-self.nu * (self.f * np.pi) ** 2 * t) 

233 + (self.b - self.a) / 2 * self.x 

234 + (self.b + self.a) / 2 

235 ) 

236 

237 if noise > 0: 

238 assert t == 0 

239 _noise = self.u_init 

240 rng = self.xp.random.default_rng(seed=seed) 

241 _noise[iu] = rng.normal(size=u[iu].shape) 

242 noise_hat = self.transform(_noise) 

243 low_pass = self.get_filter_matrix(axis=0, kmax=self.nvars - 2) 

244 noise_hat[iu] = (low_pass @ noise_hat[iu].flatten()).reshape(noise_hat[iu].shape) 

245 _noise[:] = self.itransform(noise_hat) 

246 u += _noise * noise * (self.x - 1) * (self.x + 1) 

247 

248 self.check_BCs(u) 

249 

250 if self.spectral_space: 

251 u_hat = self.u_init 

252 u_hat[...] = self.transform(u) 

253 return u_hat 

254 else: 

255 return u 

256 

257 

258class Heat2DChebychev(GenericSpectralLinear): 

259 """ 

260 2D heat equation, periodic (FFT) or Dirichlet (Chebychev) in each direction, in first-order formulation. 

261 

262 Discretized on (-1, 1)x(-1,1) using spectral methods based on FFT and Chebychev. 

263 """ 

264 

265 dtype_u = mesh 

266 dtype_f = mesh 

267 

268 def __init__(self, nx=128, ny=128, base_x='fft', base_y='chebychev', a=0, b=0, c=0, fx=1, fy=1, nu=1.0, **kwargs): 

269 """ 

270 Constructor. `kwargs` are forwarded to parent class constructor. 

271 

272 Args: 

273 nx (int): Resolution in x-direction 

274 ny (int): Resolution in y-direction 

275 base_x (str): Spectral base in x-direction 

276 base_y (str): Spectral base in y-direction 

277 a (float): BC at y=0 and x=-1 

278 b (float): BC at y=0 and x=+1 

279 c (float): BC at y=1 and x = -1 

280 fx (int): Horizontal frequency of initial conditions 

281 fy (int): Vertical frequency of initial conditions 

282 nu (float): Diffusion parameter 

283 """ 

284 assert nx % 2 == 1 or base_x == 'fft' 

285 assert ny % 2 == 1 or base_y == 'fft' 

286 self._makeAttributeAndRegister( 

287 'nx', 'ny', 'base_x', 'base_y', 'a', 'b', 'c', 'fx', 'fy', 'nu', localVars=locals(), readOnly=True 

288 ) 

289 

290 bases = [{'base': base_x, 'N': nx}, {'base': base_y, 'N': ny}] 

291 components = ['u', 'ux', 'uy'] 

292 

293 super().__init__(bases, components, Dirichlet_recombination=False, spectral_space=False, **kwargs) 

294 

295 self.X, self.Y = self.get_grid() 

296 

297 I = self.get_Id() 

298 self.Dx = self.get_differentiation_matrix(axes=(0,)) 

299 self.Dy = self.get_differentiation_matrix(axes=(1,)) 

300 

301 L_lhs = { 

302 'ux': {'u': -self.Dx, 'ux': I}, 

303 'uy': {'u': -self.Dy, 'uy': I}, 

304 'u': {'ux': -nu * self.Dx, 'uy': -nu * self.Dy}, 

305 } 

306 self.setup_L(L_lhs) 

307 

308 M_lhs = {'u': {'u': I}} 

309 self.setup_M(M_lhs) 

310 

311 for base in [base_x, base_y]: 

312 assert base in ['chebychev', 'fft'] 

313 

314 alpha = (self.b - self.a) / 2.0 

315 beta = (self.c - self.b) / 2.0 

316 gamma = (self.c + self.a) / 2.0 

317 

318 if base_x == 'chebychev': 

319 y = self.Y[0, :] 

320 if self.base_y == 'fft': 

321 self.add_BC(component='u', equation='u', axis=0, x=-1, v=beta * y - alpha + gamma, kind='Dirichlet') 

322 self.add_BC(component='ux', equation='ux', axis=0, v=2 * alpha, kind='integral') 

323 else: 

324 assert a == b, f'Need periodic boundary conditions in x for {base_x} method!' 

325 if base_y == 'chebychev': 

326 x = self.X[:, 0] 

327 self.add_BC(component='u', equation='u', axis=1, x=-1, v=alpha * x - beta + gamma, kind='Dirichlet') 

328 self.add_BC(component='uy', equation='uy', axis=1, v=2 * beta, kind='integral') 

329 else: 

330 assert c == b, f'Need periodic boundary conditions in y for {base_y} method!' 

331 self.setup_BCs() 

332 

333 def eval_f(self, u, *args, **kwargs): 

334 """ 

335 Evaluate the right hand side nu (u_xx + u_yy), computed by spectral differentiation of the auxiliary components 

336 `ux` and `uy`. The algebraic `ux` and `uy` equations get zero. 

337 

338 Args: 

339 u (dtype_u): Solution in physical space 

340 *args: Not used, the right hand side does not depend on time 

341 **kwargs: Not used, the right hand side does not depend on time 

342 

343 Returns: 

344 Heat2DChebychev.dtype_f: The right hand side in physical space 

345 """ 

346 f = self.f_init 

347 iu, iux, iuy = self.index(self.components) 

348 

349 me_hat = self.u_init_forward 

350 me_hat[:] = self.transform(u) 

351 me_hat[iu] = self.nu * (self.Dx @ me_hat[iux].flatten() + self.Dy @ me_hat[iuy].flatten()).reshape( 

352 me_hat[iu].shape 

353 ) 

354 me = self.itransform(me_hat) 

355 

356 f[self.index("u")] = me[iu].real 

357 return f 

358 

359 def u_exact(self, t): 

360 """ 

361 Get the exact solution at time `t`: sin(fx x) sin(fy y) exp(-nu (fx^2 + fy^2) t) plus the linear function in x 

362 and y that satisfies the boundary conditions, where the frequencies are multiplied by pi in Chebychev 

363 directions. The components `ux` and `uy` are set to the derivatives. 

364 

365 Args: 

366 t (float): When you want the exact solution 

367 

368 Returns: 

369 Heat2DChebychev.dtype_u: Exact solution in physical space 

370 """ 

371 xp = self.xp 

372 iu, iux, iuy = self.index(self.components) 

373 u = self.u_init 

374 

375 fx = self.fx if self.base_x == 'fft' else np.pi * self.fx 

376 fy = self.fy if self.base_y == 'fft' else np.pi * self.fy 

377 

378 time_dep = xp.exp(-self.nu * (fx**2 + fy**2) * t) 

379 

380 alpha = (self.b - self.a) / 2.0 

381 beta = (self.c - self.b) / 2.0 

382 gamma = (self.c + self.a) / 2.0 

383 

384 u[iu] = xp.sin(fx * self.X) * xp.sin(fy * self.Y) * time_dep + alpha * self.X + beta * self.Y + gamma 

385 u[iux] = fx * xp.cos(fx * self.X) * xp.sin(fy * self.Y) * time_dep + alpha 

386 u[iuy] = fy * xp.sin(fx * self.X) * xp.cos(fy * self.Y) * time_dep + beta 

387 

388 return u 

389 

390 

391class Heat2DUltraspherical(GenericSpectralLinear): 

392 """ 

393 2D heat equation, periodic (FFT) or Dirichlet (ultraspherical) in each direction, in second-order formulation. 

394 

395 Discretized on (-1, 1)x(-1,1) using spectral methods based on FFT and Gegenbauer. 

396 """ 

397 

398 dtype_u = mesh 

399 dtype_f = mesh 

400 

401 def __init__( 

402 self, nx=128, ny=128, base_x='fft', base_y='ultraspherical', a=0, b=0, c=0, fx=1, fy=1, nu=1.0, **kwargs 

403 ): 

404 """ 

405 Constructor. `kwargs` are forwarded to parent class constructor. 

406 

407 Args: 

408 nx (int): Resolution in x-direction 

409 ny (int): Resolution in y-direction 

410 base_x (str): Spectral base in x-direction 

411 base_y (str): Spectral base in y-direction 

412 a (float): BC at y=0 and x=-1 

413 b (float): BC at y=0 and x=+1 

414 c (float): BC at y=1 and x = -1 

415 fx (int): Horizontal frequency of initial conditions 

416 fy (int): Vertical frequency of initial conditions 

417 nu (float): Diffusion parameter 

418 """ 

419 self._makeAttributeAndRegister( 

420 'nx', 'ny', 'base_x', 'base_y', 'a', 'b', 'c', 'fx', 'fy', 'nu', localVars=locals(), readOnly=True 

421 ) 

422 

423 bases = [{'base': base_x, 'N': nx}, {'base': base_y, 'N': ny}] 

424 components = ['u'] 

425 

426 super().__init__(bases, components, Dirichlet_recombination=False, spectral_space=False, **kwargs) 

427 

428 self.X, self.Y = self.get_grid() 

429 

430 self.Dxx = self.get_differentiation_matrix(axes=(0,), p=2) 

431 self.Dyy = self.get_differentiation_matrix(axes=(1,), p=2) 

432 self.S2 = self.get_basis_change_matrix(p=2) 

433 I = self.get_Id() 

434 

435 L_lhs = { 

436 'u': {'u': -nu * self.Dxx - nu * self.Dyy}, 

437 } 

438 self.setup_L(L_lhs) 

439 

440 M_lhs = {'u': {'u': I}} 

441 self.setup_M(M_lhs) 

442 

443 for base in [base_x, base_y]: 

444 assert base in ['ultraspherical', 'fft'] 

445 

446 alpha = (self.b - self.a) / 2.0 

447 beta = (self.c - self.b) / 2.0 

448 gamma = (self.c + self.a) / 2.0 

449 

450 if base_x == 'ultraspherical': 

451 y = self.Y[0, :] 

452 if self.base_y == 'fft': 

453 self.add_BC(component='u', equation='u', axis=0, x=-1, v=beta * y - alpha + gamma, kind='Dirichlet') 

454 self.add_BC(component='u', equation='u', axis=0, v=beta * y + alpha + gamma, x=1, line=-2, kind='Dirichlet') 

455 else: 

456 assert a == b, f'Need periodic boundary conditions in x for {base_x} method!' 

457 if base_y == 'ultraspherical': 

458 x = self.X[:, 0] 

459 self.add_BC( 

460 component='u', equation='u', axis=1, x=-1, v=alpha * x - beta + gamma, kind='Dirichlet', line=-1 

461 ) 

462 self.add_BC( 

463 component='u', equation='u', axis=1, x=+1, v=alpha * x + beta + gamma, kind='Dirichlet', line=-2 

464 ) 

465 else: 

466 assert c == b, f'Need periodic boundary conditions in y for {base_y} method!' 

467 self.setup_BCs() 

468 

469 def eval_f(self, u, *args, **kwargs): 

470 """ 

471 Evaluate the right hand side nu (u_xx + u_yy) with the ultraspherical second derivative matrices, converted back 

472 to the Chebychev basis. 

473 

474 Args: 

475 u (dtype_u): Solution in physical space 

476 *args: Not used, the right hand side does not depend on time 

477 **kwargs: Not used, the right hand side does not depend on time 

478 

479 Returns: 

480 Heat2DUltraspherical.dtype_f: The right hand side in physical space 

481 """ 

482 f = self.f_init 

483 iu = self.index('u') 

484 

485 me_hat = self.u_init_forward 

486 me_hat[:] = self.transform(u) 

487 me_hat[iu] = self.nu * (self.S2 @ (self.Dxx + self.Dyy) @ me_hat[iu].flatten()).reshape(me_hat[iu].shape) 

488 me = self.itransform(me_hat) 

489 

490 f[iu] = me[iu].real 

491 return f 

492 

493 def u_exact(self, t): 

494 """ 

495 Get the exact solution at time `t`: sin(fx x) sin(fy y) exp(-nu (fx^2 + fy^2) t) plus the linear function in x 

496 and y that satisfies the boundary conditions, where the frequencies are multiplied by pi in ultraspherical 

497 directions. 

498 

499 Args: 

500 t (float): When you want the exact solution 

501 

502 Returns: 

503 Heat2DUltraspherical.dtype_u: Exact solution in physical space 

504 """ 

505 xp = self.xp 

506 iu = self.index('u') 

507 u = self.u_init 

508 

509 fx = self.fx if self.base_x == 'fft' else np.pi * self.fx 

510 fy = self.fy if self.base_y == 'fft' else np.pi * self.fy 

511 

512 time_dep = xp.exp(-self.nu * (fx**2 + fy**2) * t) 

513 

514 alpha = (self.b - self.a) / 2.0 

515 beta = (self.c - self.b) / 2.0 

516 gamma = (self.c + self.a) / 2.0 

517 

518 u[iu] = xp.sin(fx * self.X) * xp.sin(fy * self.Y) * time_dep + alpha * self.X + beta * self.Y + gamma 

519 

520 return u