├── .gitignore ├── .travis.yml ├── CITATION.cff ├── LICENSE ├── README.md ├── environment.yml ├── index.html ├── main.py ├── post └── post.py ├── requirements.txt ├── setup.py ├── src ├── Theory.ipynb ├── __init__.py ├── field.py ├── fluid.py ├── io.py └── valid.py └── tests ├── __init__.py └── test_solver.py /.gitignore: -------------------------------------------------------------------------------- 1 | Dat/ 2 | Dat_2/ 3 | DecayingTurbulence/ 4 | EllipticalVortex/ 5 | ShearLayer-1/ 6 | ShearLayer-2/ 7 | ShearLayer-3/ 8 | __pycache__/ 9 | src/__pycache__/ 10 | 11 | run.py 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.6" 5 | - "3.7" 6 | - "3.8" 7 | - "nightly" 8 | matrix: 9 | allow_failures: 10 | - python: "3.5" 11 | - python: "nightly" 12 | install: 13 | - pip install -r requirements.txt 14 | - pip install pytest 15 | - pip install pytest-cov 16 | - pip install coveralls 17 | script: 18 | - pytest --cov=src/ --cov=tests/ 19 | after_success: 20 | - coveralls 21 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Lauber" 5 | given-names: "Marin" 6 | orcid: "https://orcid.org/0000-0003-2191-9318" 7 | title: "2D-Turbulence-Python" 8 | version: 1.0 9 | doi: 10.5281/zenodo.5517704 10 | date-released: 2021-07-20 11 | url: "https://github.com/marinlauber/2D-Turbulence-Python" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Marin Lauber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decaying Turbulence in Python (OOP) 2 | 3 | 4 | [![Build Status](https://travis-ci.org/marinlauber/2D-Turbulence-Python.svg?branch=master)](https://travis-ci.org/github/marinlauber/2D-Turbulence-Python) 5 | 6 | [![Coverage Status](https://coveralls.io/repos/github/marinlauber/2D-Turbulence-Python/badge.svg?branch=master)](https://coveralls.io/github/marinlauber/2D-Turbulence-Python?branch=master) 7 | 8 | [![DOI](https://zenodo.org/badge/215633416.svg)](https://zenodo.org/badge/latestdoi/215633416) 9 | 10 | Pseudo-spectral collocation code for two-dimensional turbulence simulations written in object-oriented python. 11 | 12 | ## Getting Started 13 | 14 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 15 | 16 | ### Prerequisites 17 | 18 | To run the code provided in this repository, you will need standard python packages, such as `numpy` and `matplotlib`. These should already be installed on most machines. 19 | 20 | To use the `master` branch of this code (the fast one!), you will need to have `pyFFTW` installed. Installation instructions can be found [here](https://pypi.org/project/pyFFTW/). 21 | 22 | Alternatively, you can create a new conda environment using the `environment.yml` file provided. First, got in the '2D-Turbulence-Python' cloned repository and creat the new environment 23 | ``` 24 | $ conda env create -f environment.yml 25 | ``` 26 | This will create a new conda environment called `2D-Turbulence-Python`. Then activate it 27 | ``` 28 | $ conda activate 2D-Turbulence-Python 29 | ``` 30 | This will install all the required packages and allows you to use this repo. 31 | 32 | This repository contains three (as of today) branches, the `master` branch that includes the pseudo-spectral code running with the `pyFFTW` library, the `CDS` branch that contains the high-order (up to 6th) compact difference scheme version and the `numpy` branch that contains a numpy-only version of the code. When the repository has been cloned/downloaded, you can switch between branches using 33 | ``` 34 | git checkout numpy (or CDS) 35 | ``` 36 | to switch to the desired branch. The solver is implemented such that functions calls perform the same tasks on each branch. 37 | 38 | ### Running The Tests 39 | 40 | Once you are happy with the version you are going to use, check that everything works by running the validation script 41 | ``` 42 | python valid.py 43 | ``` 44 | This runs a simulation of the [Taylor-Green Vortex](https://en.wikipedia.org/wiki/Taylor%E2%80%93Green_vortex) for which we have an analytical solution. The output should look similar to this 45 | ``` 46 | Starting integration on field. 47 | 48 | Iteration 100, time 0.020, time remaining 0.079. TKE: 0.180 ENS: 23580.54 49 | Iteration 200, time 0.041, time remaining 0.058. TKE: 0.129 ENS: 16934.10 50 | Iteration 300, time 0.061, time remaining 0.038. TKE: 0.092 ENS: 12157.35 51 | Iteration 400, time 0.082, time remaining 0.017. TKE: 0.066 ENS: 8725.782 52 | 53 | Execution time for 484 iterations is 2.737 seconds. 54 | The L2-norm of the Error in the Taylor-Green vortex on a 128x128 grid is 1.362e-10. 55 | The Linf-norm of the Error in the Taylor-Green vortex on a 128x128 grid is 2.725e-10. 56 | ``` 57 | You should get errors in both norms close to the values displayed above. 58 | 59 | This repo also contains some more basic test, that can be run using `pytest` 60 | 61 | ``` 62 | $ pytest 63 | ``` 64 | 65 | ## Using the code 66 | 67 | Simulations are initialized by defining a grid, and specifying the Reynolds number of the flow 68 | ```python 69 | flow = Fluid(nx=64, ny=64, Re=1) 70 | flow.init_solver() 71 | flow.init_field(TaylorGreen) 72 | ``` 73 | Here we have initialized the Taylor-Green vortex. The solver initiation generates all the working arrays and transforms the initial conditions. Simulations can also be initialized using results from previous runs (these need to have been saved with `flow.write(folder='', 1)`) 74 | ```python 75 | from src.field import FromDat 76 | flow.init_field(FromDat, name="vort_000001.dat") 77 | ``` 78 | here we reset the flow timer using the time value saved in the `vort_ID.dat` file. The `finish` time of the simulation must be adjusted accordingly, as well as the `ID` if the field is saved. This allows user-generated field to be used, within the limitations of the method (periodic boundary conditions). The main loop of the solver is called as 79 | ```python 80 | # loop to solve 81 | while(flow.time<=finish): 82 | flow.update() 83 | if(flow.it % 1000 == 0): 84 | print("Iteration \t %d, time \t %f, time remaining \t %f. TKE: %f, ENS: %f" %(flow.it, 85 | flow.time, finish-flow.time, flow.tke(), flow.enstrophy())) 86 | flow.write(file="fluid") 87 | ``` 88 | Small simulations can also be run live, which can also be handy for de-bugging 89 | ```python 90 | flow.run_live(finish, every=100) 91 | ``` 92 | 93 | ## Additional Content 94 | 95 | For a description of the theory behind this code, or to run other cases, such as a double shear layer, or decaying isotropic turbulence, look at [this](https://marinlauber.github.io/2D-Turbulence-Python/). 96 | 97 | 100 | 101 | ## Authors 102 | 103 | * **Marin Lauber** - *Initial work* - [github](https://github.com/marinlauber) 104 | 105 | ## License 106 | 107 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 108 | 109 | 114 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: 2D-Turbulence-Python 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.6 6 | - numpy 7 | - matplotlib 8 | - pytest 9 | - netcdf4 10 | - pip 11 | - pip: 12 | - pyfftw -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Marin Lauber" 5 | __copyright__ = "Copyright 2019, Marin Lauber" 6 | __license__ = "GPL" 7 | __version__ = "1.0.1" 8 | __email__ = "M.Lauber@soton.ac.uk" 9 | 10 | import time as t 11 | from src.fluid import Fluid 12 | from src.field import DecayingTurbulence 13 | 14 | if __name__=="__main__": 15 | 16 | # build fluid and solver 17 | flow = Fluid(512, 512, 2000, pad=1.) 18 | flow.init_solver() 19 | flow.init_field(DecayingTurbulence) 20 | 21 | print("Starting integration on field.\n") 22 | start_time = t.time() 23 | finish = 0.01 24 | 25 | # loop to solve 26 | while(flow.time<=finish): 27 | flow.update() 28 | if(flow.it % 100 == 0): 29 | print("Iteration \t %d, time \t %f, time remaining \t %f. TKE: %f, ENS: %f" %(flow.it, 30 | flow.time, finish-flow.time, flow.tke(), flow.enstrophy())) 31 | flow.write(file="fluid") 32 | 33 | # flow.run_live(finish, every=200) 34 | 35 | end_time = t.time() 36 | print("\nExecution time for %d iterations is %f seconds." %(flow.it, end_time-start_time)) -------------------------------------------------------------------------------- /post/post.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Marin Lauber" 5 | __copyright__ = "Copyright 2020, Marin Lauber" 6 | __license__ = "GPL" 7 | __version__ = "1.0.1" 8 | __email__ = "M.Lauber@soton.ac.uk" 9 | 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | from mpl_toolkits.axes_grid1 import make_axes_locatable 13 | import os 14 | from math import ceil as ceil 15 | import netCDF4 as nc 16 | 17 | try: 18 | plt.style.use('mystyle') 19 | except OSError: 20 | print("Using default ploting scheme...") 21 | 22 | def plot(x, y, ID): 23 | plt.plot(x, y, '-k', label=ID) 24 | plt.legend() 25 | plt.savefig(ID+".png") 26 | plt.close() 27 | 28 | 29 | def save_image(data, fn, cm="RdBu"): 30 | sizes = np.shape(data) 31 | height = float(sizes[0]); width = float(sizes[1]) 32 | fig = plt.figure() 33 | fig.set_size_inches(width/height, 1, forward=False) 34 | ax = plt.Axes(fig, [0., 0., 1., 1.]) 35 | ax.set_axis_off() 36 | fig.add_axes(ax) 37 | ax.contourf(data, cmap=cm) 38 | plt.savefig(fn, dpi = 4*height, bbox_inches="tight", pad_inches=0) 39 | plt.close() 40 | 41 | 42 | def save_contour(data, fn, time, cm="RdBu"): 43 | plt.figure(figsize=(8,6)) 44 | plt.contourf(np.linspace(0, 1, data.shape[0]), np.linspace(0, 1, data.shape[1]), 45 | np.round(data, 5), cmap=cm,levels=51) 46 | plt.colorbar() 47 | plt.xticks([]); plt.yticks([]) 48 | plt.title(f"Time: {time:.2f} s") 49 | plt.savefig(fn, dpi=600) 50 | plt.close() 51 | 52 | 53 | def save_comp(w, fn, time, cm="PRGn", res=(1920,1080)): 54 | nx = w.shape[0]; nk=w.shape[1]//2+1 55 | kx, kk = _wavenumber(nx, nk) 56 | wh = np.fft.rfft2(w, axes=(-2,-1)) 57 | k2 = kx[:,np.newaxis]**2+kk**2 58 | psih = get_psi(wh, k2) 59 | # u, v = get_velocity(psih, kx, kk) 60 | kE, k, E = get_tke(psih, k2) 61 | k, O = get_ens(wh, k2) 62 | 63 | fig = plt.figure(figsize=(12,6.75)) 64 | ax1 = plt.subplot2grid((2,2), (0,0), rowspan=2) 65 | ax2 = plt.subplot2grid((2,2), (0,1)) 66 | ax3 = plt.subplot2grid((2,2), (1,1)) 67 | 68 | # plot vorticity on two left cells 69 | p=ax1.imshow(np.round(w, 5),cmap=cm,extent=[0,1,0,1]) 70 | divider = make_axes_locatable(ax1) 71 | cax = divider.append_axes('right', size='5%', pad=0.15) 72 | fig.colorbar(p, cax=cax, orientation='vertical') 73 | 74 | # set labels 75 | ax1.set_xlabel(r'$x/2\pi$'); ax1.set_ylabel(r'$y/2\pi$') 76 | ax1.set_title(r'$\omega:=\nabla^2\psi$') 77 | 78 | # plot TKE on top right 79 | ax2.loglog(k, k*E/np.sum(E), '-k') 80 | ax2.loglog([8,80],[0.08,0.000008], ':k', alpha=0.5, lw=0.5) 81 | ax2.text(92,0.000004,r'$k^{-4}$') 82 | ax2.set_xlabel('Wavenumber'); ax2.set_ylabel(r'$E(k)/\sum E(k)$') 83 | ax2.set_title('Energy Spectrum') 84 | ax2.set_ylim(1e-8,1) 85 | 86 | # plot ENS on bottom right 87 | ax3.loglog(k, k*O/np.sum(O), '-k') 88 | ax3.loglog([8,80],[0.08,0.8*10**(-5/3.)], ':k', alpha=0.5, lw=0.5) 89 | ax3.text(92,0.6*10**(-5/3.),r'$k^{-5/3}$') 90 | ax3.set_ylim(1e-5,1) 91 | ax3.set_xlabel('Wavenumber'); ax3.set_ylabel(r'$S(k)/\sum S(k)$') 92 | ax3.set_title('Enstrophy Spectrum') 93 | 94 | # finalize 95 | fig.suptitle(f"Time: {time:.2f} s",x=0.25,y=0.98,fontsize=16) 96 | plt.tight_layout() 97 | plt.savefig(fn, dpi=ceil(res[0]/12.)) 98 | plt.close() 99 | 100 | 101 | def _wavenumber(*args): 102 | k_i = () 103 | for arg in args: 104 | k = np.fft.fftfreq(arg, d=1./arg) 105 | k_i = k_i + (k,) 106 | return k_i 107 | 108 | 109 | def get_psi(wh, k2): 110 | k2I = np.zeros_like(k2) 111 | fk = k2 != 0.0 112 | k2I[fk] = 1./k2[fk] 113 | psih = wh * k2I 114 | return psih 115 | 116 | 117 | def get_velocity(psih, kx, ky): 118 | uh = 1j*ky[:,np.newaxis]*psih 119 | vh = -1j*kx*psih 120 | return np.fft.irfft2(uh, axes=(-2,-1)), np.fft.irfft2(vh, axes=(-2,-1)) 121 | 122 | 123 | def get_tke(psih, k2, res=256): 124 | tke = np.real(0.5*k2*psih*np.conj(psih)) 125 | kmod = np.sqrt(k2) 126 | k = np.arange(1, k2.shape[0], 1, dtype=np.float64) # nyquist limit for this grid 127 | E = np.zeros_like(k) 128 | dk = (np.max(k)-np.min(k))/res 129 | for i in range(len(k)): 130 | E[i] += np.sum(tke[(kmod=k[i]-dk)]) 131 | kE = np.sum(k*E)/np.sum(E) 132 | return kE, k, E 133 | 134 | 135 | def get_ens(wh, k2, res=256): 136 | ens = np.real(0.5*k2*wh*np.conj(wh)) 137 | kmod = np.sqrt(k2) 138 | k = np.arange(1, k2.shape[0], 1, dtype=np.float64) # nyquist limit for this grid 139 | O = np.zeros_like(k) 140 | dk = (np.max(k)-np.min(k))/res 141 | for i in range(len(k)): 142 | O[i] += np.sum(ens[(kmod=k[i]-dk)]) 143 | return k, O 144 | 145 | 146 | if __name__=="__main__": 147 | 148 | print("Cleaning workspace..") 149 | os.system("rm -f *.png vid.mp4") 150 | print("Done.") 151 | 152 | print("Generating .png files and assembling movie...") 153 | 154 | # the resolution we want 155 | res=(2560,1440) 156 | 157 | # load the data 158 | data = nc.Dataset("fluid.nc") 159 | 160 | # save avery snapshot 161 | for i in range(len(data["t"])): 162 | w = data["w"][i,:,:] 163 | save_comp(w, "vort_"+"%06d"%(i+1)+".png", time=data["t"][i], cm="RdBu", res=res) 164 | i += 1 165 | 166 | # generate the movie 167 | os.system("ffmpeg -r 24 -i vort_%06d.png -s "+str(res[0])+"x"+str(res[1])+" -vcodec libx264 -crf 25 -pix_fmt yuv420p vid.mp4") 168 | os.system("rm *.png") 169 | 170 | print("Done.\nExiting.") 171 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | netcdf4 4 | pyfftw 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="OOpyPST-marinlauber", # Replace with your own username 8 | version="0.0.1", 9 | author="Marin Lauber", 10 | author_email="M.Lauber@soton.ac.uk", 11 | description="OOP 2D pseudo-spectral code", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/marinlauber/OOpyPST", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | python_requires='>=3.5', 22 | ) -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | # empty __init__.py file 2 | -------------------------------------------------------------------------------- /src/field.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | 4 | L2 = lambda v : np.sqrt(1./np.dot(*v.shape)*np.einsum('ij->', (np.abs(v))**2)) 5 | Linf = lambda v : np.max(np.abs(v)) 6 | 7 | def _spec_variance(ph): 8 | # only half the spectrum for real ffts, needs spectral normalisation 9 | nx, nk = ph.shape 10 | ny = (nk-1)*2 11 | var_dens = 2 * np.abs(ph)**2 / (nx*ny)**2 12 | # only half of coefs [0] and [nx/2+1] due to symmetry in real fft2 13 | var_dens[..., 0] /= 2. 14 | var_dens[...,-1] /= 2. 15 | 16 | return var_dens.sum(axis=(-2,-1)) 17 | 18 | 19 | def Curl(u, v, dx, dy): 20 | curl = np.zeros_like(u) 21 | curl[1:-1,1:-1] = (u[1:-1,2:]-u[1:-1,:-2])/dy - ((v[2:,1:-1]-v[:-2,1:-1])/dx) 22 | return curl 23 | 24 | 25 | def FromDat(x, y, Re, **kwargs): 26 | field = np.genfromtxt(kwargs.get('name', 1.))[1:,:] 27 | if(type(field)==np.ndarray): 28 | if(field.shape==(len(x), len(y))): 29 | return field 30 | else: 31 | print("Specified velocity field does not match grid initialized.") 32 | 33 | 34 | def TaylorGreen(x, y, Re, **kwargs): 35 | kappa = kwargs.get('kappa', 1.) 36 | t = kwargs.get('time', 0.) 37 | field = 2 * kappa * np.cos(kappa * x) * np.cos(kappa * y[:, np.newaxis]) *\ 38 | np.exp(-2 * kappa**2 * t / Re) 39 | return field 40 | 41 | 42 | def ShearLayer(x, y, Re, **kwargs): 43 | delta = kwargs.get('delta', 0.005) 44 | sigma = kwargs.get('sigma', 15./np.pi) 45 | field = delta * np.cos(x) - sigma * np.cosh(sigma * (y[:,np.newaxis] -\ 46 | 0.5*np.pi))**(-2) 47 | field+= delta * np.cos(x) + sigma * np.cosh(sigma * (1.5*np.pi -\ 48 | y[:,np.newaxis]))**(-2) 49 | return field 50 | 51 | 52 | def ConvectiveVortex(x, y, Re, **kwargs): 53 | Uinf = kwargs.get('Uinf', 1.) 54 | beta = kwargs.get('beta', 1./50.) 55 | R = kwargs.get('R', 0.005*np.pi) 56 | dx=x[1]-x[0]; dy=y[1]-y[0] 57 | # radial distance to vortex core 58 | rx = x - np.pi 59 | ry = y[:,np.newaxis]-np.pi 60 | r = np.sqrt(rx**2+ry**2) 61 | 62 | # init field 63 | # u = Uinf*(1-beta*(y[:,np.newaxis]-np.pi)/R*np.exp(-r**2/2)) 64 | # v = Uinf*beta*(x-np.pi)/R*np.exp(-r**2/2) 65 | beta = 5. 66 | u = Uinf-beta/(2*np.pi)*np.exp(0.5*(1-r**2))*ry 67 | v = Uinf+beta/(2*np.pi)*np.exp(0.5*(1-r**2))*rx 68 | return Curl(u, v, dx, dy) 69 | 70 | 71 | def McWilliams(x, y, Re, **kwargs): 72 | """ 73 | Generates McWilliams vorticity field, see: 74 | McWilliams (1984), "The emergence of isolated coherent vortices in turbulent flow" 75 | """ 76 | # Fourier mesh 77 | nx = len(x); kx = np.fft.fftfreq(nx, d=1./nx) 78 | ny = len(y); ky = np.fft.fftfreq(ny, d=1./ny) 79 | nk = ny//2+1 80 | 81 | # generate variable 82 | k2 = kx[:nk]**2 + ky[:,np.newaxis]**2 83 | fk = k2 != 0.0 84 | 85 | # ensemble variance proportional to the prescribed scalar wavenumber function 86 | ck = np.zeros((nx, nk)) 87 | ck[fk] = (np.sqrt(k2[fk])*(1+(k2[fk]/36)**2))**(-1) 88 | 89 | # Gaussian random realization for each of the Fourier components of psi 90 | psih = np.random.randn(nx, nk)*ck+\ 91 | 1j*np.random.randn(nx, nk)*ck 92 | 93 | # ṃake sure the stream function has zero mean 94 | cphi = 0.65*np.max(kx) 95 | wvx = np.sqrt(k2) 96 | filtr = np.exp(-23.6*(wvx-cphi)**4.) 97 | filtr[wvx<=cphi] = 1. 98 | KEaux = _spec_variance(filtr*np.sqrt(k2)*psih) 99 | psi = psih/np.sqrt(KEaux) 100 | 101 | # inverse Laplacian in k-space 102 | wh = k2 * psi 103 | 104 | # vorticity in physical space 105 | field = np.fft.irfft2(wh) 106 | return field 107 | 108 | 109 | def EnergySpectrum(k, s=3, kp=12): 110 | 111 | # normalise the spectrum 112 | a_s = (2*s + 1)**(s + 1) / (2**s * math.factorial(s)) 113 | 114 | # compute sectrum at this wave number 115 | E = a_s/ (2 * kp) * (k / kp)**(2*s + 1) * np.exp(-(s + 0.5) * (k / kp)**2) 116 | 117 | return E 118 | 119 | 120 | def PhaseFunction(kx, ky): 121 | 122 | # half the array size 123 | lenx2 = len(kx)//2; leny2 = len(ky)//2 124 | 125 | # define phase array 126 | xi = np.zeros((len(kx), len(ky))) 127 | 128 | # compute phase field in k space, need more points because of how 129 | # python organises wavenumbers 130 | zeta = 2 * np.pi * np.random.rand(lenx2+1, leny2+1) 131 | eta = 2 * np.pi * np.random.rand(lenx2+1, leny2+1) 132 | 133 | # quadrant \xi(kx,ky) = \zeta(kx,ky) + \eta(kx,ky) 134 | xi[:lenx2, :leny2] = zeta[:-1,:-1] + eta[:-1,:-1] 135 | 136 | # quadrant \xi(-kx,ky) = -\zeta(kx,ky) + \eta(kx,ky) 137 | xi[lenx2:, :leny2] = np.flip(-zeta[1:,:-1] + eta[1:,:-1], 0) 138 | 139 | # quadrant \xi(-kx,-ky) = -\zeta(kx,ky) - \eta(kx,ky) 140 | xi[lenx2:, leny2:] = np.flip(-zeta[1:,1:] - eta[1:,1:]) 141 | 142 | # quadrant \xi(kx,-ky) = \zeta(kx,ky) - \eta(kx,ky) 143 | xi[:lenx2, leny2:] = np.flip(zeta[:-1,1:] - eta[:-1,1:], 1) 144 | 145 | return np.exp(1j * xi) 146 | 147 | 148 | def DecayingTurbulence(x, y, Re, **kwargs): 149 | """ 150 | Generates random vorticity field, see: 151 | Omer San, Anne E. Staples : High-order methods for decaying two-dimensional homogeneous isotropic turbulence 152 | """ 153 | # Fourier mesh 154 | nx = len(x); kx = np.fft.fftfreq(nx, d=1./nx) 155 | ny = len(y); ky = np.fft.fftfreq(ny, d=1./ny) 156 | 157 | # define 2D spectrum array 158 | w_hat = np.empty((len(kx), len(ky)), dtype=np.complex128) 159 | 160 | # compute vorticity field in k space 161 | k = np.sqrt(kx**2 + ky[:, np.newaxis]**2) 162 | w_hat = np.sqrt((k / np.pi) * EnergySpectrum(k)) 163 | 164 | # add random phase 165 | whatk = w_hat * PhaseFunction(kx, ky) 166 | 167 | # transforms initial field in physical space 168 | w = np.fft.ifft2(whatk) * nx * ny 169 | 170 | return np.real(w) -------------------------------------------------------------------------------- /src/fluid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Marin Lauber" 5 | __copyright__ = "Copyright 2019, Marin Lauber" 6 | __license__ = "GPL" 7 | __version__ = "1.0.1" 8 | __email__ = "M.Lauber@soton.ac.uk" 9 | 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | import pyfftw 13 | from src.field import _spec_variance 14 | from src.io import netCDFwriter 15 | 16 | class Fluid(object): 17 | 18 | def __init__(self, nx, ny, Re, dt=0.0001, pad=3./2.): 19 | """ 20 | initalizes the fluid, given a number or grid points in x and y. Sets flow parameters. 21 | Parameters: 22 | nx : intger 23 | - gird points in the x-direction 24 | ny : integer 25 | - grid points in the y-direction 26 | Re : float 27 | - Reynolds number of the flow, for very large value, set to zero 28 | dt : float 29 | - time-step, outdated as we use adaptive time-step 30 | pad : float 31 | - padding length for Jacobian evaluation 32 | """ 33 | # input data 34 | self.nx = nx 35 | self.ny = ny; self.nk = self.ny//2+1 36 | self.Re = Re; self.ReI = 0. 37 | if self.Re != 0.: self.ReI = 1./self.Re 38 | self.dt = dt 39 | self.pad = pad 40 | self.time = 0. 41 | self.it = 0 42 | self.uptodate = False 43 | self.filterfac = 23.6 44 | 45 | self.FFTW = True 46 | self.fftw_num_threads = 6 47 | self.forced = False 48 | 49 | # we assume 2pi periodic domain in each dimensions 50 | self.x, self.dx = np.linspace(0, 2*np.pi, nx, endpoint=False, retstep=True) 51 | self.y, self.dy = np.linspace(0, 2*np.pi, ny, endpoint=False, retstep=True) 52 | 53 | # do not write by default 54 | self.write_enable = False 55 | 56 | 57 | def _wavenumber(self): 58 | kx = np.fft.fftfreq(self.nx, d=1./self.nx) 59 | ky = np.fft.fftfreq(self.ny, d=1./self.ny) 60 | if self.order!="spectral": 61 | kx = self.k_prime(kx, self.dx, self.coeffs[self.order]) 62 | ky = self.k_prime(ky, self.dy, self.coeffs[self.order]) 63 | return kx, ky 64 | 65 | 66 | def k_prime(self, kx, dx, coeffs=(1./3, 0., 14./9, 1./9, 0.)): 67 | alpha, beta, a, b, c = coeffs 68 | kp = (a * np.sin(kx*dx) + (.5*b)*np.sin(2*kx*dx) + (c/3*np.sin(3*kx*dx))) 69 | kp /= (1 + 2*alpha*np.cos(kx*dx) + 2*beta*np.cos(2*kx*dx)) / dx 70 | return kp 71 | 72 | 73 | def init_solver(self, order="spectral"): 74 | """ 75 | Initalizes storage arrays and FFT objects. This is relatively expansive 76 | as pyFFTW find the fastest way to perform the transfoms, but it is only 77 | called once. 78 | """ 79 | # numerical method 80 | self.order = order 81 | self.coeffs = {"CDS2": ( 0, 0, 1, 0, 0), 82 | "CDS4": ( 0, 0, 4./3, -1./3, 0), 83 | "Pade4": (1./4, 0, 3./2, 0, 0), 84 | "Pade6": (1./3, 0, 14./9, 1./9, 0.)} 85 | 86 | 87 | # fourier grid 88 | self.kx, self.ky = self._wavenumber() 89 | try: 90 | self.k2 91 | except AttributeError: 92 | self.k2 = self.kx[:self.nk]**2 + self.ky[:,np.newaxis]**2 93 | self.k2I = self._empty_imag((self.nx, self.nk)) 94 | fk = self.k2 != 0.0 95 | self.k2I[fk] = 1./self.k2[fk] 96 | 97 | # utils 98 | self.mx = int(self.pad * self.nx) 99 | self.mk = int(self.pad * self.nk) 100 | self.my = int(self.pad * self.ny) 101 | 102 | # for easier slicing when padding 103 | self.padder = np.ones(self.mx, dtype=bool) 104 | self.padder[int(self.nx/2):int(self.nx*(self.pad-0.5)):] = False 105 | 106 | # initialise array required for solving 107 | self.u = self._empty_real() 108 | self.v = self._empty_real() 109 | self.w = self._empty_real() 110 | self.f = self._empty_real() 111 | 112 | self.uh = self._empty_imag() 113 | self.vh = self._empty_imag() 114 | self.wh0 = self._empty_imag() 115 | self.wh = self._empty_imag() 116 | self.fh = self._empty_imag() 117 | self.psih = self._empty_imag() 118 | self.dwhdt = self._empty_imag() 119 | 120 | # assign padded arrays for non-linear term 121 | self.a = self._empty_imag((self.mx,self.mk)) 122 | self.a1 = self._empty_imag((self.mx,self.mk)) 123 | self.a2 = self._empty_imag((self.mx,self.mk)) 124 | self.a3 = self._empty_imag((self.mx,self.mk)) 125 | self.a4 = self._empty_imag((self.mx,self.mk)) 126 | 127 | self.b = self._empty_real((self.mx,self.my)) 128 | self.b1 = self._empty_real((self.mx,self.my)) 129 | self.b2 = self._empty_real((self.mx,self.my)) 130 | self.b3 = self._empty_real((self.mx,self.my)) 131 | self.b4 = self._empty_real((self.mx,self.my)) 132 | 133 | # for fast transform 134 | # pyfftw.interfaces.cache.enable() 135 | pyfftw.config.NUM_THREADS = self.fftw_num_threads 136 | pyfftw.config.PLANNER_EFFORT = 'FFTW_MEASURE' 137 | 138 | self.w_to_wh = pyfftw.FFTW(self.w, self.wh, threads=self.fftw_num_threads, 139 | axes=(-2,-1)) 140 | self.wh_to_w = pyfftw.FFTW(self.wh, self.w, threads=self.fftw_num_threads, 141 | direction='FFTW_BACKWARD', axes=(-2,-1)) 142 | self.u_to_uh = pyfftw.FFTW(self.u, self.uh, threads=self.fftw_num_threads, 143 | axes=(-2,-1)) 144 | self.uh_to_u = pyfftw.FFTW(self.uh, self.u, threads=self.fftw_num_threads, 145 | direction='FFTW_BACKWARD', axes=(-2,-1)) 146 | self.v_to_vh = pyfftw.FFTW(self.v, self.vh, threads=self.fftw_num_threads, 147 | axes=(-2,-1)) 148 | self.f_to_fh = pyfftw.FFTW(self.f, self.fh, threads=self.fftw_num_threads, 149 | axes=(-2,-1)) 150 | self.vh_to_v = pyfftw.FFTW(self.vh, self.v, threads=self.fftw_num_threads, 151 | direction='FFTW_BACKWARD', axes=(-2,-1)) 152 | self.b_to_a = pyfftw.FFTW(self.b, self.a, threads=self.fftw_num_threads, 153 | axes=(-2,-1)) 154 | self.a1_to_b1 = pyfftw.FFTW(self.a1, self.b1, threads=self.fftw_num_threads, 155 | direction='FFTW_BACKWARD', axes=(-2,-1)) 156 | self.a2_to_b2 = pyfftw.FFTW(self.a2, self.b2, threads=self.fftw_num_threads, 157 | direction='FFTW_BACKWARD', axes=(-2,-1)) 158 | self.a3_to_b3 = pyfftw.FFTW(self.a3, self.b3, threads=self.fftw_num_threads, 159 | direction='FFTW_BACKWARD', axes=(-2,-1)) 160 | self.a4_to_b4 = pyfftw.FFTW(self.a4, self.b4, threads=self.fftw_num_threads, 161 | direction='FFTW_BACKWARD', axes=(-2,-1)) 162 | 163 | # ṣpectral filter 164 | try: 165 | self.fltr 166 | except AttributeError: 167 | self._init_filter() 168 | 169 | 170 | def init_field(self, func, **kwargs): 171 | """ 172 | Inital the vorticity field. 173 | 174 | Params: 175 | field : a function that return the desired field 176 | prototype function is : f(x, y, Re, *kwargs) 177 | """ 178 | if not callable(func): 179 | raise "Error: func must be callable, prototype function is : f(x, y, Re, *kwargs)" 180 | self.w[:,:] = func(self.x, self.y, self.Re, **kwargs) 181 | self.w_to_wh() 182 | 183 | 184 | # bit-aligned storage arrays for pyFFTW 185 | def _empty_real(self, *args): 186 | shape = (self.nx, self.ny) 187 | for sp in args: 188 | shape = sp 189 | if self.FFTW: 190 | out = pyfftw.empty_aligned(shape, dtype='float64') 191 | out.flat[:] = 0. 192 | return out 193 | else: 194 | return np.zeros(shape, dtype='float64') 195 | 196 | 197 | def _empty_imag(self, *args): 198 | shape = (self.nx, self.nk) 199 | for sp in args: 200 | shape = sp 201 | if self.FFTW: 202 | out = pyfftw.empty_aligned(shape, dtype='complex128') 203 | out.flat[:] = 0. + 0.*1j 204 | return out 205 | else: 206 | return np.zeros(shape, dtype='complex128') 207 | 208 | 209 | def get_u(self): 210 | self.uh[:,:] = 1j*self.ky[:,np.newaxis]*self.psih[:, :] 211 | self.uh_to_u() 212 | 213 | 214 | def get_v(self): 215 | self.vh[:,:] = -1j*self.kx[:self.nk]*self.psih[:, :] 216 | self.vh_to_v() 217 | 218 | 219 | def _init_filter(self): 220 | """ 221 | Exponential filter, designed to completely dampens higest modes to machine accuracy 222 | """ 223 | cphi = 0.65*np.max(self.kx) 224 | wvx = np.sqrt(self.k2) 225 | filtr = np.exp(-self.filterfac*(wvx-cphi)**4.) 226 | filtr[wvx<=cphi] = 1. 227 | self.fltr = filtr 228 | 229 | 230 | def _cfl_limit(self): 231 | """ 232 | Adjust time-step based on the courant condition 233 | """ 234 | self.get_u() 235 | self.get_v() 236 | Dc = np.max(np.pi*((1.+abs(self.u))/self.dx + (1.+abs(self.v))/self.dy)) 237 | Dmu = np.max(np.pi**2*(self.dx**(-2) + self.dy**(-2))) 238 | self.dt = np.sqrt(3.) / (Dc + Dmu) 239 | 240 | 241 | def _init_forcing(self, f): 242 | if callable(f): 243 | self.func = f 244 | self.forced = True 245 | 246 | 247 | def _add_forcing(self): 248 | self.f[:, :] = self.func(self.x, self.y) 249 | self.f_to_fh() 250 | 251 | 252 | def update(self, s=3): 253 | """ 254 | Hybrid implicit-explicit total variational diminishing Runge-Kutta 3rd-order 255 | from Gottlieb and Shu (1998) or low-storage S-order Runge-Kutta method from 256 | Jameson, Schmidt and Turkel (1981). 257 | Input: 258 | s : float 259 | - desired order of the method, default is 3rd order 260 | """ 261 | # iniitalise field 262 | if self.forced: self._add_forcing() 263 | self.wh0[:, :] = self.wh[:, :] + self.fh[:, :] 264 | 265 | for k in range(s, 0, -1): 266 | # for t, v, d in zip([1.,.75,1./3.],[0.,.25,2./3.],[1.,.25,2./3.]): 267 | # invert Poisson equation for the stream function (changes to k-space) 268 | self._get_psih() 269 | 270 | # get convective forces (resets dwhdt) 271 | self._add_convection() 272 | 273 | # add diffusion 274 | self._add_diffusion() 275 | 276 | # step in time 277 | self.wh[:, :] = self.wh0[:, :] + (self.dt/k) * self.dwhdt[:, :] 278 | # self.w[:, :] = (t*self.w0[:, :] + v*self.w[:, :] + d*self.dt*self.dwdt[:, :]) 279 | 280 | self.time += self.dt 281 | self._cfl_limit() 282 | self.it += 1 283 | 284 | 285 | def _get_psih(self): 286 | """ 287 | Spectral stream-function from spectral vorticity 288 | psi = omega / (k_x^2 + k_y^2) 289 | """ 290 | self.psih[:,:] = self.wh[:,:] * self.k2I[:,:] 291 | 292 | 293 | def _add_convection(self): 294 | """ 295 | Convective term 296 | d/dx psi * d/dy omega - d/dy psi * d/dx omega 297 | To prevent alliasing, we zero-pad the array before using the 298 | convolution theorem to evaluate it in physical space. 299 | 300 | Note: this resets dwhdt when called 301 | """ 302 | # padded arrays 303 | j1f_padded = np.zeros((self.mx,self.mk),dtype='complex128') 304 | j2f_padded = np.zeros((self.mx,self.mk),dtype='complex128') 305 | j3f_padded = np.zeros((self.mx,self.mk),dtype='complex128') 306 | j4f_padded = np.zeros((self.mx,self.mk),dtype='complex128') 307 | 308 | j1f_padded[self.padder, :self.nk] = 1.0j*self.kx[:self.nk ]*self.psih[:, :] 309 | j2f_padded[self.padder, :self.nk] = 1.0j*self.ky[:, np.newaxis]*self.wh[:, :] 310 | j3f_padded[self.padder, :self.nk] = 1.0j*self.ky[:, np.newaxis]*self.psih[:, :] 311 | j4f_padded[self.padder, :self.nk] = 1.0j*self.kx[:self.nk ]*self.wh[:, :] 312 | 313 | # ifft 314 | j1 = self.a1_to_b1(j1f_padded) 315 | j2 = self.a2_to_b2(j2f_padded) 316 | j3 = self.a3_to_b3(j3f_padded) 317 | j4 = self.a4_to_b4(j4f_padded) 318 | 319 | jacp = j1*j2 - j3*j4 320 | 321 | jacpf = self.b_to_a(jacp) 322 | 323 | self.dwhdt[:, :] = jacpf[self.padder, :self.nk]*self.pad**(2) # this term is the result of padding 324 | 325 | 326 | def _add_diffusion(self): 327 | """ 328 | Diffusion term of the Navier-Stokes 329 | 1/Re * (-k_x^2 -k_y^2) * omega 330 | """ 331 | self.dwhdt[:, :] = self.dwhdt[:, :] - self.ReI*self.k2*self.wh[:, :] 332 | 333 | 334 | def _add_spec_filter(self): 335 | self.dwhdt *= self.fltr 336 | 337 | 338 | def tke(self): 339 | ke = .5*_spec_variance(np.sqrt(self.k2)*self.psih) 340 | return ke.sum() 341 | 342 | 343 | def enstrophy(self): 344 | wh = self.wh[:,:] 345 | w = np.fft.irfft2(wh,axes=(-2,-1)) 346 | eps = .5*abs(w)**2 347 | return eps.sum(axis=(-2,-1)) 348 | 349 | 350 | def _compute_spectrum(self, res): 351 | self._get_psih() 352 | # angle averaged TKE spectrum 353 | tke = np.real(.5*self.k2*self.psih*np.conj(self.psih)) 354 | kmod = np.sqrt(self.k2) 355 | self.k = np.arange(1, self.nk, 1, dtype=np.float64) # nyquist limit for this grid 356 | self.E = np.zeros_like(self.k) 357 | dk = (np.max(self.k)-np.min(self.k))/res 358 | 359 | # binning energies with wavenumber modulus in threshold 360 | for i in range(len(self.k)): 361 | self.E[i] += np.sum(tke[(kmod=self.k[i]-dk)]) 362 | 363 | 364 | def plot_spec(self, res=200): 365 | self._compute_spectrum(200) 366 | plt.figure(figsize=(6,6)) 367 | plt.loglog(self.k, self.E, '-k', label="E(k)") 368 | plt.xlabel("k") 369 | plt.ylabel("E(k)") 370 | plt.legend() 371 | plt.show() 372 | 373 | 374 | def write(self, file): 375 | if(not self.write_enable): self.init_writer(file) 376 | self.writer.add(self) 377 | 378 | 379 | def init_writer(self, name): 380 | self.writer = netCDFwriter(name, self) 381 | self.write_enable = True 382 | 383 | 384 | def display(self, complex=False, u_e=None): 385 | u = self.w 386 | if complex: 387 | u = np.real(self.wh) 388 | if not np.any(u_e)==None: 389 | u -= u_e 390 | p=plt.imshow(u, cmap="RdBu_r") 391 | plt.colorbar(p) 392 | # plt.xticks([]); plt.yticks([]) 393 | plt.show() 394 | 395 | 396 | def display_vel(self): 397 | if(self.uptodate!=True): 398 | self.w_to_wh() 399 | self._get_psih() 400 | self.get_u() 401 | self.get_v() 402 | plt.figure() 403 | plt.streamplot(self.x, self.y, self.u, self.v) 404 | plt.xlabel("x"); plt.ylabel("y") 405 | plt.show() 406 | 407 | 408 | def run_live(self, stop, every=100): 409 | from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable 410 | plt.ion() 411 | fig = plt.figure() 412 | ax = fig.add_subplot(111) 413 | im = ax.imshow(np.fft.irfft2(self.wh, axes=(-2,-1)), norm=None, cmap="RdBu") 414 | cax = make_axes_locatable(ax).append_axes("right", size="5%", pad="2%") 415 | cb = fig.colorbar(im, cax=cax) 416 | ax.set_xticks([]); ax.set_yticks([]) 417 | while(self.time<=stop): 418 | # update using RK 419 | self.update() 420 | if(self.it % every == 0): 421 | im.set_data(np.fft.irfft2(self.wh, axes=(-2,-1))) 422 | fig.canvas.draw() 423 | fig.canvas.flush_events() 424 | plt.pause(1e-9) 425 | print("Iteration \t %d, time \t %f, time remaining \t %f. TKE: %f" %(self.it, 426 | self.time, stop-self.time, self.tke())) 427 | 428 | 429 | # if __name__=="__main__": 430 | # flow = Fluid(128, 128, 1) 431 | # flow.init_solver() 432 | # flow.init_field("TG") 433 | # print(flow.tke()) 434 | -------------------------------------------------------------------------------- /src/io.py: -------------------------------------------------------------------------------- 1 | from netCDF4 import Dataset 2 | import numpy as np 3 | 4 | class netCDFwriter(object): 5 | 6 | def __init__(self, name, flow) -> None: 7 | 8 | # the data set that we use to store data 9 | self.data = Dataset(name+'.nc','w','NETCDF4') # using netCDF4 for output format 10 | 11 | # two space dimension and a time dimension 12 | self.data.createDimension('x',flow.nx) 13 | self.data.createDimension('y',flow.ny) 14 | self.data.createDimension('t',None) 15 | 16 | # fill-in the coordinates 17 | self.x = self.data.createVariable('x','float32',('x')) 18 | self.x[:] = flow.x 19 | self.y = self.data.createVariable('y','float32',('y')) 20 | self.y[:] = flow.y 21 | self.t = self.data.createVariable('t','float32',('t')) 22 | 23 | # set up the vorticity 24 | self.w = self.data.createVariable('w','float32',('t','y','x')) 25 | self.w.setncattr('units','1/s') 26 | 27 | # counter 28 | self.c = 0 29 | 30 | # add initial flow data 31 | self.add(flow) 32 | 33 | 34 | def add(self, flow) -> None: 35 | 36 | # set the time 37 | self.t[self.c] = flow.time 38 | 39 | # write vorticity 40 | self.w[self.c,:,:] = np.fft.irfft2(flow.wh, axes=(-2,-1)) 41 | 42 | # update counter 43 | self.c += 1 44 | 45 | 46 | def close(self) -> None: 47 | # close the Dataset, not mandatory 48 | self.data.close() -------------------------------------------------------------------------------- /src/valid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Marin Lauber" 5 | __copyright__ = "Copyright 2019, Marin Lauber" 6 | __license__ = "GPL" 7 | __version__ = "1.0.1" 8 | __email__ = "M.Lauber@soton.ac.uk" 9 | 10 | import time as t 11 | import numpy as np 12 | from src.fluid import Fluid 13 | from src.field import TaylorGreen,L2,Linf 14 | 15 | if __name__=="__main__": 16 | 17 | # build fluid and solver 18 | flow = Fluid(128, 128, 1.) 19 | flow.init_solver() 20 | flow.init_field(TaylorGreen) 21 | 22 | print("Starting integration on field.\n") 23 | start_time = t.time() 24 | finish = 0.1 25 | 26 | # loop to solve 27 | while(flow.time<=finish): 28 | 29 | # update using RK 30 | flow.update() 31 | 32 | # print every 100 iterations 33 | if (flow.it % 100 == 0): 34 | print("Iteration \t %d, time \t %f, time remaining \t %f. TKE: %f, ENS: %f" %(flow.it, 35 | flow.time, finish-flow.time, flow.tke(), flow.enstrophy())) 36 | # flow.run_live(finish, every=100) 37 | 38 | end_time = t.time() 39 | print("\nExecution time for %d iterations is %f seconds." %(flow.it, end_time-start_time)) 40 | 41 | # get final results 42 | flow.wh_to_w() 43 | w_n = flow.w.copy() 44 | 45 | # exact solution 46 | w_e = TaylorGreen(flow.x, flow.y,flow.Re, time=flow.time) 47 | 48 | # L2-norm and exit 49 | print("The L2-norm of the Error in the Taylor-Green vortex on a %dx%d grid is %e." % (flow.nx, flow.ny, L2(w_e - w_n)) ) 50 | print("The Linf-norm of the Error in the Taylor-Green vortex on a %dx%d grid is %e." % (flow.nx, flow.ny, Linf(w_e - w_n)) ) 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # empty __init__.py file 2 | -------------------------------------------------------------------------------- /tests/test_solver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Marin Lauber" 5 | __copyright__ = "Copyright 2019, Marin Lauber" 6 | __license__ = "GPL" 7 | __version__ = "1.0.1" 8 | __email__ = "M.Lauber@soton.ac.uk" 9 | 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | from src.fluid import Fluid 13 | from src.field import TaylorGreen, ShearLayer, McWilliams 14 | 15 | def test_diagnostics(): 16 | flow = Fluid(64, 64, 1.) 17 | flow.init_solver() 18 | flow.init_field(McWilliams) 19 | flow._get_psih() 20 | assert np.isclose(np.round(flow.tke(),1), 0.5, atol=1e-3),\ 21 | "Error: TKE do not match" 22 | assert flow.enstrophy!=0.0, "Error: Enstrophy is zero" 23 | flow._compute_spectrum(200) 24 | 25 | 26 | def test_plots(): 27 | flow = Fluid(16, 16, 1.) 28 | flow.init_solver() 29 | flow.init_field(ShearLayer) 30 | plt.ion() 31 | flow.plot_spec() 32 | plt.close() 33 | flow.display() 34 | plt.close() 35 | flow.display_vel() 36 | plt.close() 37 | 38 | 39 | def test_update(): 40 | # build field 41 | flow = Fluid(32, 32, 1.) 42 | flow.init_solver() 43 | flow.init_field(TaylorGreen) 44 | # start update 45 | while(flow.time<=0.1): 46 | flow.update() 47 | # get final results 48 | flow.wh_to_w() 49 | w_n = flow.w.copy() 50 | # exact solution 51 | w_e = TaylorGreen(flow.x, flow.y,flow.Re, time=flow.time) 52 | assert np.allclose(w_n, w_e, atol=1e-6), "Error: solver diverged." 53 | 54 | 55 | # if __name__=="__main__": 56 | # test_diagnostics() 57 | # test_plots() 58 | # test_update() 59 | --------------------------------------------------------------------------------