Esempio n. 1
0
    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
Esempio n. 3
0
    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
Esempio n. 4
0
    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
Esempio n. 5
0
    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
Esempio n. 6
0
    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
Esempio n. 7
0
    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)
Esempio n. 8
0
    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
Esempio n. 9
0
    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
Esempio n. 10
0
    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
Esempio n. 11
0
    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
Esempio n. 12
0
    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
Esempio n. 13
0
    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
Esempio n. 14
0
    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
Esempio n. 15
0
    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
Esempio n. 16
0
    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
Esempio n. 17
0
    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
Esempio n. 18
0
    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
Esempio n. 19
0
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
Esempio n. 20
0
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
Esempio n. 21
0
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
Esempio n. 22
0
    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