def computeSource_Smagorinsky_SGS(self, C1=-6.39e-2, **ignored): """ SPARSE SYMMETRIC TENSOR INDEXING: m == 0 -> ij == 00 m == 1 -> ij == 01 m == 2 -> ij == 02 m == 3 -> ij == 11 m == 4 -> ij == 12 m == 5 -> ij == 22 """ # -------------------------------------------------------------- # Explicitly filter the solution field self.W_hat[:] = self.les_filter*self.U_hat # -------------------------------------------------------------- # Compute S_ij and |S|^2 S_sqr = self.W[0] S_sqr[:] = 0.0 m = 0 for i in range(3): for j in range(i, 3): self.Aij[:] = 0.5j*self.K[j]*self.W_hat[i] if i == j: self.S[m] = irfft3(self.comm, 2*self.Aij) S_sqr += self.S[m]**2 else: self.Aji[:] = 0.5j*self.K[i]*self.W_hat[j] self.S[m] = irfft3(self.comm, self.Aij + self.Aji) S_sqr += 2*self.S[m]**2 m+=1 # -------------------------------------------------------------- # Compute C_1 Delta^2 |S| S_ij coef = self.W[1] coef[:] = C1*self.D_les**2*np.sqrt(2.0*S_sqr) # -------------------------------------------------------------- # Compute FFT{div(tau)} and add to RHS update m = 0 for i in range(3): for j in range(i, 3): rfft3(self.comm, coef*self.S[m], self.tau_ij_hat) self.dU[i] -= 1j*self.K[j]*self.tau_ij_hat if i != j: self.dU[j] -= 1j*self.K[i]*self.tau_ij_hat m+=1 return
def compute_pressure(self): K = self.K U_hat = self.U_hat U = self.U omega = self.omega comm = self.comm P_hat = np.empty_like(U_hat) P = np.empty_like(U) # -------------------------------------------------------------- # take curl of velocity and inverse transform to get vorticity irfft3(comm, 1j * (K[1] * U_hat[2] - K[2] * U_hat[1]), omega[0]) irfft3(comm, 1j * (K[2] * U_hat[0] - K[0] * U_hat[2]), omega[1]) irfft3(comm, 1j * (K[0] * U_hat[1] - K[1] * U_hat[0]), omega[2]) # -------------------------------------------------------------- # compute the convective transport as the physical-space # cross-product of vorticity and velocity rfft3(comm, U[1] * omega[2] - U[2] * omega[1], self.dU[0]) rfft3(comm, U[2] * omega[0] - U[0] * omega[2], self.dU[1]) rfft3(comm, U[0] * omega[1] - U[1] * omega[0], self.dU[2]) P_hat = -1j * np.sum(self.dU * self.K_Ksq, axis=0) irfft3(comm, P_hat, P) return P
def computeSource_Smagorinksy_SGS(self, Cs=1.2, **ignored): """ Smagorinsky Model (takes Cs as input) Takes one keyword argument: Cs: (float, optional), Smagorinsky constant """ self.W_hat[:] = self.les_filter*self.U_hat for i in range(3): for j in range(3): self.A[j, i] = 0.5*irfft3(self.comm, 1j*(self.K[j]*self.W_hat[i] +self.K[i]*self.W_hat[j])) # compute SGS flux tensor, nuT = 2|S|(Cs*D)**2 nuT = self.W[0] nuT = np.sqrt(2.0*np.sum(np.square(self.A), axis=(0, 1))) nuT*= 2.0*(Cs*self.D_les)**2 self.W_hat[:] = 0.0 for i in range(3): for j in range(3): self.W_hat[i]+= 1j*self.K[j]*rfft3(self.comm, self.A[j, i]*nuT) self.dU += self.W_hat return
def computeSource_Smagorinsky_SGS(self, Cs=1.2, **ignored): """ Smagorinsky Model (takes Cs as input) Takes one keyword argument: Cs: (float, optional), Smagorinsky constant """ # -------------------------------------------------------------- # Explicitly filter the solution field self.W_hat[:] = self.les_filter * self.U_hat for i in range(3): for j in range(3): self.S[i, j] = irfft3( self.comm, 1j * self.K[j] * self.W_hat[i] + 1j * self.K[i] * self.W_hat[j]) # -------------------------------------------------------------- # compute the leading coefficient, nu_T = 2|S|(Cs*D)**2 nuT = self.W[0] nuT[:] = np.sqrt(np.sum(np.square(self.S), axis=(0, 1))) nuT *= (Cs * self.D_les)**2 # -------------------------------------------------------------- # Compute FFT{div(tau)} and add to RHS update self.W_hat[:] = 0.0 for i in range(3): for j in range(3): self.W_hat[i] += 1j * self.K[j] * rfft3( self.comm, nuT * self.S[i, j]) self.dU += self.W_hat return
def filter_kernel(self, kf, Gtype='spectral', k_kf=None, dtype=np.complex128): """ kf - input cutoff wavenumber for ensured isotropic filtering Gtype - (Default='spectral') filter kernel type k_kf - (Default=None) spectral-space wavenumber field pre- normalized by filter cutoff wavenumber. Pass this into FILTER_KERNEL for anisotropic filtering since this function generates isotropic filter kernels by default. If not None, kf is ignored. """ if k_kf is None: A = self.L / self.L.min() # domain size aspect ratios A.resize((3, 1, 1, 1)) # ensure proper array broadcasting kmag = np.sqrt(np.sum(np.square(self.K / A), axis=0)) k_kf = kmag / kf Ghat = np.empty(k_kf.shape, dtype=dtype) if Gtype == 'spectral': Ghat[:] = (np.abs(k_kf) < 1.0).astype(dtype) elif Gtype == 'tophat': Ghat[:] = np.sin(pi * k_kf) / (pi * k_kf**2) elif Gtype == 'comp_exp': # A Compact Exponential filter that: # 1) has compact support in _both_ physical and spectral space # 2) is strictly positive in _both_ spaces # 3) is smooth (infinitely differentiable) in _both_ spaces # 4) has simply-connected support in spectral space with # an outer radius kf, and # 5) has disconnected (lobed) support in physical space # with an outer radius of 2*pi/kf with np.errstate(divide='ignore'): Ghat[:] = np.exp(-k_kf**2 / (0.25 - k_kf**2), where=k_kf < 0.5, out=np.zeros_like(k_kf)).astype(dtype) G = irfft3(self.comm, Ghat) G[:] = np.square(G) rfft3(self.comm, G, Ghat) Ghat *= 1.0 / self.comm.allreduce(Ghat[0, 0, 0], op=MPI.MAX) Ghat -= 1j * np.imag(Ghat) elif Gtype == 'inv_comp_exp': # Same as 'comp_exp' but the physical-space and # spectral-space kernels are swapped so that the # physical-space support is a simply-connected ball raise ValueError('inv_comp_exp not yet implemented!') else: raise ValueError('did not understand filter type') return Ghat
def filter_kernel(self, kf, Gtype='spectral', k_kf=None, dtype=np.complex128): """ kf - input cutoff wavenumber for ensured isotropic filtering Gtype - (Default='spectral') filter kernel type k_kf - (Default=None) spectral-space wavenumber field pre- normalized by filter cutoff wavenumber. Pass this into FILTER_KERNEL for anisotropic filtering since this function generates isotropic filter kernels by default. If not None, kf is ignored. """ if k_kf is None: A = self.L/self.L.min() # domain size aspect ratios A.resize((3, 1, 1, 1)) # ensure proper array broadcasting kmag = np.sqrt(np.sum(np.square(self.K/A), axis=0)) k_kf = kmag/kf Ghat = np.empty(k_kf.shape, dtype=dtype) if Gtype == 'spectral': Ghat[:] = (np.abs(k_kf) < 1.0).astype(dtype) elif Gtype == 'comp_exp': # A 'COMPact EXPonential' filter which: # 1) has compact support in a ball of spectral (physical) # radius kf (1/kf) # 2) is strictly positive, and # 3) is smooth (infinitely differentiable) # in _both_ physical and spectral space! with np.errstate(divide='ignore'): Ghat[:] = np.exp(-k_kf**2/(0.25-k_kf**2), where=k_kf < 0.5, out=np.zeros_like(k_kf)).astype(dtype) G = irfft3(self.comm, Ghat) G[:] = np.square(G) rfft3(self.comm, G, Ghat) Ghat *= 1.0/self.comm.allreduce(Ghat[0, 0, 0], op=MPI.MAX) Ghat -= 1j*np.imag(Ghat) # elif Gtype == 'inv_comp_exp': # # Same as 'comp_exp' but the physical-space and spectral- # # space kernels are swapped so that the physical-space kernel # # has only a central lobe of support. # H = np.exp(-r_rf**2/(1.0-r_rf**2)) # G = np.where(r_rf < 1.0, H, 0.0) # rfft3(self.comm, G, Ghat) # Ghat[:] = Ghat**2 # G[:] = irfft3(self.comm, Ghat) # G /= self.comm.allreduce(psum(G), op=MPI.SUM) # rfft3(self.comm, G, Ghat) elif Gtype == 'tophat': Ghat[:] = np.sin(pi*k_kf)/(pi*k_kf**2) else: raise ValueError('did not understand filter type') return Ghat
def compute_pressure(self): K = self.K U_hat = self.U_hat U = self.U omega = self.omega comm = self.comm # P_hat = np.empty_like(U_hat) # this is a vector field! # P = np.empty_like(U) # this is a vector field! # -------------------------------------------------------------- # take curl of velocity and inverse transform to get vorticity irfft3(comm, 1j * (K[1] * U_hat[2] - K[2] * U_hat[1]), omega[0]) irfft3(comm, 1j * (K[2] * U_hat[0] - K[0] * U_hat[2]), omega[1]) irfft3(comm, 1j * (K[0] * U_hat[1] - K[1] * U_hat[0]), omega[2]) # -------------------------------------------------------------- # compute the convective transport as the physical-space # cross-product of vorticity and velocity rfft3(comm, U[1] * omega[2] - U[2] * omega[1], self.dU[0]) rfft3(comm, U[2] * omega[0] - U[0] * omega[2], self.dU[1]) rfft3(comm, U[0] * omega[1] - U[1] * omega[0], self.dU[2]) P_hat = -1j * np.sum( self.dU * self.K_Ksq, axis=0 ) # this doesn't fill P_hat, it points the name "P_hat" to a new scalar field array! # irfft3(comm, P_hat, P) this is the wrong array size (see above)! return irfft3(comm, P_hat, self.Pres)
def computeSource_HIT_random_forcing(self, rseed=None, **ignored): """ Source function to be added to spectralLES solver instance Takes one keyword argument: rseed: (positive integer, optional), changes the random seed of the pseudo-RNG inside the np.random module """ mpi_reduce = self.comm.allreduce # -------------------------------------------------------------- # generate a random, band-pass-filtered velocity field self.compute_random_HIT_spectrum(-5. / 3., self.nk[-1], rseed) self.W_hat *= self.forcing_filter # -------------------------------------------------------------- # scale to constant energy injection rate irfft3(self.comm, self.W_hat[0], self.W[0]) irfft3(self.comm, self.W_hat[1], self.W[1]) irfft3(self.comm, self.W_hat[2], self.W[2]) dvScale = self.epsilon * self.Nx / mpi_reduce(psum(self.W * self.U)) self.W_hat *= dvScale # -------------------------------------------------------------- # add forcing term to the RHS accumulator self.dU += self.W_hat return dvScale
def computeSource_linear_forcing(self, dvScale=None, computeRHS=True, **ignored): """ Source function to be added to spectralLES solver instance inclusion of keyword dvScale necessary to actually compute the source term Takes one keyword argument: dvScale: (optional) user-provided linear scaling computeRHS: (default=True) add source term to RHS accumulator """ # Update the HIT forcing function self.W_hat[:] = self.U_hat*self.hit_filter if dvScale is None: irfft3(self.comm, self.W_hat[0], self.W[0]) irfft3(self.comm, self.W_hat[1], self.W[1]) irfft3(self.comm, self.W_hat[2], self.W[2]) dvScale = self.epsilon*self.Nx/self.comm.allreduce( psum(self.U*self.W)) if computeRHS: self.dU += dvScale*self.W_hat return dvScale
def initialize_HIT_random_spectrum(self, Einit=None, kexp=-5./6., kpeak=None, rseed=None): """ Generates a random, incompressible, velocity initial condition with a scaled Gamie-Ostriker isotropic turbulence spectrum """ if Einit is None: Einit = 0.72*(self.epsilon*self.L.max())**(2./3.) # the constant of 0.72 is empirically-based if kpeak is None: a = self.L/self.L.min() # domain size aspect ratios kpeak = np.max((self.nx//8)/a) # this gives kmax/4 self.compute_random_HIT_spectrum(kexp, kpeak, rseed) # Solenoidally-project, U_hat*(1-ki*kj/k^2) self.W_hat -= np.sum(self.W_hat*self.K_Ksq, axis=0)*self.K # - Third, scale to Einit irfft3(self.comm, self.W_hat[0], self.U[0]) irfft3(self.comm, self.W_hat[1], self.U[1]) irfft3(self.comm, self.W_hat[2], self.U[2]) Urms = sqrt(2.0*Einit) self.U *= Urms*sqrt(self.Nx/self.comm.allreduce(psum(self.U**2))) # transform to finish initial conditions rfft3(self.comm, self.U[0], self.U_hat[0]) rfft3(self.comm, self.U[1], self.U_hat[1]) rfft3(self.comm, self.U[2], self.U_hat[2]) return
def computeSource_linear_forcing(self, dvScale=None, computeRHS=True, **ignored): """ Source function to be added to spectralLES solver instance inclusion of keyword dvScale necessary to actually compute the source term Takes one keyword argument: dvScale: (optional) user-provided linear scaling computeRHS: (default=True) add source term to RHS accumulator """ mpi_reduce = self.comm.allreduce # -------------------------------------------------------------- # Band-pass filter the solution velocity field self.W_hat[:] = self.U_hat*self.forcing_filter # -------------------------------------------------------------- # scale to constant energy injection rate if dvScale is None: irfft3(self.comm, self.W_hat[0], self.W[0]) irfft3(self.comm, self.W_hat[1], self.W[1]) irfft3(self.comm, self.W_hat[2], self.W[2]) dvScale = self.epsilon*self.Nx/mpi_reduce(psum(self.U*self.W)) # adding 0.0 to turn off the forcing function! self.W_hat *= dvScale # -------------------------------------------------------------- # add forcing term to the RHS accumulator if computeRHS: self.dU += self.W_hat return dvScale
def computeAD_vorticity_form(self, **ignored): """ Computes right-hand-side (RHS) advection and diffusion term of the incompressible Navier-Stokes equations using a vorticity formulation for the advection term. This function overwrites the previous contents of self.dU. """ K = self.K U_hat = self.U_hat U = self.U omega = self.omega comm = self.comm # --------------------------------------------------------------# # take curl of velocity and inverse transform to get vorticity # # --------------------------------------------------------------# irfft3(comm, 1j * (K[1] * U_hat[2] - K[2] * U_hat[1]), omega[0]) irfft3(comm, 1j * (K[2] * U_hat[0] - K[0] * U_hat[2]), omega[1]) irfft3(comm, 1j * (K[0] * U_hat[1] - K[1] * U_hat[0]), omega[2]) # -------------------------------------------------------------- # compute the convective transport as the physical-space # cross-product of vorticity and velocity rfft3(comm, U[1] * omega[2] - U[2] * omega[1], self.dU[0]) rfft3(comm, U[2] * omega[0] - U[0] * omega[2], self.dU[1]) rfft3(comm, U[0] * omega[1] - U[1] * omega[0], self.dU[2]) # -------------------------------------------------------------- # add the diffusive transport term self.dU -= self.nu * self.Ksq * self.U_hat return
def computeSource_ales244_SGS(self, H_244, **ignored): """ H_244 - ALES coefficients h_ij for 244-term Volterra series truncation. H_244.shape = (6, 244) """ tau_hat = self.tau_hat UU_hat = self.UU_hat irfft3(self.comm, self.les_filter * self.U_hat[0], self.W[0]) irfft3(self.comm, self.les_filter * self.U_hat[1], self.W[1]) irfft3(self.comm, self.les_filter * self.U_hat[2], self.W[2]) m = 0 for j in range(3): for i in range(j, 3): rfft3(self.comm, self.W[i] * self.W[j], UU_hat[m]) m += 1 # loop over 6 stress tensor components for m in range(6): tau_hat[5 - m] = H_244[m, 0] # constant coefficient # loop over 27 stencil points n = 1 for z in range(-1, 2): for y in range(-1, 2): for x in range(-1, 2): # compute stencil shift operator. # NOTE: dx = 2*pi/N for standard incompressible HIT # but really shift theorem needs 2*pi/N, not dx pos = np.array([z, y, x]) * self.dx pos.resize((3, 1, 1, 1)) shift = np.exp(1j * np.sum(self.K * pos, axis=0)) # 3 ui Volterra series components for i in range(2, 0, -1): tau_hat[5 - m] += H_244[m, n] * shift * self.U_hat[i] n += 1 # 6 uiuj collocated Volterra series components for p in range(6): tau_hat[5 - m] += H_244[m, n] * shift * UU_hat[5 - p] n += 1 self.W_hat[:] = 0.0 m = 0 for j in range(3): for i in range(j, 3): self.W_hat[i] += 1j * (1 + (i != j)) * self.K[j] * tau_hat[m] m += 1 self.dU += self.W_hat return
def compute_dvScale_constant_injection(self): """ empty docstring! """ mpi_reduce = self.comm.allreduce # Band-pass filter the solution velocity field self.W_hat[:] = self.U_hat * self.forcing_filter # scale to constant energy injection rate irfft3(self.comm, self.W_hat[0], self.W[0]) irfft3(self.comm, self.W_hat[1], self.W[1]) irfft3(self.comm, self.W_hat[2], self.W[2]) dvScale = self.epsilon * self.Nx / mpi_reduce(psum(self.U * self.W)) return dvScale
def computeSource_HIT_random_forcing(self, rseed=None, **ignored): """ Source function to be added to spectralLES solver instance Takes one keyword argument: rseed: (positive integer, optional), changes the random seed of the pseudo-RNG inside the np.random module """ self.compute_random_HIT_spectrum(-5./3., self.nk[-1], rseed) self.W_hat *= self.hit_filter irfft3(self.comm, self.W_hat[0], self.W[0]) irfft3(self.comm, self.W_hat[1], self.W[1]) irfft3(self.comm, self.W_hat[2], self.W[2]) dvScale = self.epsilon/self.comm.allreduce(psum(self.W*self.U)) self.W_hat *= dvScale self.dU += self.W_hat return dvScale
def RK4_integrate(self, dt, *Sources, **kwargs): """ 4th order Runge-Kutta time integrator for spectralLES Arguments: ---------- dt: current timestep *Sources: (Optional) User-supplied source terms. This is a special Python syntax, basically any argument you feed RK4_integrate() after dt will be stored in the list Source_terms. If no arguments are given, Sources = [], in which case the loop is skipped. **kwargs: (Optional) the keyword arguments to be passed to all Sources. """ a = [1./6., 1./3., 1./3., 1./6.] b = [0.5, 0.5, 1.] self.U_hat1[:] = self.U_hat0[:] = self.U_hat for rk in range(4): irfft3(self.comm, self.U_hat[0], self.U[0]) irfft3(self.comm, self.U_hat[1], self.U[1]) irfft3(self.comm, self.U_hat[2], self.U[2]) self.computeAD(**kwargs) for computeSource in Sources: computeSource(**kwargs) # Filter the nonlinear contributions to the RHS self.dU *= self.dealias # Apply the Leray-Hopf projection operator (1 - Helmholtz # operator) to filtered nonlinear contributions in order to # enforce the divergence-free continuity condition. # This operation is equivalent to computing the pressure # field using a physical-space pressure-Poisson solver and # then adding the pressure-gradient transport term to the RHS. self.dU -= np.sum(self.dU*self.K_Ksq, axis=0)*self.K if rk < 3: self.U_hat[:] = self.U_hat0 + b[rk]*dt*self.dU self.U_hat1[:] += a[rk]*dt*self.dU irfft3(self.comm, self.U_hat[0], self.U[0]) irfft3(self.comm, self.U_hat[1], self.U[1]) irfft3(self.comm, self.U_hat[2], self.U[2]) return
def computeSource_ales244_SGS(self, H_244, **ignored): """ h_ij Fortran column-major ordering: 11,12,13,22,23,33 equivalent ordering for spectralLES: 22,21,20,11,10,00 sparse tensor indexing for ales244_solver UU_hat and tau_hat: m == 0 -> ij == 22 m == 1 -> ij == 21 m == 2 -> ij == 20 m == 3 -> ij == 11 m == 4 -> ij == 10 m == 5 -> ij == 00 H_244 - ALES coefficients h_ij for 244-term Volterra series truncation. H_244.shape = (6, 244) """ tau_hat = self.tau_hat UU_hat = self.UU_hat W_hat = self.W_hat W_hat[:] = self.les_filter * self.U_hat irfft3(self.comm, W_hat[0], self.W[0]) irfft3(self.comm, W_hat[1], self.W[1]) irfft3(self.comm, W_hat[2], self.W[2]) m = 0 for i in range(2, -1, -1): for j in range(i, -1, -1): rfft3(self.comm, self.W[i] * self.W[j], UU_hat[m]) m += 1 # loop over 6 stress tensor components for m in range(6): tau_hat[m] = H_244[m, 0] # constant coefficient # loop over 27 stencil points n = 1 for z in range(-1, 2): for y in range(-1, 2): for x in range(-1, 2): # compute stencil shift operator. # NOTE: dx = 2*pi/N for standard incompressible HIT # but really shift theorem needs 2*pi/N, not dx pos = np.array([z, y, x]) * self.dx pos.resize((3, 1, 1, 1)) shift = np.exp(1j * np.sum(self.K * pos, axis=0)) # 3 ui Volterra series components for i in range(2, -1, -1): tau_hat[m] += H_244[m, n] * shift * W_hat[i] n += 1 # 6 uiuj collocated Volterra series components for p in range(6): tau_hat[m] += H_244[m, n] * shift * UU_hat[p] n += 1 m = 0 for i in range(2, -1, -1): for j in range(i, -1, -1): self.dU[i] -= 1j * self.K[j] * tau_hat[m] if i != j: self.dU[j] -= 1j * self.K[i] * tau_hat[m] m += 1 return
def computeSource_4termGEV_SGS(self, C=None, **ignored): """ Empty Docstring! """ # -------------------------------------------------------------- # Explicitly filter the solution field self.W_hat[:] = self.les_filter*self.U_hat # -------------------------------------------------------------- # Compute S_ij, R_ij, S_kl S_kl, R_kl R_kl, and |S| S = self.S R = self.R S_mod = self.W[0] S_sqr = self.W[1] R_sqr = self.W[2] S_sqr[:] = 0.0 R_sqr[:] = 0.0 for i in range(3): for j in range(3): self.Aij[:] = 0.5j*self.K[j]*self.W_hat[i] if i == j: S[i, j] = irfft3(self.comm, 2*self.Aij) R[i, j] = 0.0 else: self.Aji[:] = 0.5j*self.K[i]*self.W_hat[j] S[i, j] = irfft3(self.comm, self.Aij + self.Aji) R[i, j] = irfft3(self.comm, self.Aij - self.Aji) S_sqr += S[i, j]**2 R_sqr += R[i, j]**2 S_mod[:] = np.sqrt(2.0*S_sqr) # -------------------------------------------------------------- # Compute tau_ij = Delta**2 C_m G_ij^m and update RHS for i in range(3): for j in range(3): # G_ij^1 = |S| S_ij self.tau_ij[:] = C[0]*S_mod*S[i, j] # G_ij^2 = -(S_ik R_jk + R_ik S_jk) self.tau_ij -= C[1]*np.sum(S[i]*R[j] + S[j]*R[i], axis=0) # G_ij^3 = S_ik S_jk - 1/3 delta_ij S_kl S_kl self.tau_ij += C[2]*np.sum(S[i]*S[j], axis=0) if i == j: self.tau_ij -= C[2]*(1/3)*S_sqr # G_ij^4 = - R_ik R_jk - 1/3 delta_ij R_kl R_kl self.tau_ij -= C[3]*np.sum(R[i]*R[j], axis=0) if i == j: self.tau_ij -= C[3]*(1/3)*R_sqr rfft3(self.comm, self.tau_ij, self.tau_ij_hat) self.dU[i] -= 1j*self.K[j]*self.tau_ij_hat return
def ales244_static_les_test(pp=None, sp=None): """ Arguments: ---------- pp: (optional) program parameters, parsed by argument parser provided by this file sp: (optional) solver parameters, parsed by spectralLES.parser """ if comm.rank == 0: print("\n----------------------------------------------------------") print("MPI-parallel Python spectralLES simulation of problem \n" "`Homogeneous Isotropic Turbulence' started with " "{} tasks at {}.".format(comm.size, timeofday())) print("----------------------------------------------------------") # if function called without passing in parsed arguments, then parse # the arguments from the command line if pp is None: pp = hit_parser.parse_known_args()[0] if sp is None: sp = spectralLES.parser.parse_known_args()[0] if comm.rank == 0: print('\nProblem Parameters:\n-------------------') for k, v in vars(pp).items(): print(k, v) print('\nSpectralLES Parameters:\n-----------------------') for k, v in vars(sp).items(): print(k, v) print("\n----------------------------------------------------------\n") assert len(set(pp.N)) == 1, ('Error, this beta-release HIT program ' 'requires equal mesh dimensions') N = pp.N[0] assert len(set(pp.L)) == 1, ('Error, this beta-release HIT program ' 'requires equal domain dimensions') L = pp.L[0] if N % comm.size > 0: if comm.rank == 0: print('Error: job started with improper number of MPI tasks for ' 'the size of the data specified!') MPI.Finalize() sys.exit(1) # ------------------------------------------------------------------------- # Configure the solver, writer, and analyzer # -- construct solver instance from sp's attribute dictionary solver = ales244_solver(comm, **vars(sp)) U_hat = solver.U_hat U = solver.U omega = solver.omega K = solver.K # -- configure solver instance to solve the NSE with the vorticity # formulation of the advective term, linear forcing, and # the ales244 SGS model solver.computeAD = solver.computeAD_vorticity_form Sources = [ solver.computeSource_linear_forcing, solver.computeSource_ales244_SGS ] H_244 = np.loadtxt('h_ij.dat', usecols=(1, 2, 3, 4, 5, 6), unpack=True) kwargs = {'H_244': H_244, 'dvScale': None} # -- form HIT initial conditions from either user-defined values or # physics-based relationships using epsilon and L Urms = 1.2 * (pp.epsilon * L)**(1. / 3.) # empirical coefficient Einit = getattr(pp, 'Einit', None) or Urms**2 # == 2*KE_equilibrium kexp = getattr(pp, 'kexp', None) or -1. / 3. # -> E(k) ~ k^(-2./3.) kpeak = getattr(pp, 'kpeak', None) or N // 4 # ~ kmax/2 # -- currently using a fixed random seed of comm.rank for testing solver.initialize_HIT_random_spectrum(Einit, kexp, kpeak, rseed=comm.rank) # -- configure the writer and analyzer from both pp and sp attributes writer = mpiWriter(comm, odir=pp.odir, N=N) analyzer = mpiAnalyzer(comm, odir=pp.adir, pid=pp.pid, L=L, N=N, config='hit', method='spectral') Ek_fmt = "\widehat{{{0}}}^*\widehat{{{0}}}".format # ------------------------------------------------------------------------- # Setup the various time and IO counters tauK = sqrt(pp.nu / pp.epsilon) # Kolmogorov time-scale taul = 0.2 * L * sqrt(3) / Urms # 0.2 is empirical coefficient c = pp.cfl * sqrt(2 * Einit) / Urms dt = solver.new_dt_constant_nu(c) # use as estimate if pp.tlimit == np.Inf: # put a very large but finite limit on the run pp.tlimit = 262 * taul # such as (256+6)*tau, for spinup and 128 samples dt_rst = getattr(pp, 'dt_rst', None) or 2 * taul dt_spec = getattr(pp, 'dt_spec', None) or max(0.1 * taul, tauK, 10 * dt) dt_drv = getattr(pp, 'dt_drv', None) or max(tauK, 10 * dt) t_sim = t_rst = t_spec = t_drv = 0.0 tstep = irst = ispec = 0 # ------------------------------------------------------------------------- # Run the simulation while t_sim < pp.tlimit + 1.e-8: # -- Update the dynamic dt based on CFL constraint dt = solver.new_dt_constant_nu(pp.cfl) t_test = t_sim + 0.5 * dt # -- output log messages every step if needed/wanted KE = 0.5 * comm.allreduce(psum(np.square(U))) / solver.Nx if comm.rank == 0: print("cycle = %7d time = %15.8e dt = %15.8e KE = %15.8e" % (tstep, t_sim, dt, KE)) # - output snapshots and data analysis products if t_test >= t_spec: analyzer.spectral_density(U_hat, '%3.3d_u' % ispec, 'velocity PSD\t%s' % Ek_fmt('u_i')) irfft3(comm, 1j * (K[0] * U_hat[1] - K[1] * U_hat[0]), omega[2]) irfft3(comm, 1j * (K[2] * U_hat[0] - K[0] * U_hat[2]), omega[1]) irfft3(comm, 1j * (K[1] * U_hat[2] - K[2] * U_hat[1]), omega[0]) analyzer.spectral_density(omega, '%3.3d_omga' % ispec, 'vorticity PSD\t%s' % Ek_fmt('\omega_i')) t_spec += dt_spec ispec += 1 if t_test >= t_rst: writer.write_scalar('Velocity1_%3.3d.rst' % irst, U[0], np.float64) writer.write_scalar('Velocity2_%3.3d.rst' % irst, U[1], np.float64) writer.write_scalar('Velocity3_%3.3d.rst' % irst, U[2], np.float64) t_rst += dt_rst irst += 1 # -- Update the forcing pattern if t_test >= t_drv: # call solver.computeSource_linear_forcing to compute dvScale only kwargs['dvScale'] = Sources[0](computeRHS=False) t_drv += dt_drv if comm.rank == 0: print("------ updated linear forcing pattern ------") # -- integrate the solution forward in time solver.RK4_integrate(dt, *Sources, **kwargs) t_sim += dt tstep += 1 sys.stdout.flush() # forces Python 3 to flush print statements # ------------------------------------------------------------------------- # Finalize the simulation irfft3(comm, 1j * (K[0] * U_hat[1] - K[1] * U_hat[0]), omega[2]) irfft3(comm, 1j * (K[2] * U_hat[0] - K[0] * U_hat[2]), omega[1]) irfft3(comm, 1j * (K[1] * U_hat[2] - K[2] * U_hat[1]), omega[0]) analyzer.spectral_density(U_hat, '%3.3d_u' % ispec, 'velocity PSD\t%s' % Ek_fmt('u_i')) analyzer.spectral_density(omega, '%3.3d_omga' % ispec, 'vorticity PSD\t%s' % Ek_fmt('\omega_i')) writer.write_scalar('Velocity1_%3.3d.rst' % irst, U[0], np.float64) writer.write_scalar('Velocity2_%3.3d.rst' % irst, U[1], np.float64) writer.write_scalar('Velocity3_%3.3d.rst' % irst, U[2], np.float64) return
def taylor_green_vortex(): if comm.rank == 0: print("Python MPI spectral DNS simulation of problem " "`Taylor-Green vortex' started with " "{} tasks at {}.".format(comm.size, timeofday())) # ------------------------------------------------------------------------- L = 2 * np.pi N = 64 nu = 0.000625 if N % comm.size > 0: if comm.rank == 0: print('Job started with improper number of MPI tasks for the ' 'size of the data specified!') MPI.Finalize() sys.exit(1) solver = spectralLES(comm, N, L, nu, epsilon=0, Gtype='spectral') kmax_dealias = (2. / 3.) * (N // 2 + 1) solver.les_filter = np.array( (abs(solver.K[0]) < kmax_dealias) * (abs(solver.K[1]) < kmax_dealias) * (abs(solver.K[2]) < kmax_dealias), dtype=np.int8) sys.stdout.flush() # forces Python 3 to flush print statements # ------------------------------------------------------------------------- t = 0.0 dt = 0.01 tlimit = 1.0 tstep = 0 start = time.time() solver.initialize_Taylor_Green_vortex() solver.computeAD = solver.computeAD_vorticity_form while t < tlimit - 1.e-8: k = comm.reduce(0.5 * np.sum(np.square(solver.U) * (1. / N)**3)) if comm.rank == 0: print('cycle = %2.0d, KE = %12.10f' % (tstep, k)) t += dt tstep += 1 solver.RK4_integrate(dt) sys.stdout.flush() # forces Python 3 to flush print statements solver.U[0] = irfft3(comm, solver.U_hat[0]) solver.U[1] = irfft3(comm, solver.U_hat[1]) solver.U[2] = irfft3(comm, solver.U_hat[2]) k = comm.reduce(0.5 * np.sum(np.square(solver.U) * (1. / N)**3)) k_true = 0.12451526736699045 # from spectralDNS3D_short.py run until T=1.0 if comm.rank == 0: print("Time = %12.8f, KE = %16.13f" % (time.time() - start, k)) # assert that the two codes must be within single-precision round-off # error of each other assert round(abs(k - k_true), 7) < 1.e-7 # if code passes assertion then output the relative error in codes for # proper bragging rights print("relative error in avg. KE compared to spectralDNS3D_short.py: " "{}".format(abs(k - k_true) / k_true)) return
def homogeneous_isotropic_turbulence(pp=None, sp=None): """ Arguments: ---------- pp: (optional) program parameters, parsed by argument parser provided by this file sp: (optional) solver parameters, parsed by spectralLES.parser """ if comm.rank == 0: print("\n----------------------------------------------------------") print("MPI-parallel Python spectralLES simulation of problem \n" "`Homogeneous Isotropic Turbulence' started with " "{} tasks at {}.".format(comm.size, timeofday())) print("----------------------------------------------------------") # if function called without passing in parsed arguments, then parse # the arguments from the command line if pp is None: pp = hit_parser.parse_known_args()[0] if sp is None: sp = spectralLES.parser.parse_known_args()[0] if comm.rank == 0: print('\nProblem Parameters:\n-------------------') for k, v in vars(pp).items(): print(k, v) print('\nSpectralLES Parameters:\n-----------------------') for k, v in vars(sp).items(): print(k, v) print("\n----------------------------------------------------------\n") assert len(set(pp.N)) == 1, ('Error, this beta-release HIT program ' 'requires equal mesh dimensions') N = pp.N[0] assert len(set(pp.L)) == 1, ('Error, this beta-release HIT program ' 'requires equal domain dimensions') L = pp.L[0] if N % comm.size > 0: if comm.rank == 0: print('Error: job started with improper number of MPI tasks for ' 'the size of the data specified!') MPI.Finalize() sys.exit(1) # ------------------------------------------------------------------------- # Configure the solver, writer, and analyzer # -- construct solver instance from sp's attribute dictionary solver = spectralLES(comm, **vars(sp)) # -- configure solver instance to solve the NSE with the vorticity # formulation of the advective term, linear forcing, and # Smagorinsky SGS model. solver.computeAD = solver.computeAD_vorticity_form Sources = [ solver.computeSource_linear_forcing, solver.computeSource_Smagorinksy_SGS ] Ck = 1.6 Cs = sqrt((pi**-2) * ((3 * Ck)**-1.5)) # == 0.098... # Cs = 0.2 kwargs = {'Cs': Cs, 'dvScale': None} # -- form HIT initial conditions from either user-defined values or # physics-based relationships using epsilon and L Urms = 1.2 * (pp.epsilon * L)**(1. / 3.) # empirical coefficient Einit = getattr(pp, 'Einit', None) or Urms**2 # == 2*KE_equilibrium kexp = getattr(pp, 'kexp', None) or -1. / 3. # -> E(k) ~ k^(-2./3.) kpeak = getattr(pp, 'kpeak', None) or N // 4 # ~ kmax/2 # ! currently using a fixed random seed of comm.rank for testing solver.initialize_HIT_random_spectrum(Einit, kexp, kpeak, rseed=comm.rank) U_hat = solver.U_hat U = solver.U omega = solver.omega K = solver.K # -- configure the writer and analyzer from both pp and sp attributes writer = mpiWriter(comm, odir=pp.odir, N=N) analyzer = mpiAnalyzer(comm, odir=pp.adir, pid=pp.pid, L=L, N=N, config='hit', method='spectral') Ek_fmt = "\widehat{{{0}}}^*\widehat{{{0}}}".format emin = np.inf emax = np.NINF analyzer.mpi_moments_file = '%s%s.moments' % (analyzer.odir, pp.pid) # ------------------------------------------------------------------------- # Setup the various time and IO counters tauK = sqrt(pp.nu / pp.epsilon) # Kolmogorov time-scale taul = 0.2 * L * sqrt(3) / Urms # 0.2 is empirical coefficient c = pp.cfl * sqrt(2 * Einit) / Urms dt = solver.new_dt_constant_nu(c) # use as estimate print("Integral time scale = {}".format(taul)) if pp.tlimit == np.Inf: # put a very large but finite limit on the run pp.tlimit = 262 * taul # such as (256+6)*tau, for spinup and 128 samples dt_rst = getattr(pp, 'dt_rst', None) or 4 * taul dt_bin = getattr(pp, 'dt_bin', None) or taul dt_stat = getattr(pp, 'dt_stat', None) or max(0.2 * taul, 2 * tauK, 20 * dt) dt_spec = getattr(pp, 'dt_spec', None) or max(0.1 * taul, tauK, 10 * dt) dt_drv = getattr(pp, 'dt_drv', None) or max(tauK, 10 * dt) t_sim = t_rst = t_bin = t_stat = t_spec = t_drv = 0.0 tstep = irst = ibin = istat = ispec = 0 # -- ensure that analysis and simulation outputs are properly synchronized # This assumes that dt_spec < dt_stat < dt_bin < dt_rst, and that # division remainders smaller than 0.1 are potentially # consequential round-off errors in what should be integer multiples # due to the user supplying insufficient significant digits if ((dt_stat % dt_spec) < 0.1 * dt_spec): dt_stat -= dt_stat % dt_spec if ((dt_bin % dt_spec) < 0.1 * dt_spec): dt_bin -= dt_bin % dt_spec if ((dt_bin % dt_stat) < 0.1 * dt_stat): dt_bin -= dt_bin % dt_stat if ((dt_rst % dt_bin) < 0.1 * dt_bin): dt_rst -= dt_rst % dt_bin # ------------------------------------------------------------------------- # Run the simulation while t_sim < pp.tlimit + 1.e-8: # -- Update the dynamic dt based on CFL constraint dt = solver.new_dt_constant_nu(pp.cfl) t_test = t_sim + 0.5 * dt compute_vorticity = True # reset the vorticity computation flag # -- output log messages every step if needed/wanted KE = 0.5 * comm.allreduce(np.sum(np.square(U))) * (1. / N)**3 if comm.rank == 0: print("cycle = %7d time = %15.8e dt = %15.8e KE = %15.8e" % (tstep, t_sim, dt, KE)) # - output snapshots and data analysis products if t_test >= t_spec: analyzer.spectral_density(U_hat, '%3.3d_u' % ispec, 'velocity PSD\t%s' % Ek_fmt('u_i')) omega[2] = irfft3(comm, 1j * (K[0] * U_hat[1] - K[1] * U_hat[0])) omega[1] = irfft3(comm, 1j * (K[2] * U_hat[0] - K[0] * U_hat[2])) omega[0] = irfft3(comm, 1j * (K[1] * U_hat[2] - K[2] * U_hat[1])) analyzer.spectral_density(omega, '%3.3d_omga' % ispec, 'vorticity PSD\t%s' % Ek_fmt('\omega_i')) t_spec += dt_spec ispec += 1 compute_vorticity = False # if t_test >= t_stat: # if compute_vorticity: # omega[2] = irfft3(comm, 1j*(K[0]*U_hat[1] - K[1]*U_hat[0])) # omega[1] = irfft3(comm, 1j*(K[2]*U_hat[0] - K[0]*U_hat[2])) # omega[0] = irfft3(comm, 1j*(K[1]*U_hat[2] - K[2]*U_hat[1])) # enst = 0.5*np.sum(np.square(omega), axis=0) # emin = min(emin, comm.allreduce(np.min(enst), op=MPI.MIN)) # emax = max(emax, comm.allreduce(np.max(enst), op=MPI.MAX)) # scalar_analysis(analyzer, enst, (emin, emax), None, None, # '%3.3d_enst' % istat, 'enstrophy', '\Omega') # t_stat += dt_stat # istat += 1 # -- output singe-precision binary files and restart checkpoints # if t_test >= t_bin: # writer.write_scalar('Enstrophy_%3.3d.bin' % ibin, enst, np.float32) # t_bin += dt_bin # ibin += 1 if t_test >= t_rst: writer.write_scalar('Velocity1_%3.3d.rst' % irst, U[0], np.float64) writer.write_scalar('Velocity2_%3.3d.rst' % irst, U[1], np.float64) writer.write_scalar('Velocity3_%3.3d.rst' % irst, U[2], np.float64) t_rst += dt_rst irst += 1 # -- Update the forcing pattern if t_test >= t_drv: # call solver.computeSource_linear_forcing to compute dvScale only kwargs['dvScale'] = Sources[0](computeRHS=False) t_drv += dt_drv if comm.rank == 0: print("------ updated dvScale for linear forcing ------") # print(kwargs['dvScale']) # -- integrate the solution forward in time solver.RK4_integrate(dt, *Sources, **kwargs) t_sim += dt tstep += 1 sys.stdout.flush() # forces Python 3 to flush print statements # ------------------------------------------------------------------------- # Finalize the simulation omega[2] = irfft3(comm, 1j * (K[0] * U_hat[1] - K[1] * U_hat[0])) omega[1] = irfft3(comm, 1j * (K[2] * U_hat[0] - K[0] * U_hat[2])) omega[0] = irfft3(comm, 1j * (K[1] * U_hat[2] - K[2] * U_hat[1])) enst = 0.5 * np.sum(np.square(omega), axis=0) analyzer.spectral_density(U_hat, '%3.3d_u' % ispec, 'velocity PSD\t%s' % Ek_fmt('u_i')) analyzer.spectral_density(omega, '%3.3d_omga' % ispec, 'vorticity PSD\t%s' % Ek_fmt('\omega_i')) # emin = min(emin, comm.allreduce(np.min(enst), op=MPI.MIN)) # emax = max(emax, comm.allreduce(np.max(enst), op=MPI.MAX)) # scalar_analysis(analyzer, enst, (emin, emax), None, None, # '%3.3d_enst' % istat, 'enstrophy', '\Omega') writer.write_scalar('Enstrophy_%3.3d.bin' % ibin, enst, np.float32) writer.write_scalar('Velocity1_%3.3d.rst' % irst, U[0], np.float64) writer.write_scalar('Velocity2_%3.3d.rst' % irst, U[1], np.float64) writer.write_scalar('Velocity3_%3.3d.rst' % irst, U[2], np.float64) return
def RK4_integrate(self, dt, *Sources, **kwargs): """ 4th order Runge-Kutta time integrator for spectralLES Arguments: ---------- dt: current timestep *Sources: (Optional) User-supplied source terms. This is a special Python syntax, basically any argument you feed RK4_integrate() after dt will be stored in the list Source_terms. If no arguments are given, Sources = [], in which case the loop is skipped. **kwargs: (Optional) the keyword arguments to be passed to all Sources. Notes ----- This function applies the Leray-Hopf projection operator (aka the residual of the Helmholtz operator) to the entire RHS. By performing this operation on the entire RHS, we simultaneously enforce the divergence-free continuity condition _and_ automatically make all SGS source terms deviatoric! This operation is equivalent to computing the total _mechanical_ pressure (aka the thermodynamic pressure plus the non-deviatoric component of all source terms) using a physical-space Poisson solver and then adding a mechanical-pressure-gradient transport term to the RHS. """ a = [1 / 6, 1 / 3, 1 / 3, 1 / 6] b = [0.5, 0.5, 1.0, 0.0] self.U_hat1[:] = self.U_hat0[:] = self.U_hat[:] for rk in range(4): # ---------------------------------------------------------- # ensure all computeAD and computeSource methods have an # updated physical-space solution field irfft3(self.comm, self.U_hat[0], self.U[0]) irfft3(self.comm, self.U_hat[1], self.U[1]) irfft3(self.comm, self.U_hat[2], self.U[2]) # ---------------------------------------------------------- # compute all RHS terms self.computeAD(**kwargs) for computeSource in Sources: computeSource(**kwargs) # ---------------------------------------------------------- # dealias and project the entire RHS self.dU *= self.dealias self.dU -= np.sum(self.dU * self.K_Ksq, axis=0) * self.K # ---------------------------------------------------------- # accumulate the intermediate RK stages self.U_hat[:] = self.U_hat0 + b[rk] * dt * self.dU self.U_hat1[:] += a[rk] * dt * self.dU # -------------------------------------------------------------- # update the spectral-space solution field with the final RK stage self.U_hat[:] = self.U_hat1[:] # -------------------------------------------------------------- # ensure the user has an updated physical-space solution field irfft3(self.comm, self.U_hat[0], self.U[0]) irfft3(self.comm, self.U_hat[1], self.U[1]) irfft3(self.comm, self.U_hat[2], self.U[2]) return