|
| 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 | + |
| 153 | + # compute rescaled Rayleigh number to extract viscosity and thermal diffusivity |
| 154 | + Ra = Rayleigh / (max([abs(BCs['T_top'] - BCs['T_bottom']), np.finfo(float).eps]) * self.axes[2].L ** 3) |
| 155 | + self.kappa = (Ra * Prandtl) ** (-1 / 2.0) |
| 156 | + self.nu = (Ra / Prandtl) ** (-1 / 2.0) |
| 157 | + |
| 158 | + # construct operators |
| 159 | + L_lhs = { |
| 160 | + 'p': {'u': U01 @ Dx, 'v': U01 @ Dy, 'w': Dz}, # divergence free constraint |
| 161 | + 'u': {'p': U02 @ Dx, 'u': -self.nu * (U02 @ (Dxx + Dyy) + Dzz)}, |
| 162 | + 'v': {'p': U02 @ Dy, 'v': -self.nu * (U02 @ (Dxx + Dyy) + Dzz)}, |
| 163 | + 'w': {'p': U12 @ Dz, 'w': -self.nu * (U02 @ (Dxx + Dyy) + Dzz), 'T': -U02 @ Id}, |
| 164 | + 'T': {'T': -self.kappa * (U02 @ (Dxx + Dyy) + Dzz)}, |
| 165 | + } |
| 166 | + self.setup_L(L_lhs) |
| 167 | + |
| 168 | + # mass matrix |
| 169 | + M_lhs = {i: {i: U02 @ Id} for i in ['u', 'v', 'w', 'T']} |
| 170 | + self.setup_M(M_lhs) |
| 171 | + |
| 172 | + # Prepare going from second (first for divergence free equation) derivative basis back to Chebychev-T |
| 173 | + self.base_change = self._setup_operator({**{comp: {comp: S2} for comp in ['u', 'v', 'w', 'T']}, 'p': {'p': S1}}) |
| 174 | + |
| 175 | + # BCs |
| 176 | + self.add_BC( |
| 177 | + component='p', equation='p', axis=2, v=self.BCs['p_integral'], kind='integral', line=-1, scalar=True |
| 178 | + ) |
| 179 | + self.add_BC(component='T', equation='T', axis=2, x=-1, v=self.BCs['T_bottom'], kind='Dirichlet', line=-1) |
| 180 | + self.add_BC(component='T', equation='T', axis=2, x=1, v=self.BCs['T_top'], kind='Dirichlet', line=-2) |
| 181 | + self.add_BC(component='w', equation='w', axis=2, x=1, v=self.BCs['w_top'], kind='Dirichlet', line=-1) |
| 182 | + self.add_BC(component='w', equation='w', axis=2, x=-1, v=self.BCs['w_bottom'], kind='Dirichlet', line=-2) |
| 183 | + self.remove_BC(component='w', equation='w', axis=2, x=-1, kind='Dirichlet', line=-2, scalar=True) |
| 184 | + for comp in ['u', 'v']: |
| 185 | + self.add_BC( |
| 186 | + component=comp, equation=comp, axis=2, v=self.BCs[f'{comp}_top'], x=1, kind='Dirichlet', line=-2 |
| 187 | + ) |
| 188 | + self.add_BC( |
| 189 | + component=comp, |
| 190 | + equation=comp, |
| 191 | + axis=2, |
| 192 | + v=self.BCs[f'{comp}_bottom'], |
| 193 | + x=-1, |
| 194 | + kind='Dirichlet', |
| 195 | + line=-1, |
| 196 | + ) |
| 197 | + |
| 198 | + # eliminate Nyquist mode if needed |
| 199 | + if nx % 2 == 0: |
| 200 | + Nyquist_mode_index = self.axes[0].get_Nyquist_mode_index() |
| 201 | + for component in self.components: |
| 202 | + self.add_BC( |
| 203 | + component=component, equation=component, axis=0, kind='Nyquist', line=int(Nyquist_mode_index), v=0 |
| 204 | + ) |
| 205 | + if ny % 2 == 0: |
| 206 | + Nyquist_mode_index = self.axes[0].get_Nyquist_mode_index() |
| 207 | + for component in self.components: |
| 208 | + self.add_BC( |
| 209 | + component=component, equation=component, axis=1, kind='Nyquist', line=int(Nyquist_mode_index), v=0 |
| 210 | + ) |
| 211 | + self.setup_BCs() |
| 212 | + |
| 213 | + self.work_counters['rhs'] = WorkCounter() |
| 214 | + |
| 215 | + def eval_f(self, u, *args, **kwargs): |
| 216 | + f = self.f_init |
| 217 | + |
| 218 | + if self.spectral_space: |
| 219 | + u_hat = u.copy() |
| 220 | + else: |
| 221 | + u_hat = self.transform(u) |
| 222 | + |
| 223 | + f_impl_hat = self.u_init_forward |
| 224 | + |
| 225 | + iu, iv, iw, iT, ip = self.index(['u', 'v', 'w', 'T', 'p']) |
| 226 | + |
| 227 | + # evaluate implicit terms |
| 228 | + if not hasattr(self, '_L_T_base'): |
| 229 | + self._L_T_base = self.base_change @ self.L |
| 230 | + f_impl_hat = -(self._L_T_base @ u_hat.flatten()).reshape(u_hat.shape) |
| 231 | + |
| 232 | + if self.spectral_space: |
| 233 | + f.impl[:] = f_impl_hat |
| 234 | + else: |
| 235 | + f.impl[:] = self.itransform(f_impl_hat).real |
| 236 | + |
| 237 | + # ------------------------------------------- |
| 238 | + # treat convection explicitly with dealiasing |
| 239 | + |
| 240 | + # start by computing derivatives |
| 241 | + if not hasattr(self, '_Dx_expanded') or not hasattr(self, '_Dz_expanded'): |
| 242 | + Dz = self.Dz |
| 243 | + Dy = self.Dy |
| 244 | + Dx = self.Dx |
| 245 | + |
| 246 | + self._Dx_expanded = self._setup_operator( |
| 247 | + {'u': {'u': Dx}, 'v': {'v': Dx}, 'w': {'w': Dx}, 'T': {'T': Dx}, 'p': {}} |
| 248 | + ) |
| 249 | + self._Dy_expanded = self._setup_operator( |
| 250 | + {'u': {'u': Dy}, 'v': {'v': Dy}, 'w': {'w': Dy}, 'T': {'T': Dy}, 'p': {}} |
| 251 | + ) |
| 252 | + self._Dz_expanded = self._setup_operator( |
| 253 | + {'u': {'u': Dz}, 'v': {'v': Dz}, 'w': {'w': Dz}, 'T': {'T': Dz}, 'p': {}} |
| 254 | + ) |
| 255 | + Dx_u_hat = (self._Dx_expanded @ u_hat.flatten()).reshape(u_hat.shape) |
| 256 | + Dy_u_hat = (self._Dy_expanded @ u_hat.flatten()).reshape(u_hat.shape) |
| 257 | + Dz_u_hat = (self._Dz_expanded @ u_hat.flatten()).reshape(u_hat.shape) |
| 258 | + |
| 259 | + padding = (self.dealiasing,) * self.ndim |
| 260 | + Dx_u_pad = self.itransform(Dx_u_hat, padding=padding).real |
| 261 | + Dy_u_pad = self.itransform(Dy_u_hat, padding=padding).real |
| 262 | + Dz_u_pad = self.itransform(Dz_u_hat, padding=padding).real |
| 263 | + u_pad = self.itransform(u_hat, padding=padding).real |
| 264 | + |
| 265 | + fexpl_pad = self.xp.zeros_like(u_pad) |
| 266 | + fexpl_pad[iu][:] = -(u_pad[iu] * Dx_u_pad[iu] + u_pad[iv] * Dy_u_pad[iu] + u_pad[iw] * Dz_u_pad[iu]) |
| 267 | + fexpl_pad[iv][:] = -(u_pad[iu] * Dx_u_pad[iv] + u_pad[iv] * Dy_u_pad[iv] + u_pad[iw] * Dz_u_pad[iv]) |
| 268 | + fexpl_pad[iw][:] = -(u_pad[iu] * Dx_u_pad[iw] + u_pad[iv] * Dy_u_pad[iw] + u_pad[iw] * Dz_u_pad[iw]) |
| 269 | + fexpl_pad[iT][:] = -(u_pad[iu] * Dx_u_pad[iT] + u_pad[iv] * Dy_u_pad[iT] + u_pad[iw] * Dz_u_pad[iT]) |
| 270 | + |
| 271 | + if self.spectral_space: |
| 272 | + f.expl[:] = self.transform(fexpl_pad, padding=padding) |
| 273 | + else: |
| 274 | + f.expl[:] = self.itransform(self.transform(fexpl_pad, padding=padding)).real |
| 275 | + |
| 276 | + self.work_counters['rhs']() |
| 277 | + return f |
| 278 | + |
| 279 | + def u_exact(self, t=0, noise_level=1e-3, seed=99): |
| 280 | + assert t == 0 |
| 281 | + assert ( |
| 282 | + self.BCs['v_top'] == self.BCs['v_bottom'] |
| 283 | + ), 'Initial conditions are only implemented for zero velocity gradient' |
| 284 | + |
| 285 | + me = self.spectral.u_init |
| 286 | + iu, iw, iT, ip = self.index(['u', 'w', 'T', 'p']) |
| 287 | + |
| 288 | + # linear temperature gradient |
| 289 | + assert self.Lz == 1 |
| 290 | + for comp in ['T', 'u', 'v', 'w']: |
| 291 | + a = self.BCs[f'{comp}_top'] - self.BCs[f'{comp}_bottom'] |
| 292 | + b = self.BCs[f'{comp}_bottom'] |
| 293 | + me[self.index(comp)] = a * self.Z + b |
| 294 | + |
| 295 | + # perturb slightly |
| 296 | + rng = self.xp.random.default_rng(seed=seed) |
| 297 | + |
| 298 | + noise = self.spectral.u_init |
| 299 | + noise[iT] = rng.random(size=me[iT].shape) |
| 300 | + |
| 301 | + me[iT] += noise[iT].real * noise_level * (self.Z - 1) * (self.Z + 1) |
| 302 | + |
| 303 | + if self.spectral_space: |
| 304 | + me_hat = self.spectral.u_init_forward |
| 305 | + me_hat[:] = self.transform(me) |
| 306 | + return me_hat |
| 307 | + else: |
| 308 | + return me |
0 commit comments