|
| 1 | +import numpy as np |
| 2 | +from mpi4py import MPI |
| 3 | + |
| 4 | +from pySDC.implementations.problem_classes.generic_spectral import GenericSpectralLinear |
| 5 | +from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh |
| 6 | +from pySDC.core.convergence_controller import ConvergenceController |
| 7 | +from pySDC.core.hooks import Hooks |
| 8 | +from pySDC.implementations.convergence_controller_classes.check_convergence import CheckConvergence |
| 9 | +from pySDC.core.problem import WorkCounter |
| 10 | + |
| 11 | + |
| 12 | +class RayleighBenard3D(GenericSpectralLinear): |
| 13 | + """ |
| 14 | + Rayleigh-Benard Convection is a variation of incompressible Navier-Stokes. |
| 15 | +
|
| 16 | + The equations we solve are |
| 17 | +
|
| 18 | + u_x + v_y + w_z = 0 |
| 19 | + T_t - kappa (T_xx + T_yy + T_zz) = -uT_x - vT_y - wT_z |
| 20 | + u_t - nu (u_xx + u_yy + u_zz) + p_x = -uu_x - vu_y - wu_z |
| 21 | + v_t - nu (v_xx + v_yy + v_zz) + p_y = -uv_x - vv_y - wv_z |
| 22 | + w_t - nu (w_xx + w_yy + w_zz) + p_z - T = -uw_x - vw_y - ww_z |
| 23 | +
|
| 24 | + with u the horizontal velocity, v the vertical velocity (in z-direction), T the temperature, p the pressure, indices |
| 25 | + denoting derivatives, kappa=(Rayleigh * Prandtl)**(-1/2) and nu = (Rayleigh / Prandtl)**(-1/2). Everything on the left |
| 26 | + hand side, that is the viscous part, the pressure gradient and the buoyancy due to temperature are treated |
| 27 | + implicitly, while the non-linear convection part on the right hand side is integrated explicitly. |
| 28 | +
|
| 29 | + The domain, vertical boundary conditions and pressure gauge are |
| 30 | +
|
| 31 | + Omega = [0, 8) x (-1, 1) |
| 32 | + T(z=+1) = 0 |
| 33 | + T(z=-1) = 2 |
| 34 | + u(z=+-1) = v(z=+-1) = 0 |
| 35 | + integral over p = 0 |
| 36 | +
|
| 37 | + The spectral discretization uses FFT horizontally, implying periodic BCs, and an ultraspherical method vertically to |
| 38 | + facilitate the Dirichlet BCs. |
| 39 | +
|
| 40 | + Parameters: |
| 41 | + Prandtl (float): Prandtl number |
| 42 | + Rayleigh (float): Rayleigh number |
| 43 | + nx (int): Horizontal resolution |
| 44 | + nz (int): Vertical resolution |
| 45 | + BCs (dict): Can specify boundary conditions here |
| 46 | + dealiasing (float): Dealiasing factor for evaluating the non-linear part |
| 47 | + comm (mpi4py.Intracomm): Space communicator |
| 48 | + """ |
| 49 | + |
| 50 | + dtype_u = mesh |
| 51 | + dtype_f = imex_mesh |
| 52 | + |
| 53 | + def __init__( |
| 54 | + self, |
| 55 | + Prandtl=1, |
| 56 | + Rayleigh=2e6, |
| 57 | + nx=256, |
| 58 | + ny=256, |
| 59 | + nz=64, |
| 60 | + BCs=None, |
| 61 | + dealiasing=1.5, |
| 62 | + comm=None, |
| 63 | + Lz=1, |
| 64 | + Lx=1, |
| 65 | + Ly=1, |
| 66 | + useGPU=False, |
| 67 | + **kwargs, |
| 68 | + ): |
| 69 | + """ |
| 70 | + Constructor. `kwargs` are forwarded to parent class constructor. |
| 71 | +
|
| 72 | + Args: |
| 73 | + Prandtl (float): Prandtl number |
| 74 | + Rayleigh (float): Rayleigh number |
| 75 | + nx (int): Resolution in x-direction |
| 76 | + nz (int): Resolution in z direction |
| 77 | + BCs (dict): Vertical boundary conditions |
| 78 | + dealiasing (float): Dealiasing for evaluating the non-linear part in real space |
| 79 | + comm (mpi4py.Intracomm): Space communicator |
| 80 | + Lx (float): Horizontal length of the domain |
| 81 | + """ |
| 82 | + # TODO: documentation |
| 83 | + BCs = {} if BCs is None else BCs |
| 84 | + BCs = { |
| 85 | + 'T_top': 0, |
| 86 | + 'T_bottom': Lz, |
| 87 | + 'w_top': 0, |
| 88 | + 'w_bottom': 0, |
| 89 | + 'v_top': 0, |
| 90 | + 'v_bottom': 0, |
| 91 | + 'u_top': 0, |
| 92 | + 'u_bottom': 0, |
| 93 | + 'p_integral': 0, |
| 94 | + **BCs, |
| 95 | + } |
| 96 | + if comm is None: |
| 97 | + try: |
| 98 | + from mpi4py import MPI |
| 99 | + |
| 100 | + comm = MPI.COMM_WORLD |
| 101 | + except ModuleNotFoundError: |
| 102 | + pass |
| 103 | + self._makeAttributeAndRegister( |
| 104 | + 'Prandtl', |
| 105 | + 'Rayleigh', |
| 106 | + 'nx', |
| 107 | + 'ny', |
| 108 | + 'nz', |
| 109 | + 'BCs', |
| 110 | + 'dealiasing', |
| 111 | + 'comm', |
| 112 | + 'Lx', |
| 113 | + 'Ly', |
| 114 | + 'Lz', |
| 115 | + localVars=locals(), |
| 116 | + readOnly=True, |
| 117 | + ) |
| 118 | + |
| 119 | + bases = [ |
| 120 | + {'base': 'fft', 'N': nx, 'x0': 0, 'x1': self.Lx, 'useFFTW': not useGPU}, |
| 121 | + {'base': 'fft', 'N': ny, 'x0': 0, 'x1': self.Ly, 'useFFTW': not useGPU}, |
| 122 | + {'base': 'ultraspherical', 'N': nz, 'x0': 0, 'x1': self.Lz}, |
| 123 | + ] |
| 124 | + components = ['u', 'v', 'w', 'T', 'p'] |
| 125 | + super().__init__(bases, components, comm=comm, useGPU=useGPU, **kwargs) |
| 126 | + |
| 127 | + self.X, self.Y, self.Z = self.get_grid() |
| 128 | + self.Kx, self.Ky, self.Kz = self.get_wavenumbers() |
| 129 | + |
| 130 | + # construct 3D matrices |
| 131 | + Dzz = self.get_differentiation_matrix(axes=(2,), p=2) |
| 132 | + Dz = self.get_differentiation_matrix(axes=(2,)) |
| 133 | + Dy = self.get_differentiation_matrix(axes=(1,)) |
| 134 | + Dyy = self.get_differentiation_matrix(axes=(1,), p=2) |
| 135 | + Dx = self.get_differentiation_matrix(axes=(0,)) |
| 136 | + Dxx = self.get_differentiation_matrix(axes=(0,), p=2) |
| 137 | + Id = self.get_Id() |
| 138 | + |
| 139 | + S1 = self.get_basis_change_matrix(p_out=0, p_in=1) |
| 140 | + S2 = self.get_basis_change_matrix(p_out=0, p_in=2) |
| 141 | + |
| 142 | + U01 = self.get_basis_change_matrix(p_in=0, p_out=1) |
| 143 | + U12 = self.get_basis_change_matrix(p_in=1, p_out=2) |
| 144 | + U02 = self.get_basis_change_matrix(p_in=0, p_out=2) |
| 145 | + |
| 146 | + self.Dx = Dx |
| 147 | + self.Dxx = Dxx |
| 148 | + self.Dy = Dy |
| 149 | + self.Dyy = Dyy |
| 150 | + self.Dz = S1 @ Dz |
| 151 | + self.Dzz = S2 @ Dzz |
| 152 | + self.S2 = S2 |
| 153 | + self.S1 = S1 |
| 154 | + |
| 155 | + # compute rescaled Rayleigh number to extract viscosity and thermal diffusivity |
| 156 | + Ra = Rayleigh / (max([abs(BCs['T_top'] - BCs['T_bottom']), np.finfo(float).eps]) * self.axes[2].L ** 3) |
| 157 | + self.kappa = (Ra * Prandtl) ** (-1 / 2.0) |
| 158 | + self.nu = (Ra / Prandtl) ** (-1 / 2.0) |
| 159 | + |
| 160 | + # construct operators |
| 161 | + _D = U02 @ (Dxx + Dyy) + Dzz |
| 162 | + L_lhs = { |
| 163 | + 'p': {'u': U01 @ Dx, 'v': U01 @ Dy, 'w': Dz}, # divergence free constraint |
| 164 | + 'u': {'p': U02 @ Dx, 'u': -self.nu * _D}, |
| 165 | + 'v': {'p': U02 @ Dy, 'v': -self.nu * _D}, |
| 166 | + 'w': {'p': U12 @ Dz, 'w': -self.nu * _D, 'T': -U02 @ Id}, |
| 167 | + 'T': {'T': -self.kappa * _D}, |
| 168 | + } |
| 169 | + self.setup_L(L_lhs) |
| 170 | + |
| 171 | + # mass matrix |
| 172 | + _U02 = U02 @ Id |
| 173 | + M_lhs = {i: {i: _U02} for i in ['u', 'v', 'w', 'T']} |
| 174 | + self.setup_M(M_lhs) |
| 175 | + |
| 176 | + # BCs |
| 177 | + self.add_BC( |
| 178 | + component='p', equation='p', axis=2, v=self.BCs['p_integral'], kind='integral', line=-1, scalar=True |
| 179 | + ) |
| 180 | + self.add_BC(component='T', equation='T', axis=2, x=-1, v=self.BCs['T_bottom'], kind='Dirichlet', line=-1) |
| 181 | + self.add_BC(component='T', equation='T', axis=2, x=1, v=self.BCs['T_top'], kind='Dirichlet', line=-2) |
| 182 | + self.add_BC(component='w', equation='w', axis=2, x=1, v=self.BCs['w_top'], kind='Dirichlet', line=-1) |
| 183 | + self.add_BC(component='w', equation='w', axis=2, x=-1, v=self.BCs['w_bottom'], kind='Dirichlet', line=-2) |
| 184 | + self.remove_BC(component='w', equation='w', axis=2, x=-1, kind='Dirichlet', line=-2, scalar=True) |
| 185 | + for comp in ['u', 'v']: |
| 186 | + self.add_BC( |
| 187 | + component=comp, equation=comp, axis=2, v=self.BCs[f'{comp}_top'], x=1, kind='Dirichlet', line=-2 |
| 188 | + ) |
| 189 | + self.add_BC( |
| 190 | + component=comp, |
| 191 | + equation=comp, |
| 192 | + axis=2, |
| 193 | + v=self.BCs[f'{comp}_bottom'], |
| 194 | + x=-1, |
| 195 | + kind='Dirichlet', |
| 196 | + line=-1, |
| 197 | + ) |
| 198 | + |
| 199 | + # eliminate Nyquist mode if needed |
| 200 | + if nx % 2 == 0: |
| 201 | + Nyquist_mode_index = self.axes[0].get_Nyquist_mode_index() |
| 202 | + for component in self.components: |
| 203 | + self.add_BC( |
| 204 | + component=component, equation=component, axis=0, kind='Nyquist', line=int(Nyquist_mode_index), v=0 |
| 205 | + ) |
| 206 | + if ny % 2 == 0: |
| 207 | + Nyquist_mode_index = self.axes[0].get_Nyquist_mode_index() |
| 208 | + for component in self.components: |
| 209 | + self.add_BC( |
| 210 | + component=component, equation=component, axis=1, kind='Nyquist', line=int(Nyquist_mode_index), v=0 |
| 211 | + ) |
| 212 | + self.setup_BCs() |
| 213 | + |
| 214 | + self.work_counters['rhs'] = WorkCounter() |
| 215 | + |
| 216 | + def eval_f(self, u, *args, **kwargs): |
| 217 | + f = self.f_init |
| 218 | + |
| 219 | + if self.spectral_space: |
| 220 | + u_hat = u.copy() |
| 221 | + else: |
| 222 | + u_hat = self.transform(u) |
| 223 | + |
| 224 | + f_impl_hat = self.u_init_forward |
| 225 | + |
| 226 | + iu, iv, iw, iT, ip = self.index(['u', 'v', 'w', 'T', 'p']) |
| 227 | + derivative_indices = [iu, iv, iw, iT] |
| 228 | + |
| 229 | + # evaluate implicit terms |
| 230 | + f_impl_hat = -(self.L @ u_hat.flatten()).reshape(u_hat.shape) |
| 231 | + for i in derivative_indices: |
| 232 | + f_impl_hat[i] = (self.S2 @ f_impl_hat[i].flatten()).reshape(f_impl_hat[i].shape) |
| 233 | + f_impl_hat[ip] = (self.S1 @ f_impl_hat[ip].flatten()).reshape(f_impl_hat[ip].shape) |
| 234 | + |
| 235 | + if self.spectral_space: |
| 236 | + self.xp.copyto(f.impl, f_impl_hat) |
| 237 | + else: |
| 238 | + f.impl[:] = self.itransform(f_impl_hat).real |
| 239 | + |
| 240 | + # ------------------------------------------- |
| 241 | + # treat convection explicitly with dealiasing |
| 242 | + |
| 243 | + # start by computing derivatives |
| 244 | + padding = (self.dealiasing,) * self.ndim |
| 245 | + derivatives = [] |
| 246 | + u_hat_flat = [u_hat[i].flatten() for i in derivative_indices] |
| 247 | + |
| 248 | + _D_u_hat = self.u_init_forward |
| 249 | + for D in [self.Dx, self.Dy, self.Dz]: |
| 250 | + _D_u_hat *= 0 |
| 251 | + for i in derivative_indices: |
| 252 | + self.xp.copyto(_D_u_hat[i], (D @ u_hat_flat[i]).reshape(_D_u_hat[i].shape)) |
| 253 | + derivatives.append(self.itransform(_D_u_hat, padding=padding).real) |
| 254 | + |
| 255 | + u_pad = self.itransform(u_hat, padding=padding).real |
| 256 | + |
| 257 | + fexpl_pad = self.xp.zeros_like(u_pad) |
| 258 | + for i in derivative_indices: |
| 259 | + for i_vel, iD in zip([iu, iv, iw], range(self.ndim)): |
| 260 | + fexpl_pad[i] -= u_pad[i_vel] * derivatives[iD][i] |
| 261 | + |
| 262 | + if self.spectral_space: |
| 263 | + self.xp.copyto(f.expl, self.transform(fexpl_pad, padding=padding)) |
| 264 | + else: |
| 265 | + f.expl[:] = self.itransform(self.transform(fexpl_pad, padding=padding)).real |
| 266 | + |
| 267 | + self.work_counters['rhs']() |
| 268 | + return f |
| 269 | + |
| 270 | + def u_exact(self, t=0, noise_level=1e-3, seed=99): |
| 271 | + assert t == 0 |
| 272 | + assert ( |
| 273 | + self.BCs['v_top'] == self.BCs['v_bottom'] |
| 274 | + ), 'Initial conditions are only implemented for zero velocity gradient' |
| 275 | + |
| 276 | + me = self.spectral.u_init |
| 277 | + iu, iw, iT, ip = self.index(['u', 'w', 'T', 'p']) |
| 278 | + |
| 279 | + # linear temperature gradient |
| 280 | + assert self.Lz == 1 |
| 281 | + for comp in ['T', 'u', 'v', 'w']: |
| 282 | + a = self.BCs[f'{comp}_top'] - self.BCs[f'{comp}_bottom'] |
| 283 | + b = self.BCs[f'{comp}_bottom'] |
| 284 | + me[self.index(comp)] = a * self.Z + b |
| 285 | + |
| 286 | + # perturb slightly |
| 287 | + rng = self.xp.random.default_rng(seed=seed) |
| 288 | + |
| 289 | + noise = self.spectral.u_init |
| 290 | + noise[iT] = rng.random(size=me[iT].shape) |
| 291 | + |
| 292 | + me[iT] += noise[iT].real * noise_level * (self.Z - 1) * (self.Z + 1) |
| 293 | + |
| 294 | + if self.spectral_space: |
| 295 | + me_hat = self.spectral.u_init_forward |
| 296 | + me_hat[:] = self.transform(me) |
| 297 | + return me_hat |
| 298 | + else: |
| 299 | + return me |
| 300 | + |
| 301 | + def get_fig(self): # pragma: no cover |
| 302 | + """ |
| 303 | + Get a figure suitable to plot the solution of this problem |
| 304 | +
|
| 305 | + Returns |
| 306 | + ------- |
| 307 | + self.fig : matplotlib.pyplot.figure.Figure |
| 308 | + """ |
| 309 | + import matplotlib.pyplot as plt |
| 310 | + from mpl_toolkits.axes_grid1 import make_axes_locatable |
| 311 | + |
| 312 | + plt.rcParams['figure.constrained_layout.use'] = True |
| 313 | + self.fig, axs = plt.subplots(2, 1, sharex=True, sharey=True, figsize=((10, 5))) |
| 314 | + self.cax = [] |
| 315 | + divider = make_axes_locatable(axs[0]) |
| 316 | + self.cax += [divider.append_axes('right', size='3%', pad=0.03)] |
| 317 | + divider2 = make_axes_locatable(axs[1]) |
| 318 | + self.cax += [divider2.append_axes('right', size='3%', pad=0.03)] |
| 319 | + return self.fig |
| 320 | + |
| 321 | + def plot(self, u, t=None, fig=None, quantity='T'): # pragma: no cover |
| 322 | + r""" |
| 323 | + Plot the solution. |
| 324 | +
|
| 325 | + Parameters |
| 326 | + ---------- |
| 327 | + u : dtype_u |
| 328 | + Solution to be plotted |
| 329 | + t : float |
| 330 | + Time to display at the top of the figure |
| 331 | + fig : matplotlib.pyplot.figure.Figure |
| 332 | + Figure with the same structure as a figure generated by `self.get_fig`. If none is supplied, a new figure will be generated. |
| 333 | + quantity : (str) |
| 334 | + quantity you want to plot |
| 335 | +
|
| 336 | + Returns |
| 337 | + ------- |
| 338 | + None |
| 339 | + """ |
| 340 | + fig = self.get_fig() if fig is None else fig |
| 341 | + axs = fig.axes |
| 342 | + |
| 343 | + imV = axs[1].pcolormesh(self.X, self.Z, self.compute_vorticity(u).real) |
| 344 | + |
| 345 | + if self.spectral_space: |
| 346 | + u = self.itransform(u) |
| 347 | + |
| 348 | + imT = axs[0].pcolormesh(self.X, self.Z, u[self.index(quantity)].real) |
| 349 | + |
| 350 | + for i, label in zip([0, 1], [rf'${quantity}$', 'vorticity']): |
| 351 | + axs[i].set_aspect(1) |
| 352 | + axs[i].set_title(label) |
| 353 | + |
| 354 | + if t is not None: |
| 355 | + fig.suptitle(f't = {t:.2f}') |
| 356 | + axs[1].set_xlabel(r'$x$') |
| 357 | + axs[1].set_ylabel(r'$z$') |
| 358 | + fig.colorbar(imT, self.cax[0]) |
| 359 | + fig.colorbar(imV, self.cax[1]) |
0 commit comments