#!/usr/bin/env python # -*- coding: utf-8 -*- """beta plane barotropic vorticity model. By James Penn, Geoffrey K. Vallis This is meant as a self-contained, simple to use barotropic code. A more complete python code is to be found at pyqg https://github.com/pyqg/pyqg] or a fast fortran code on the web site of K. Shafer Smith (Courant). This script uses a pseudospectral method to solve the barotropic vorticity equation in two dimensions D/Dt[ω] = forcing - dissipation (1) where ω = ξ + f. ξ is local vorticity ∇ × u and f is global rotation. Assuming an incompressible two-dimensional flow u = (u, v), the streamfunction ψ = ∇ × (ψ êz) can be used to give (u,v) u = ∂/∂y[ψ] v = -∂/∂x[ψ] (2) and therefore local vorticity can be given as a Poisson equation ξ = ∆ψ (3) where ∆ is the laplacian operator. Since ∂/∂t[f] = 0 equation (1) can be written in terms of the local vorticity D/Dt[ξ] + u·∇f = 0 => D/Dt[ξ] = -vβ (4) using the beta-plane approximation f = f0 + βy. This can be written entirely in terms of the streamfunction and this is the form that will be solved numerically. D/Dt[∆ψ] = -β ∂/∂x[ψ] (5) The spectral method defines ψ as a Fourier sum ψ = Σ A(t) exp(i (kx + ly)) and as such spatial derivatives can be calculated analytically ∂/∂x[ψ] = ikψ ∂/∂y[ψ] = ilψ The pseudospectral method will use the analytic derivatives to calculate values for (u, v) which will then be used to evaluate nonlinear terms. This version has no forcing and a high wavenumber Smith filter which replaces hyperviscosity. It can be de-aliased. References: * This code is partly based on a MATLAB script bvebb.m (Original source Dr. James Kent & Prof. John Thuburn) """ from __future__ import (division, print_function) import matplotlib.pyplot as plt import numpy as np from numpy import pi, cos, sin from numpy.fft import fftshift, fftfreq from numpy.fft.fftpack import rfft2, irfft2 from mystuff import show_plot,draw_plot ### Configuration nx = 256 ny = 256 # numerical resolution Lx = 1.0 Ly = 1.0 # domain size [m] ubar = 0.00 # background zonal velocity [m/s] beta = 12.0 # beta-plane f = f0 + βy [1/s 1/m] n_diss = 2.0 # Small-scale dissipation of the form ∆^2n_diss, such that n_diss = 2 would be a ∆^4 hyperviscosity. tau = 0.1 # coefficient of dissipation # smaller = more dissipation #Poorly determined coefficients for forcing and dissipation r_rayleigh = (1./50000.)/np.sqrt(10.) forcing_amp_factor=100.0/np.sqrt(1.) r_rayleigh = 0. forcing_amp_factor=0. t = 0.0 tmax = 1000 step = 0 ALLOW_SPEEDUP = True # if True, allow the simulation to take a larger SPEEDUP_AT_C = 0.4 # timestep when the Courant number drops below # value of parameter SPEEDUP_AT_C SLOWDN_AT_C = 0.6 # reduce the timestep when Courant number # is bigger than SLOWDN_AT_C PLOT_EVERY_S = 100 PLOT_EVERY_S2 = 40 ### Function Definitions def ft(phi): """Go from physical space to spectral space.""" return rfft2(phi, axes=(-2, -1)) def ift(psi): """Go from spectral space to physical space.""" return irfft2(psi, axes=(-2,-1)) def courant_number(psix, psiy, dx, dt): """Calculate the Courant Number given the velocity field and step size.""" maxu = np.max(np.abs(psiy)) maxv = np.max(np.abs(psix)) maxvel = maxu + maxv return maxvel*dt/dx def grad(phit): """Returns the spatial derivatives of a Fourier transformed variable. Returns (∂/∂x[F[φ]], ∂/∂y[F[φ]]) i.e. (ik F[φ], il F[φ])""" global ik, il phixt = ik*phit # d/dx F[φ] = ik F[φ] phiyt = il*phit # d/dy F[φ] = il F[φ] return (phixt, phiyt) def velocity(psit): """Returns the velocity field (u, v) from F[ψ].""" psixt, psiyt = grad(psit) psix = ift(psixt) # v = ∂/∂x[ψ] psiy = ift(psiyt) # u = - ∂/∂y[ψ] return (-psiy, psix) def spectral_variance(phit): global nx, ny var_density = 2.0 * np.abs(phit)**2 / (nx*ny) var_density[:,0] /= 2 var_density[:,-1] /= 2 return var_density.sum() def anti_alias(phit): """Set the coefficients of wavenumbers > k_mask to be zero.""" k_mask = (8./9.)*(nk+1)**2. phit[(np.abs(ksq/(dk*dk)) >= k_mask)] = 0.0 def high_wn_filter(phit): """Applies the high wavenumber filter of smith et al 2002""" filter_dec = -np.log(1.+2.*pi/nk)/((nk-kcut)**filter_exp) filter_idx = np.abs(ksq/(dk*dk)) >= kcut**2. phit[filter_idx] *= np.exp(filter_dec*(np.sqrt(ksq[filter_idx]/(dk*dk))-kcut)**filter_exp) def forcing_spatial_mask(phi): """TODO: Make spatial mask for forcing""" phi[idx_sp_mask] *= 0. _prhs, _pprhs = 0.0, 0.0 # previous two right hand sides def adams_bashforth(zt, rhs, dt): """Take a single step forward in time using Adams-Bashforth 3.""" global step, t, _prhs, _pprhs if step is 0: # forward euler dt1 = dt dt2 = 0.0 dt3 = 0.0 elif step is 1: # AB2 at step 2 dt1 = 1.5*dt dt2 = -0.5*dt dt3 = 0.0 else: # AB3 from step 3 on dt1 = 23./12.*dt dt2 = -16./12.*dt dt3 = 5./12.*dt newzt = zt + dt1*rhs + dt2*_prhs + dt3*_pprhs _pprhs = _prhs _prhs = rhs return newzt ## SETUP ### Physical Domain nl = ny nk = nx/2 + 1 dx = Lx / nx dy = Ly / ny dt = 0.4 * 16.0 / nx # choose an initial dt. This will change # as the simulation progresses to maintain # numerical stability y = np.linspace(0, Ly, num=ny) y_arr = np.flipud(np.tile(y,(nx,1)).transpose()) xx = np.linspace(0, Lx, num=nx) yy = 1. - np.linspace(0, Ly, num=ny) ### Spectral Domain dk = 2.0*pi/Lx dl = 2.0*pi/Ly # calculate the wavenumbers [1/m] # The real FT has half the number of wavenumbers in one direction: # FT_x[real] -> complex : 1/2 as many complex numbers needed as real signal # FT_y[complex] -> complex : After the first transform has been done the signal # is complex, therefore the transformed domain in second dimension is same size # as it is in euclidean space. # Therefore FT[(nx, ny)] -> (nx/2, ny) # The 2D Inverse transform returns a real-only domain (nx, ny) k = dk*np.arange(0, nk, dtype=np.float64)[np.newaxis, :] l = dl*fftfreq(nl, d=1.0/nl)[:, np.newaxis] ksq = k**2 + l**2 ksq[ksq == 0] = 1.0 # avoid divide by zero - set ksq = 1 at zero wavenum rksq = 1.0 / ksq # reciprocal 1/(k^2 + l^2) ik = 1j*k # wavenumber mul. imaginary unit is useful il = 1j*l # for calculating derivatives ## Dissipation & Spectral Filters # Use ∆^2n_diss hyperviscosity to diffuse at small scales (i.e. n_diss = 2 would be ∆^4) # use the x-dimension for reference scale values nu = ((Lx/(np.floor(nx/3)*2.0*pi))**(2*n_diss))/tau #High wavenumber filter coefficients. Any waves with wavenumber below kcut are not dissipated at all. filter_exp = 8. kcut = 30. # Spectral Filter as per [Arbic and Flierl, 2003] wvx = np.sqrt((k*dx)**2 + (l*dy)**2) spectral_filter = np.exp(-23.6*(wvx-0.65*pi)**4) spectral_filter[wvx <= 0.65*pi] = 1.0 z = np.zeros((ny, nx), dtype=np.float64) zt = np.zeros((nl, nk), dtype=np.complex128) ### Diagnostic arrays time_arr = np.zeros(1) tot_energy_arr = np.zeros(1) ### Initial Condition # The McWilliams Initial Condition from [McWilliams - J. Fluid Mech. (1984)] ck = np.zeros_like(ksq) ck = np.sqrt(ksq + (1.0 + (ksq/36.0)**2))**-1 ck = 0.001/(ksq + (ksq - 3200.)**2) piit = np.random.randn(*ksq.shape)*ck + 1j*np.random.randn(*ksq.shape)*ck pii = ift(piit) pii = pii - pii.mean() piit = ft(pii) KE = spectral_variance(piit*np.sqrt(ksq)*spectral_filter) qit = -ksq * piit / np.sqrt(KE) qi = ift(qit) z[:] = qi # initialise the transformed ζ zt[:] = ft(z) anti_alias(zt) z[:]=ift(zt) amp = forcing_amp_factor* np.max(np.abs(qi)) # calc a reasonable forcing amplitude # initialise the storage arrays time_arr[0]=t psit = -rksq * zt # F[ψ] = - F[ζ] / (k^2 + l^2) psixt, psiyt = grad(psit) psix = ift(psixt) psiy = ift(psiyt) urms=np.sqrt(np.mean(psix**2 + psiy**2)) tot_energy=0.5*urms**2. tot_energy_arr[0]=tot_energy ## RUN THE SIMULATION plt.close('all') plt.ion() # plot in realtime plt.figure(1,figsize=(8, 8)) plt.clf() show_plot() plt.figure(2,figsize=(5, 10)) plt.clf() plt.figure(3,figsize=(5, 10)) plt.clf() # plt.figure(4,figsize=(4, 8)) show_plot() tplot = t + PLOT_EVERY_S tplot2 = t + PLOT_EVERY_S2 filter_testt=np.ones(zt.shape) high_wn_filter(filter_testt) #enables the profile of the high_wn_filter to be compared with deln, the equivalent for hyperviscosity. filter_testt_shift = np.fft.fftshift(filter_testt, axes=(0,)) while t < tmax: # calculate derivatives in spectral space psit = -rksq * zt # F[ψ] = - F[ζ] / (k^2 + l^2) psixt, psiyt = grad(psit) zxt, zyt = grad(zt) # transform back to physical space for pseudospectral part z[:] = ift(zt) psix = ift(psixt) psiy = ift(psiyt) zx = ift(zxt) zy = ift(zyt) # Non-linear: calculate the Jacobian in real space # and then transform back to spectral space jac = psix * zy - psiy * zx + ubar * zx jact = ft(jac) # apply forcing in spectral space by exciting certain wavenumbers # (could also apply some real-space forcing and then convert # into spectral space before adding to rhs) forcet = np.zeros_like(ksq, dtype=complex) idx = (14 < np.sqrt(ksq)/dk) & (np.sqrt(ksq)/dk < 20) forcet[idx] = 0.5*amp*(np.random.random(ksq.shape)[idx] - 0.5)*np.exp(1j*2.*pi*np.random.random(ksq.shape)[idx])#*(ksq[idx])**(-1./4.)#*np.sin(0.5*t) # calculate the size of timestep that can be taken # (assumes a domain where dx and dy are of the same order) c = courant_number(psix, psiy, dx, dt) if c >= SLOWDN_AT_C: print('DEBUG: Courant No > 0.8, reducing timestep') dt = 0.9*dt elif c < SPEEDUP_AT_C and ALLOW_SPEEDUP: dt = 1.1*dt # Possible use of time-varying dissipation, as in Maltrud & Vallis 1991, eq 2.6. # nu = 1.0 * np.sqrt(np.mean(z**2.))/(np.max(k)**(2.*n_diss -2.)) # take a timestep and diffuse rhs = -jact - beta*psixt + forcet - zt*r_rayleigh zt[:] = adams_bashforth(zt, rhs, dt) deln = 1.0 / (1.0 + nu*ksq**n_diss*dt) # zt[:] = zt * deln #Testing without hyperviscosity, and with high_wn_filter instead. #anti_alias anti_alias(zt) #high_wavenumber_filter high_wn_filter(zt) if t > tplot: plt.figure(1) # diagnostics urms=np.sqrt(np.mean(psix**2 + psiy**2)) rhines_scale = np.sqrt(urms/beta) tot_energy=0.5*urms**2. epsilon = 2 * r_rayleigh * tot_energy l_epsilon = (epsilon / beta**3.)**(1./5.) time_arr=np.append(time_arr,t) tot_energy_arr=np.append(tot_energy_arr,tot_energy) force=ift(forcet) print('[{:5d}] {:.2f} Max z: {:2.2f} c={:.2f} dt={:.2f} rh_s={:.3f} l_eps={:.3f} ratio={:2.2f} urms={:2.2f}'.format( step, t, np.max(z), c, dt, rhines_scale, l_epsilon, rhines_scale/l_epsilon, urms)) plt.clf() plt.subplot(231) # plt.imshow(z, extent=[0, Lx, 0, Ly], cmap=plt.cm.YlGnBu) plt.imshow(z, extent=[0, Lx, 0, Ly], cmap=plt.cm.RdYlBu) plt.xlabel('x') plt.ylabel('y') zmax = np.max(np.abs(z)) plt.clim(-zmax,zmax) plt.colorbar(orientation='horizontal') plt.title('Vorticity at {:.2f}s dt={:.2f}'.format(t, dt)) plt.subplot(232) power = np.fft.fftshift(np.abs(zt)**2, axes=(0,)) power_norm = np.log(power) plt.imshow(power_norm, extent=[np.min(k/dk), np.max(k/dk), np.min(l/dl), np.max(l/dl)]) # plt.imshow(filter_testt_shift, # extent=[np.min(k/dk), np.max(k/dk), np.min(l/dl), np.max(l/dl)]) plt.xlabel('k/dk') plt.ylabel('l/dl') plt.colorbar(orientation='horizontal') plt.title('Power Spectra') ax1=plt.subplot(233) ax1.plot(-np.mean(psiy,axis=1),np.linspace(0, Ly, num=ny)) ax1.axvline(0, color='black') plt.xlabel('ubar') ax2=ax1.twiny() ax2.plot(np.mean(z,axis=1)+beta*y,np.linspace(0, Ly, num=ny),'g') plt.xlabel('qbar') plt.subplot(234) plt.imshow(z+beta*y_arr, extent=[0, Lx, 0, Ly], cmap=plt.cm.YlGnBu) plt.xlabel('x') plt.ylabel('y') zmax = np.max(np.abs(z+beta*y_arr)) plt.clim(-zmax,zmax) plt.colorbar(orientation='horizontal') plt.title('Vorticity at {:.2f}s dt={:.2f}'.format(t, dt)) plt.subplot(235) plt.plot(time_arr, tot_energy_arr) plt.xlabel('Time') plt.ylabel('Total Energy') plt.subplot(236) plt.imshow(force, extent=[0, Lx, 0, Ly], cmap=plt.cm.YlGnBu) plt.xlabel('x') plt.ylabel('y') forcemax = np.max(np.abs(force)) plt.clim(-forcemax,forcemax) plt.colorbar(orientation='horizontal') plt.title('Forcing at {:.2f}s dt={:.2f}'.format(t, dt)) draw_plot(1) # plt.pause(0.01) tplot = t + PLOT_EVERY_S pass if t > tplot2: psitp = ift(psit) # plt.figure(2) # plt.subplot(2,1,1) # plt.imshow(z, extent=[0, Lx, 0, Ly], cmap=plt.cm.YlGnBu) # plt.xlabel('x') # plt.ylabel('y') # plt.subplot(2,1,2) # plt.imshow(psitp, extent=[0, Lx, 0, Ly], cmap=plt.cm.YlGnBu) # plt.xlabel('x') # plt.ylabel('y') # draw_plot(2) # plt.savefig('imshow{:1.0f}.pdf'.format(t),transparent=True) plt.figure(2) plt.clf() plt.subplot(2,1,1) plt.imshow(z, extent=[0, Lx, 0, Ly], cmap=plt.cm.YlGnBu) plt.xlabel('x') plt.ylabel('y') draw_plot() plt.subplot(2,1,2) plt.imshow(psitp, extent=[0, Lx, 0, Ly], cmap=plt.cm.YlGnBu) plt.contour(xx, yy, psitp, 10, colors='g', linestyles='solid', linewidths=0.25) plt.xlabel('x') plt.ylabel('y') draw_plot(2) plt.savefig('contour{:1.0f}.pdf'.format(t),transparent=True) plt.figure(3) plt.clf() plt.subplot(2,1,1) plt.imshow(z, extent=[0, Lx, 0, Ly], cmap=plt.cm.bone_r) plt.xlabel('x') plt.ylabel('y') draw_plot() plt.subplot(2,1,2) plt.imshow(psitp, extent=[0, Lx, 0, Ly], cmap=plt.cm.bone_r) plt.contour(xx, yy, psitp, 10, colors='k', linestyles='solid', linewidths=0.25) plt.xlabel('x') plt.ylabel('y') draw_plot(3) plt.savefig('gray{:1.0f}.pdf'.format(t),transparent=True) zmax = np.max(np.abs(z)) tplot2 = t + PLOT_EVERY_S2 pass plt.show() t = t + dt step = step + 1