├── .gitignore ├── INSTALL ├── LICENSE ├── Makefile ├── Makefile.sphinx ├── README ├── README.md ├── build.py ├── conf.py ├── core ├── Makefile ├── advection.py ├── ana_profiles.py ├── boussinesq.py ├── boussinesqTS.py ├── default.xml ├── defaults.json ├── diagnostics.py ├── euler.py ├── fluid2d.py ├── fluxes.py ├── fortran_advection.f90 ├── fortran_diag.f90 ├── fortran_fluxes.f90 ├── fortran_operators.f90 ├── fourier.py ├── gmg │ ├── Makefile │ ├── __init__.py │ ├── fortran_multigrid.f90 │ ├── halo.py │ ├── hierarchy.py │ ├── level.py │ └── subdomains.py ├── grid.py ├── island.py ├── mpitools.py ├── operators.py ├── output.py ├── parallel_file.py ├── param.py ├── plotting.py ├── qgtwolayers.py ├── quasigeostrophic.py ├── restart.py ├── sqg.py ├── test_euler_log_jobs ├── thermalwind.py ├── timers.py ├── timescheme.py └── variables.py ├── docs ├── experiment_template.py ├── fluid2d_schematic.pdf ├── howto.rst ├── model_equations.rst ├── model_numerics.rst ├── parameters.pdf └── staggering.png ├── environment.yml ├── experiments ├── Advection │ ├── advection_footprint.py │ ├── advection_irreversibility.py │ ├── all_about_advection.py │ └── plotting_adv.py ├── GravityCurrent │ └── gravitycurrent.py ├── GyreCirculation │ └── double_gyre.py ├── InterfacialWave │ └── interfacialwave.py ├── Internal_IVP │ └── internalwave.py ├── Internal_forced │ ├── forcedinternal.py │ └── forcing.py ├── KelvinHelmholtz │ └── kelvin_helmholtz.py ├── Leewave │ └── leewave.py ├── LockExchange │ ├── fdse_lab_exp.py │ └── lockexchange.py ├── QG_channel │ └── channel.py ├── QGbasic │ ├── turbqg.py │ ├── vortex_betaplane.py │ ├── vortex_betaplane_v2.py │ └── vortex_on_topography.py ├── RayleighBenard │ ├── conv_atmos.py │ ├── diag_conv.py │ ├── forcing_rayleigh.py │ ├── horiz_convection.py │ └── rayleigh_benard.py ├── SQG │ └── vortex.py ├── ShearInstab │ └── shear_instability.py ├── SymmetricInstability │ ├── plotting_SI.py │ └── symmetric_instab.py ├── TaylorInstability │ └── taylor_instability.py ├── Twodim_turbulence │ ├── freedecay │ │ └── freedecay.py │ ├── spectrum_analysis.py │ └── turb2d_forced │ │ ├── energy_cascade.py │ │ └── enstrophy_cascade.py ├── VonKarman │ └── karman_street.py ├── Vortex │ └── vortex.py ├── doublediffusion │ └── doublediffusion.py └── vorticity_dynamics │ ├── axisymm.py │ ├── merging.py │ ├── vortex.py │ └── wavepackets.py ├── gallery ├── DW_0.png ├── DW_1.png ├── TI_0.png └── gallery.rst ├── index.rst ├── install.sh ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | myexp 2 | __pycache__ 3 | *.so 4 | *~ 5 | *.pyc 6 | _build -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | cd fluid2d 2 | pip install -r requirements.txt 3 | python setup.py build_ext --inplace 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TXT = Makefile README LICENSE 2 | 3 | PYTHON = python 4 | F2PY = f2py 5 | 6 | 7 | %.so : %.f90 ; $(F2PY) -m $* -c $< --opt='-O3 -fPIC' --quiet 8 | 9 | 10 | all: 11 | export F2PY=$(F2PY) ; cd core && $(MAKE) 12 | 13 | clean: 14 | rm -f core/*.so core/gmg/*.so 15 | 16 | 17 | zip: 18 | cd .. ; zip -r Fluid2d/Fluid2d_`date '+%d_%m_%Y'`.zip Fluid2d/Makefile Fluid2d/README* Fluid2d/activate.* Fluid2d/LICENSE Fluid2d/INSTALL Fluid2d/requirements.txt Fluid2d/setup.py Fluid2d/core Fluid2d/experiments/ Fluid2d/docs -x \*.so \*~ \*.pyc \*.nc \.* \*checkpoint* \*pycache* \*.rst \*.png 19 | 20 | 21 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | Fluid2d 3 | ------- 4 | 5 | Everything you need to know is 6 | 7 | http://pagesperso.univ-brest.fr/~roullet/fluid2d/index.html 8 | 9 | If you've questions, you can reach me at 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fluid2d 2 | Fluid2d is a versatile Python-Fortran CFD code that solves a large 3 | class of 2D flows. It is designed to be used easily by Students 4 | learning Fluid Mechanics or Geophysical Fluid Dynamics and by their 5 | professors willing to illustrate their course and to organize 6 | numerical practicals. The idea is to visualize flows on the fly, as 7 | they are computed. The effect of parameter changes can be seen 8 | immediately. The key quantity of fluid2D is the vorticity. If you feel 9 | weak on vorticity dynamics, this code is for you. You should rapidly 10 | become as expert as the experts. 11 | 12 | You can learn how basic processes work because of the power of 13 | animations. It is quite easy to go beyond textbooks and to reach 14 | research questions. 15 | 16 | 17 | # Install 18 | Fluid2d installation does not rely on the standard `pip install Fluid2d`. The procedure is a bit more convoluted 19 | 20 | 1) git clone Fluid2d. To copy-paste from your computer to the virtual desktop, use the copy-paste interactive window on the left 21 | 22 | > git clone https://github.com/pvthinker/Fluid2d.git 23 | 24 | 2) create the conda environment (this step may fail in that case, execute the next step). It takes a few minutes to download everything. It’s a bit a shame to have to download all these packages in your $HOME but that’s how it works. 25 | 26 | > cd Fluid2d 27 | 28 | > conda create --name fluid2d --file requirements.txt 29 | 30 | if conda create fails 31 | 32 | > conda init bash 33 | 34 | then close the terminal, reopen a new one, module load anaconda3 and repeat the conda 35 | 36 | > create --name fluid2d --file requirements.txt 37 | 38 | That should work. 39 | 40 | 3) activate this environment 41 | 42 | > conda activate fluid2d 43 | 44 | 4) you can now build Fluid2d 45 | 46 | > ./install.sh 47 | 48 | 5) last step, make Python aware of where Fluid2d is 49 | 50 | > source ~/.fluid2d/activate.sh 51 | 52 | you’re good to run Fluid2d 53 | 54 | 6) run your first experiment 55 | 56 | > cd myexp/Vortex 57 | 58 | > python vortex.py 59 | 60 | 61 | # Run an experiment 62 | Once the code is installed, you don't need to repeat stage 4). 63 | 64 | What you need to do though, every time you run Fluid2d in a new 65 | terminal is to repeat stages 3) and 5), then do something like 6) 66 | but with another experiment. 67 | 68 | # Most frequent error 69 | 70 | When trying to run an experiment you may get the following error message 71 | 72 | ModuleNotFoundError: No module named 'fluid2d' 73 | 74 | it is most likely because you forgot to activate Fluid2d. Fix it with 75 | 76 | > source ~/.fluid2d/activate.sh 77 | 78 | and it should work -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compile Fluid2d Fortran routines into Python modules 3 | using f2py 4 | 5 | March 2023: the compilation now by-pass 'make' and 'Makefile' 6 | 7 | """ 8 | import os 9 | import glob 10 | import subprocess 11 | 12 | FFLAGS = "-O3 -march=native -fPIC" 13 | 14 | srcs = ["core/fortran_advection.f90", 15 | "core/fortran_diag.f90", 16 | "core/fortran_fluxes.f90", 17 | "core/fortran_operators.f90", 18 | "core/gmg/fortran_multigrid.f90"] 19 | 20 | 21 | pwd = os.getcwd() 22 | null = subprocess.PIPE 23 | 24 | print("Compile Fortran routines to Python modules") 25 | 26 | for filename in srcs: 27 | 28 | src = os.path.basename(filename) 29 | direc = os.path.dirname(filename) 30 | lib = os.path.splitext(src)[0] 31 | 32 | localdir = f"{pwd}/{direc}" 33 | 34 | cmd = ["f2py", f'--opt={FFLAGS}', "-m", lib, "-c", src] 35 | 36 | print(" ".join(cmd)) 37 | 38 | os.chdir(localdir) 39 | subprocess.call(cmd, stdout=null, stderr=null) 40 | -------------------------------------------------------------------------------- /core/Makefile: -------------------------------------------------------------------------------- 1 | %.so : %.f90 ; $(F2PY) --quiet -m $* -c $< --opt='-O3 -fPIC -Wno-unused-variable -Wno-maybe-uninitialized -Wno-unused-dummy-argument -Wno-tabs -Wno-cpp -Wno-unused-function' 2 | 3 | 4 | all: fortran_diag.so fortran_operators.so fortran_advection.so fortran_fluxes.so 5 | cd gmg && $(MAKE) 6 | -------------------------------------------------------------------------------- /core/advection.py: -------------------------------------------------------------------------------- 1 | from operators import Operators 2 | from variables import Var 3 | from timescheme import Timescheme 4 | import fortran_diag as fd 5 | import numpy as np 6 | 7 | 8 | class Advection(object): 9 | """Advection model 10 | 11 | It solves the 2D transport equation for a passive tracer with a 12 | prescribed steady velocity field, which derives from a 13 | streamfunction 14 | 15 | """ 16 | 17 | def __init__(self, param, grid): 18 | 19 | self.list_param = ['timestepping', 'diffusion', 'Kdiff'] 20 | param.copy(self, self.list_param) 21 | 22 | self.list_grid = ['msk', 'nh', 'area', 'mpitools'] 23 | grid.copy(self, self.list_grid) 24 | 25 | # for variables 26 | param.varname_list = ['tracer', 'psi', 'u', 'v', 'vorticity'] 27 | param.sizevar = [grid.nyl, grid.nxl] 28 | self.var = Var(param) 29 | 30 | # for operators 31 | param.tracer_list = ['tracer'] 32 | param.whosetspsi = ('tracer') 33 | self.ope = Operators(param, grid) 34 | 35 | # for timescheme 36 | self.tscheme = Timescheme(param, self.var.state) 37 | self.tscheme.set(self.advection, self.timestepping) 38 | 39 | self.diags = {} 40 | 41 | def step(self, t, dt): 42 | self.tscheme.forward(self.var.state, t, dt) 43 | self.diagnostics(self.var, t) 44 | 45 | def advection(self, x, t, dxdt): 46 | self.ope.rhs_adv(x, t, dxdt) 47 | 48 | if (self.tscheme.kstage == self.tscheme.kforcing): 49 | if self.diffusion: 50 | self.ope.rhs_diffusion(x, t, dxdt) 51 | 52 | def set_psi_from_tracer(self): 53 | self.ope.invert_vorticity(self.var.state) 54 | 55 | def diagnostics(self, var, t): 56 | """ should provide at least 'maxspeed' (for cfl determination) """ 57 | 58 | if t == 0.: 59 | # since the flow is prescribed, compute it only at t=0 60 | u = var.get('u') 61 | v = var.get('v') 62 | ke, maxu = fd.computekemaxu(self.msk, u, v, self.nh) 63 | 64 | cst = self.mpitools.local_to_global([(maxu, 'max'), (ke, 'sum')]) 65 | self.diags['maxspeed'] = cst[0] 66 | self.diags['ke'] = cst[1] / self.area 67 | self.diags['enstrophy'] = 0. 68 | 69 | trac = var.get('tracer') 70 | 71 | z, z2 = fd.computesumandnorm(self.msk, trac, self.nh) 72 | 73 | cst = self.mpitools.local_to_global([(z, 'sum'), (z2, 'sum')]) 74 | 75 | z = cst[0] / self.area 76 | z2 = cst[1] / self.area 77 | 78 | self.diags['mean'] = z 79 | self.diags['rms'] = np.sqrt(z2) 80 | -------------------------------------------------------------------------------- /core/ana_profiles.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def vortex(xr, yr, Lx, Ly, 5 | x0, y0, sigma, 6 | vortex_type, ratio=1): 7 | 8 | # ratio controls the ellipticity, ratio=1 is a disc 9 | x = np.sqrt((xr-Lx*x0)**2+(yr-Ly*y0)**2*ratio**2) 10 | 11 | y = x.copy()*0. 12 | 13 | if vortex_type in ('gaussian', 'cosine', 'step'): 14 | if vortex_type == 'gaussian': 15 | y = np.exp(-x**2/(sigma**2)) 16 | 17 | if vortex_type == 'cosine': 18 | y = np.cos(x/sigma*np.pi/2) 19 | y[x > sigma] = 0. 20 | 21 | if vortex_type == 'step': 22 | y[x <= sigma] = 1. 23 | else: 24 | print('this kind of vortex (%s) is not defined' % vortex_type) 25 | 26 | return y 27 | -------------------------------------------------------------------------------- /core/boussinesq.py: -------------------------------------------------------------------------------- 1 | from operators import Operators 2 | from variables import Var 3 | from timescheme import Timescheme 4 | from importlib import import_module 5 | import fortran_diag as fd 6 | import fortran_advection as fa 7 | import numpy as np 8 | 9 | 10 | class Boussinesq(object): 11 | """ Boussinesq model 12 | 13 | It provides the step(t,dt) function 14 | and 'var' containing the mode state 15 | """ 16 | 17 | def __init__(self, param, grid): 18 | 19 | self.list_param = ['forcing', 'noslip', 'timestepping', 20 | 'diffusion', 'Kdiff', 'myrank', 21 | 'forcing_module', 'gravity', 'isisland', 22 | 'customized', 'custom_module', 'additional_tracer'] 23 | param.copy(self, self.list_param) 24 | 25 | # for potential energy 26 | self.list_param = ['xr', 'yr', 'nh', 'Lx', 'msk', 'area', 'mpitools'] 27 | grid.copy(self, self.list_param) 28 | 29 | # for variables 30 | param.varname_list = ['vorticity', 31 | 'psi', 'u', 'v', 'buoyancy', 'banom'] 32 | param.tracer_list = ['vorticity', 'buoyancy'] 33 | param.whosetspsi = ('vorticity') 34 | 35 | if hasattr(self, 'additional_tracer'): 36 | for k in range(len(self.additional_tracer)): 37 | trac = self.additional_tracer[k] 38 | param.varname_list.append(trac) 39 | param.tracer_list.append(trac) 40 | 41 | param.sizevar = [grid.nyl, grid.nxl] 42 | self.var = Var(param) 43 | bref = self.var.get('buoyancy').copy() 44 | self.bref = bref 45 | self.source = np.zeros(param.sizevar) 46 | 47 | # for operators 48 | self.ope = Operators(param, grid) 49 | 50 | # for timescheme 51 | self.tscheme = Timescheme(param, self.var.state) 52 | self.tscheme.set(self.dynamics, self.timestepping) 53 | 54 | if self.forcing: 55 | if self.forcing_module == 'embedded': 56 | self.msg_forcing = ( 57 | 'To make Fluid2d aware of your embedded forcing\n' 58 | +'you need to add in the user script \n' 59 | +'model.forc = Forcing(param, grid)\n' 60 | +'right below the line: model = f2d.model' ) 61 | 62 | pass 63 | else: 64 | try: 65 | f = import_module(self.forcing_module) 66 | except ImportError: 67 | print('module %s for forcing cannot be found' 68 | % self.forcing_module) 69 | print('make sure file **%s.py** exists' % 70 | self.forcing_module) 71 | exit(0) 72 | self.forc = f.Forcing(param, grid) 73 | 74 | self.diags = {} 75 | 76 | if self.customized: 77 | try: 78 | f = import_module(self.custom_module) 79 | print(f) 80 | self.extrastep = f.Step(param, grid) 81 | except ImportError: 82 | print('module %s for forcing cannot be found' 83 | % self.custom_module) 84 | print('make sure file **%s.py** exists' % self.custom_module) 85 | exit(0) 86 | 87 | def step(self, t, dt): 88 | 89 | # 1/ integrate advection 90 | self.tscheme.forward(self.var.state, t, dt) 91 | 92 | # 2/ integrate source 93 | if self.noslip: 94 | self.add_noslip(self.var.state) 95 | 96 | if self.customized: 97 | self.extrastep.do(self.var, t, dt) 98 | 99 | banom = self.var.get('banom') 100 | banom[:, :] = self.var.get('buoyancy')-self.bref 101 | 102 | def dynamics(self, x, t, dxdt): 103 | self.ope.rhs_adv(x, t, dxdt) 104 | # db/dx is a source term for the vorticity 105 | self.ope.rhs_torque(x, t, dxdt) 106 | if (self.tscheme.kstage == self.tscheme.kforcing): 107 | coef = self.tscheme.dtcoef 108 | if self.forcing: 109 | assert hasattr(self, 'forc'), self.msg_forcing 110 | self.forc.add_forcing(x, t, dxdt, coef=coef) 111 | if self.diffusion: 112 | self.ope.rhs_diffusion(x, t, dxdt, coef=coef) 113 | 114 | self.ope.invert_vorticity(dxdt, flag='fast') 115 | 116 | def add_noslip(self, x): 117 | self.ope.rhs_noslip(x, self.source) 118 | self.ope.invert_vorticity(x, flag='fast', island=self.isisland) 119 | 120 | def set_psi_from_vorticity(self): 121 | self.ope.invert_vorticity(self.var.state, island=self.isisland) 122 | 123 | def diagnostics(self, var, t): 124 | """ should provide at least 'maxspeed' (for cfl determination) """ 125 | 126 | nh = self.nh 127 | u = var.get('u') 128 | v = var.get('v') 129 | vort = var.get('vorticity') 130 | buoy = var.get('buoyancy') 131 | 132 | ke, maxu = fd.computekemaxu(self.msk, u, v, self.nh) 133 | 134 | z, z2 = fd.computesumandnorm(self.msk, vort, self.nh) 135 | 136 | b, b2 = fd.computesumandnorm(self.msk, buoy, self.nh) 137 | 138 | # potential energy (minus sign because buoyancy is minus density) 139 | pe = - self.gravity * fd.computesum(self.msk, buoy*self.yr, nh) 140 | 141 | cst = self.mpitools.local_to_global([(maxu, 'max'), (ke, 'sum'), 142 | (z, 'sum'), (z2, 'sum'), 143 | (pe, 'sum'), (b, 'sum'), 144 | (b2, 'sum')]) 145 | 146 | self.diags['maxspeed'] = cst[0] 147 | self.diags['ke'] = cst[1] / self.area 148 | self.diags['pe'] = cst[4] / self.area 149 | self.diags['energy'] = (cst[1]+cst[4]) / self.area 150 | self.diags['vorticity'] = cst[2] / self.area 151 | self.diags['enstrophy'] = 0.5*cst[3] / self.area 152 | self.diags['buoyancy'] = cst[5] / self.area 153 | self.diags['brms'] = np.sqrt(cst[6] / self.area-(cst[5]/self.area)**2) 154 | -------------------------------------------------------------------------------- /core/boussinesqTS.py: -------------------------------------------------------------------------------- 1 | from operators import Operators 2 | from variables import Var 3 | from timescheme import Timescheme 4 | from importlib import import_module 5 | import fortran_diag as fd 6 | import fortran_advection as fa 7 | import numpy as np 8 | 9 | 10 | class BoussinesqTS(object): 11 | """ Boussinesq model with temperature and salinity 12 | 13 | It provides the step(t,dt) function 14 | and 'var' containing the mode state 15 | """ 16 | 17 | def __init__(self, param, grid): 18 | 19 | self.list_param = ['forcing', 'noslip', 'timestepping', 20 | 'alphaT', 'betaS', 21 | 'diffusion', 'Kdiff', 'myrank', 22 | 'forcing_module', 'gravity', 'isisland', 23 | 'customized', 'custom_module', 'additional_tracer'] 24 | param.copy(self, self.list_param) 25 | 26 | # for potential energy 27 | self.list_param = ['xr', 'yr', 'nh', 'Lx', 'msk', 'area', 'mpitools'] 28 | grid.copy(self, self.list_param) 29 | 30 | # for variables 31 | param.varname_list = ['vorticity', 32 | 'psi', 'u', 'v', 'density', 'danom', 'T', 'S'] 33 | param.tracer_list = ['vorticity', 'T', 'S'] 34 | param.whosetspsi = ('vorticity') 35 | 36 | if hasattr(self, 'additional_tracer'): 37 | for k in range(len(self.additional_tracer)): 38 | trac = self.additional_tracer[k] 39 | param.varname_list.append(trac) 40 | param.tracer_list.append(trac) 41 | 42 | self.varname_list = param.varname_list 43 | 44 | param.sizevar = [grid.nyl, grid.nxl] 45 | self.var = Var(param) 46 | dref = self.var.get('density').copy() 47 | self.dref = dref 48 | self.source = np.zeros(param.sizevar) 49 | 50 | # for operators 51 | self.ope = Operators(param, grid) 52 | 53 | # for timescheme 54 | self.tscheme = Timescheme(param, self.var.state) 55 | self.tscheme.set(self.dynamics, self.timestepping) 56 | 57 | if self.forcing: 58 | if self.forcing_module == 'embedded': 59 | print('Warning: check that you have indeed added the forcing to the model') 60 | print('Right below the line : model = f2d.model') 61 | print('you should have the line: model.forc = Forcing(param, grid)') 62 | 63 | pass 64 | else: 65 | try: 66 | f = import_module(self.forcing_module) 67 | 68 | except ImportError: 69 | print('module %s for forcing cannot be found' 70 | % self.forcing_module) 71 | print('make sure file **%s.py** exists' % self.forcing_module) 72 | sys.exit(0) 73 | 74 | self.forc = f.Forcing(param, grid) 75 | 76 | self.diags = {} 77 | 78 | if self.customized: 79 | try: 80 | f = import_module(self.custom_module) 81 | print(f) 82 | self.extrastep = f.Step(param, grid) 83 | except ImportError: 84 | print('module %s for forcing cannot be found' 85 | % self.custom_module) 86 | print('make sure file **%s.py** exists' % self.custom_module) 87 | exit(0) 88 | 89 | def step(self, t, dt): 90 | 91 | # 1/ integrate advection 92 | self.tscheme.forward(self.var.state, t, dt) 93 | self.set_density() 94 | 95 | # 2/ integrate source 96 | if self.noslip: 97 | self.add_noslip(self.var.state) 98 | 99 | if self.customized: 100 | self.extrastep.do(self.var, t, dt) 101 | 102 | danom = self.var.get('danom') 103 | danom[:, :] = self.var.get('density')-self.dref 104 | 105 | def dynamics(self, x, t, dxdt): 106 | self.ope.rhs_adv(x, t, dxdt) 107 | self.eos(dxdt) 108 | # db/dx is a source term for the vorticity 109 | self.ope.rhs_torque_density(x, t, dxdt) 110 | if (self.tscheme.kstage == self.tscheme.kforcing): 111 | coef = self.tscheme.dtcoef 112 | if self.forcing: 113 | self.forc.add_forcing(x, t, dxdt, coef=coef) 114 | if self.diffusion: 115 | self.ope.rhs_diffusion(x, t, dxdt, coef=coef) 116 | self.eos(dxdt) 117 | self.ope.invert_vorticity(dxdt, flag='fast') 118 | 119 | def add_noslip(self, x): 120 | self.ope.rhs_noslip(x, self.source) 121 | self.ope.invert_vorticity(x, flag='fast', island=self.isisland) 122 | 123 | def eos(self, x): 124 | """ compute density from T and S """ 125 | idens = self.varname_list.index('density') 126 | itemp = self.varname_list.index('T') 127 | isalt = self.varname_list.index('S') 128 | 129 | x[idens] = -self.alphaT*x[itemp] + self.betaS*x[isalt] 130 | 131 | def set_density(self): 132 | self.eos(self.var.state) 133 | 134 | def set_psi_from_vorticity(self): 135 | self.ope.invert_vorticity(self.var.state, island=self.isisland) 136 | 137 | def diagnostics(self, var, t): 138 | """ should provide at least 'maxspeed' (for cfl determination) """ 139 | 140 | nh = self.nh 141 | u = var.get('u') 142 | v = var.get('v') 143 | vort = var.get('vorticity') 144 | dens = var.get('density') 145 | 146 | ke, maxu = fd.computekemaxu(self.msk, u, v, self.nh) 147 | 148 | z, z2 = fd.computesumandnorm(self.msk, vort, self.nh) 149 | 150 | b, b2 = fd.computesumandnorm(self.msk, dens, self.nh) 151 | 152 | # potential energy 153 | pe = + self.gravity * fd.computesum(self.msk, dens*self.yr, nh) 154 | 155 | cst = self.mpitools.local_to_global([(maxu, 'max'), (ke, 'sum'), 156 | (z, 'sum'), (z2, 'sum'), 157 | (pe, 'sum'), (b, 'sum'), 158 | (b2, 'sum')]) 159 | 160 | self.diags['maxspeed'] = cst[0] 161 | self.diags['ke'] = cst[1] / self.area 162 | self.diags['pe'] = cst[4] / self.area 163 | self.diags['energy'] = (cst[1]+cst[4]) / self.area 164 | self.diags['vorticity'] = cst[2] / self.area 165 | self.diags['enstrophy'] = 0.5*cst[3] / self.area 166 | self.diags['density'] = cst[5] / self.area 167 | self.diags['drms'] = np.sqrt(cst[6] / self.area-(cst[5]/self.area)**2) 168 | -------------------------------------------------------------------------------- /core/diagnostics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import fortran_diag as fd 3 | 4 | 5 | class Diag(object): 6 | def __init__(self, param, grid): 7 | self.list_param = ['npx', 'npy', 'mpi', 'list_diag'] 8 | param.copy(self, self.list_param) 9 | 10 | self.list_grid = ['msk', 'nh', 'area'] 11 | grid.copy(self, self.list_grid) 12 | 13 | if self.mpi == 1: 14 | from mpi4py import MPI 15 | 16 | def integrals(self, state): 17 | u = state.get('u') 18 | v = state.get('v') 19 | vort = state.get('vorticity') 20 | 21 | # u2,umax = computenormmaxu(self.msk,u,self.nh) 22 | # v2,vmax = computenormmaxv(self.msk,v,self.nh) 23 | 24 | ke, maxu = fd.computekemaxu(self.msk, u, v, self.nh) 25 | # ke = computekewithpsi(self.msk,psi,vort,self.nh) 26 | 27 | 28 | z, z2 = fd.computesumandnorm(self.msk, vort, self.nh) 29 | 30 | # u2 = u2 / self.area 31 | # v2 = v2 / self.area 32 | ke = ke / self.area 33 | z = z / self.area 34 | z2 = z2 / self.area 35 | 36 | if self.mpi == -1: # TODO 37 | cst = np.zeros((6,)) 38 | cst[0] = u2 39 | cst[1] = v2 40 | cst[2] = z 41 | cst[3] = z2 42 | cst[4] = umax 43 | cst[5] = vmax 44 | 45 | cst_glo = np.array(MPI.COMM_WORLD.allgather(cst)) 46 | 47 | u2, v2, z, z2, umax, vmax = ( 48 | 0., 0., 0., 0., 0., 0.) 49 | 50 | for k in range(self.npx*self.npy): 51 | u2 += cst_glo[k][0] 52 | v2 += cst_glo[k][1] 53 | z += cst_glo[k][2] 54 | z2 += cst_glo[k][3] 55 | umax = max(umax, cst_glo[k][4]) 56 | vmax = max(vmax, cst_glo[k][5]) 57 | 58 | # todo: add potential energy for the Boussinesq model 59 | self.ke = ke # 0.5*(u2+v2) 60 | self.vorticity = z 61 | self.enstrophy = 0.5*z2 62 | # self.umax = umax 63 | # self.vmax = vmax 64 | self.maxspeed = maxu # max([umax,vmax]) 65 | 66 | def forwardbackward(self, t, dt): 67 | """advect by one time step then reverse time and integrate back to 68 | start point. The difference between the two states is due to 69 | the irreversibility of the flow """ 70 | # WORK IN PROGRESS 71 | 72 | # backup the initial state 73 | x = self.var.state.copy() 74 | 75 | self.step(t, dt) 76 | self.step(t+dt, -dt) 77 | 78 | return self.var.state-x 79 | -------------------------------------------------------------------------------- /core/fourier.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def set_x_and_k(n, L): 4 | k = ((n//2+np.arange(n)) % n) - n//2 5 | return (np.arange(n)+0.5)*L/n, 2*np.pi*k/L 6 | 7 | class Fourier(object): 8 | def __init__(self, param, grid): 9 | dx =grid.dx 10 | dy = grid.dy 11 | self.nx = param.nx 12 | self.ny = param.ny 13 | self.Lx = param.Lx 14 | self.Ly = param.Ly 15 | self.nh = param.nh 16 | 17 | self.x, self.kx = set_x_and_k(self.nx, self.Lx) 18 | self.y, self.ky = set_x_and_k(self.ny, self.Ly) 19 | 20 | self.xx, self.yy = np.meshgrid(self.x, self.y) 21 | self.kxx, self.kyy = np.meshgrid(self.kx, self.ky) 22 | self.ktot = np.sqrt(self.kxx**2+self.kyy**2) 23 | self.ktot[0,0] = 1. # <- to avoid division by zero 24 | # shift in Fourier space to move 25 | # psi from cell centers to cell corners 26 | shift = np.exp(1j*(self.kxx*dx*0.5+self.kyy*dy*0.5)) 27 | self.pv2psi = -(1/self.ktot)*shift 28 | self.pv2vor = self.ktot 29 | self.pv2psi[0,0] = 0. 30 | self.ktot[0,0] = 0. 31 | 32 | def invert(self, pv, psi, vor): 33 | nh = self.nh 34 | hpv = np.fft.fft2(pv[nh:-nh,nh:-nh]) 35 | hpsi = hpv*self.pv2psi 36 | hvor = hpv*self.pv2vor 37 | psi[nh:-nh,nh:-nh] = np.real(np.fft.ifft2(hpsi)) 38 | # Take the opportunity to compute the vorticity 39 | # 40 | # Because vorticity is a diagnostic variable 41 | # it should be computed only for output purpose 42 | # but in this version, I found it easier to 43 | # compute the vorticity at all calls 44 | vor[nh:-nh,nh:-nh] = np.real(np.fft.ifft2(hvor)) 45 | -------------------------------------------------------------------------------- /core/gmg/Makefile: -------------------------------------------------------------------------------- 1 | %.so : %.f90 ; $(F2PY) --quiet -m $* -c $< --opt='-O3 -fPIC -Wno-unused-variable -Wno-maybe-uninitialized -Wno-unused-dummy-argument -Wno-tabs -Wno-cpp -Wno-unused-function' 2 | 3 | 4 | all: fortran_multigrid.so 5 | -------------------------------------------------------------------------------- /core/gmg/__init__.py: -------------------------------------------------------------------------------- 1 | from gmg.fortran_multigrid import * 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /core/gmg/subdomains.py: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # 3 | # functions relative to subdomains 4 | # 5 | ############################################################ 6 | try: 7 | from mpi4py import MPI 8 | #print('- mpi4py : found') 9 | except: 10 | print('- mpi4py : not found, please install it') 11 | exit(0) 12 | 13 | from numpy import zeros,arange,ones,cumsum,sqrt,array,meshgrid 14 | #from gmg.fortran_multigrid import buffertodomain 15 | import gmg.fortran_multigrid as fortmg 16 | from time import time 17 | #from plotutils import plot2d 18 | 19 | def set_family(myrank,np,mp,ix,iy): 20 | procs=arange(np*mp) 21 | 22 | i = procs%np 23 | j = procs//np 24 | 25 | col=((i//ix)%2)+2*((j//iy)%2) 26 | 27 | if col[myrank]==0: 28 | rank0=myrank 29 | if col[myrank]==1: 30 | rank0=myrank-ix 31 | if col[myrank]==2: 32 | rank0=myrank-iy*np 33 | if col[myrank]==3: 34 | rank0=myrank-ix-iy*np 35 | 36 | # print(col[myrank]) 37 | 38 | 39 | if (ix= 0.5] = 0 100 | 101 | self.msknoslip = self.msk.copy() 102 | self.finalize_msk() 103 | 104 | def finalize_msk(self): 105 | """ Define grid quantities that depend on the mask 106 | 107 | should be after the mask array has been touched""" 108 | msk = self.msk 109 | #self.msknoslip = self.msk.copy()-self.msknoslip 110 | 111 | self.area = self.domain_integration(msk) 112 | 113 | # compute r2 for angular momentum 114 | x0 = self.domain_integration(self.xr*msk) / self.area 115 | y0 = self.domain_integration(self.yr*msk) / self.area 116 | 117 | self.xr0 = (self.xr - x0)*msk 118 | self.yr0 = (self.yr - y0)*msk 119 | 120 | x2 = self.domain_integration((self.xr0)**2*msk) / self.area 121 | y2 = self.domain_integration((self.yr0)**2*msk) / self.area 122 | self.x0 = x0 123 | self.y0 = y0 124 | self.x2 = x2 125 | self.y2 = y2 126 | if (self.myrank == 0) and (self.debug): 127 | print('domain barycenter is at (x0,y0)=(%g,%g)' % (x0, y0)) 128 | print('domain has %i interior points' % self.area) 129 | print('ratio fluid/solid is %g ' % (self.area/self.nx/self.ny)) 130 | self.r2 = self.xr0**2 + self.yr0**2 131 | 132 | def domain_integration(self, z2d): 133 | """Define the domain integral function on grid cells""" 134 | 135 | nh = self.nh 136 | integral = np.sum(z2d[nh:-nh, nh:-nh])*1. 137 | 138 | integral = self.mpitools.local_to_global([(integral, 'sum')]) 139 | 140 | return integral 141 | 142 | 143 | if __name__ == "__main__": 144 | 145 | param = Param() 146 | param.myrank = 0 147 | param.npx = 2 148 | param.npy = 2 149 | param.nx = 10 150 | param.ny = 10 151 | param.geometry = 'closed' 152 | grid = Grid(param) 153 | 154 | print("myrank is :", param.myrank) 155 | print(grid.xr[0, :]) 156 | print(grid.yr[:, 0]) 157 | print(grid.msk) 158 | print('my coordinates in the subdomains matrix (%i,%i)' 159 | % (grid.j0, grid.i0)) 160 | print('global domain area =%f' % grid.area) 161 | print('global boundary perimeter =%f' % grid.bcarea) 162 | -------------------------------------------------------------------------------- /core/island.py: -------------------------------------------------------------------------------- 1 | from numpy import ones, arange, meshgrid, sum, int8, sqrt, roll, zeros 2 | from param import Param 3 | from fortran_operators import celltocorner 4 | 5 | 6 | class Island(Param): 7 | def __init__(self, param, grid): 8 | 9 | self.list_param = ['nxl', 'nyl', 'dx', 'dy'] 10 | grid.copy(self, self.list_param) 11 | 12 | self.rhsp = zeros((self.nyl, self.nxl)) 13 | self.psi = zeros((self.nyl, self.nxl)) 14 | self.nbisland = 0 15 | self.data = [] 16 | 17 | def add(self, idx, psi0): 18 | self.data.append({'idx': idx, 'psi0': psi0}) 19 | self.nbisland += 1 20 | 21 | def finalize(self, mskp_model): 22 | print('found %i islands' % self.nbisland) 23 | mskp = zeros((self.nyl, self.nxl), dtype=int8) 24 | work = zeros((self.nyl, self.nxl)) 25 | mskr = zeros((self.nyl, self.nxl)) 26 | for k in range(self.nbisland): 27 | idx = self.data[k]['idx'] 28 | psi0 = self.data[k]['psi0'] 29 | mskr[:, :] = 1. 30 | mskp[:, :] = 0 31 | mskr[idx] = 0. 32 | celltocorner(mskr, work) 33 | mskp[work == 1] = 1 34 | mskp = 1-mskp 35 | 36 | vort = (roll(mskp, -1, axis=1)+roll(mskp, -1, axis=0) 37 | + roll(mskp, +1, axis=1)+roll(mskp, +1, axis=0)) 38 | 39 | z = (vort)*psi0/(self.dx*self.dy) # *(1-mskp) 40 | self.rhsp[vort > 0] = z[vort > 0] 41 | self.psi[mskp == 1] = psi0 42 | # print(self.psi[:,10]) 43 | print('island are ok') 44 | -------------------------------------------------------------------------------- /core/mpitools.py: -------------------------------------------------------------------------------- 1 | from param import Param 2 | import numpy as np 3 | try: 4 | from mpi4py import MPI 5 | except: 6 | MPI = 0 7 | 8 | 9 | class Mpitools(object): 10 | def __init__(self, param): 11 | self.list_param = ['npx', 'npy', 'myrank'] 12 | param.copy(self, self.list_param) 13 | 14 | self.nbproc = self.npx * self.npy 15 | 16 | def local_to_global(self, list_scalars): 17 | """ dict_scalars is a list of tuples 18 | [ (value,'sum'), (value,'max') ...] """ 19 | 20 | nb = len(list_scalars) 21 | cst = np.zeros((nb,)) 22 | 23 | for k in range(nb): 24 | cst[k] = list_scalars[k][0] 25 | 26 | if self.nbproc > 1: 27 | if type(MPI) == int: 28 | cst_glo = np.zeros((nb, 1)) 29 | cst_glo[:, 0] = cst 30 | else: 31 | cst_glo = np.array(MPI.COMM_WORLD.allgather(cst)) 32 | 33 | for k in range(nb): 34 | ope = list_scalars[k][1] 35 | if ope == 'max': 36 | cst[k] = np.max(cst_glo[:, k]) 37 | elif ope == 'sum': 38 | cst[k] = np.sum(cst_glo[:, k]) 39 | 40 | return cst 41 | 42 | 43 | if __name__ == "__main__": 44 | 45 | p = Param() 46 | p.npx = 2 47 | p.npy = 2 48 | 49 | rank = MPI.COMM_WORLD.Get_rank() 50 | p.myrank = rank 51 | 52 | d = [(rank, 'max'), (1, 'sum'), (rank, 'sum')] 53 | 54 | tool = Mpitools(p) 55 | 56 | r = tool.local_to_global(d) 57 | 58 | if rank == 0: 59 | print('max(rank)=', r[0]) 60 | print('sum(1) =', r[1]) 61 | print('sum(rank)=', r[2]) 62 | -------------------------------------------------------------------------------- /core/param.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path as path 3 | import sys 4 | import getopt 5 | 6 | 7 | class Param(object): 8 | """class to set up the default parameters value of the model 9 | 10 | the default parameters are stored in a json file along with 11 | their definition 12 | 13 | launch the code with the -h to get the help on all the model 14 | parameters, e.g. 15 | 16 | python vortex.py -h 17 | 18 | """ 19 | 20 | def __init__(self, defaultfile): 21 | """defaultfile is a sequel, it's no longer used the default file is 22 | systematically the defaults.json located in the fluid2d/core 23 | 24 | """ 25 | 26 | import grid 27 | d = path.dirname(grid.__file__) 28 | jasonfile = d+'/defaults.json' 29 | 30 | with open(jasonfile) as f: 31 | namelist = json.load(f) 32 | 33 | self.set_parameters(namelist) 34 | 35 | opts, args = getopt.getopt(str(sys.argv[1:]), 'h:v', ['']) 36 | if '-h' in args: 37 | self.manall() 38 | sys.exit() 39 | 40 | if '-v' in args: 41 | self.print_param = True 42 | else: 43 | self.print_param = False 44 | 45 | def set_parameters(self, namelist): 46 | avail = {} 47 | doc = {} 48 | for d in namelist.keys(): 49 | dd = namelist[d] 50 | for name in dd.keys(): 51 | val = dd[name]['default'] 52 | # print(name, val) 53 | setattr(self, name, val) 54 | if 'avail' in dd[name]: 55 | avail[name] = dd[name]['avail'] 56 | if 'doc' in dd[name]: 57 | doc[name] = dd[name]['doc'] 58 | self.avail = avail 59 | self.doc = doc 60 | 61 | def man(self, name): 62 | if name in self.doc: 63 | helpstr = self.doc[name] 64 | if name in self.avail: 65 | availstr = ', '.join([str(l) for l in self.avail[name]]) 66 | helpstr += ' / available values = ['+availstr+']' 67 | else: 68 | helpstr = 'no manual for this parameter' 69 | 70 | name = '\033[0;32;40m' + name + '\033[0m' 71 | print(' - "%s" : %s\n' % (name, helpstr)) 72 | 73 | def manall(self): 74 | ps = self.listall() 75 | for p in ps: 76 | self.man(p) 77 | 78 | def checkall(self): 79 | for p, avail in self.avail.items(): 80 | if getattr(self, p) in avail: 81 | # the parameter 'p' is well set 82 | pass 83 | else: 84 | msg = 'parameter "%s" should in ' % p 85 | msg += str(avail) 86 | raise ValueError(msg) 87 | 88 | def listall(self): 89 | """ return the list of all the parameters""" 90 | ps = [d for d in self.__dict__ if not(d in ['avail', 'doc'])] 91 | return ps 92 | 93 | def printvalues(self): 94 | """ print the value of each parameter""" 95 | for d in self.__dict__.keys(): 96 | if not(d in ['avail', 'doc']): 97 | print('%20s :' % d, getattr(self, d)) 98 | 99 | def copy(self, obj, list_param): 100 | """ copy attributes listed in list_param to obj 101 | 102 | On output it returns missing attributes 103 | """ 104 | missing = [] 105 | for k in list_param: 106 | if hasattr(self, k): 107 | setattr(obj, k, getattr(self, k)) 108 | else: 109 | missing.append(k) 110 | return missing 111 | 112 | 113 | if __name__ == "__main__": 114 | param = Param('default.xml') 115 | print('liste of parameters') 116 | print(param.listall()) 117 | 118 | # to have the documentation on one particular parameter 119 | param.man('beta') 120 | 121 | # to get the documentation on all the parameters 122 | param.manall() 123 | 124 | # to check that all parameters that should a value taken from a list 125 | # have an acceptable value 126 | param.checkall() 127 | -------------------------------------------------------------------------------- /core/restart.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from netCDF4 import Dataset 3 | import glob 4 | 5 | 6 | class Restart(object): 7 | """Class to manage the execution of Fluid2d with restarts 8 | 9 | In the user script, instead of calling f2d.loop() use 10 | 11 | Restart(param, grid, f2d) 12 | 13 | => add 'from restart import Restart' in the header to load the class 14 | 15 | If no restart exists, the code will start from scratch, otherwise 16 | the code will start from the last restart found. Note that 17 | 'param.tend' is interpreted as the 'time of integration' of the 18 | run, not the 'final time' of the run. 19 | 20 | restart files are overwritten during a job 21 | 22 | Works with mpi and several subdomains. 23 | 24 | Each 'his', 'diag', and 'restart' file have the restart index 25 | added in their name, e.g. 26 | 27 | vortex_00_his_000.nc : is the 'his' of rank 000 during job 0 (first) 28 | vortex_01_his_000.nc : is the 'his' of rank 000 during job 1 (second) 29 | ... 30 | 31 | vortex_00_restart_000.nc : is the restart created at the end of 32 | job 0, to start job 1. 33 | ... 34 | 35 | output.txt is over-written by each job 36 | """ 37 | 38 | def __init__(self, param, grid, f2d, launch=True): 39 | self.list_param = ['expname', 'expdir', 'nbproc', 'myrank', 40 | 'tend', 'varname_list', 'ninterrestart', 'diag_fluxes'] 41 | param.copy(self, self.list_param) 42 | 43 | self.list_grid = ['nh', 'nxl', 'nyl'] 44 | grid.copy(self, self.list_grid) 45 | 46 | self.template = self.expdir + '/%s_%02i_restart_%03i.nc' 47 | 48 | self.get_lastrestart() 49 | 50 | self.timelength = f2d.tend 51 | 52 | if self.myrank == 0: 53 | print('-'*50) 54 | if self.lastrestart is not None: 55 | self.restart_file = self.template % ( 56 | self.expname, self.lastrestart, self.myrank) 57 | if self.myrank == 0: 58 | print(' Restart found') 59 | print(' Restarting from %s' % self.restart_file) 60 | tend, t, dt, kt, tnextdiag, tnexthis = self.read(f2d.model.var) 61 | f2d.tend += round(t) 62 | f2d.t = t 63 | f2d.dt = dt 64 | f2d.kt = kt 65 | f2d.output.tnextdiag = tnextdiag 66 | f2d.output.tnexthis = tnexthis 67 | 68 | self.nextrestart = self.lastrestart+1 69 | 70 | else: 71 | if self.myrank == 0: 72 | print(' No restart') 73 | print(' Starting from scratch') 74 | self.nextrestart = 0 75 | 76 | f2d.output.diagfile = self.expdir + '/%s_%02i_diag.nc' % ( 77 | self.expname, self.nextrestart) 78 | f2d.output.template = self.expdir +'/%s_%02i_his' % ( 79 | self.expname, self.nextrestart)+'_%03i.nc' 80 | f2d.output.hisfile = f2d.output.template % (self.myrank) 81 | f2d.output.hisfile_joined = self.expdir + '/%s_%02i_his.nc' % ( 82 | self.expname, self.nextrestart) 83 | if self.diag_fluxes: 84 | f2d.output.template = self.expdir +'/%s_%02i_flx' % ( 85 | self.expname, self.nextrestart)+'_%03i.nc' 86 | f2d.output.flxfile = f2d.output.template % (self.myrank) 87 | f2d.output.flxfile_joined = self.expdir + '/%s_%02i_flx.nc' % ( 88 | self.expname, self.nextrestart) 89 | 90 | 91 | 92 | # split the integration in 'ninterrestart' intervals and 93 | # save a restart at the end of each 94 | 95 | self.lengthsubint = self.timelength/self.ninterrestart 96 | 97 | if launch: 98 | self.launchf2d(f2d) 99 | 100 | def launchf2d(self, f2d): 101 | """launch fluid2d and write a restart at the end 102 | 103 | if ninterrestart>1 do it several times. This is safer if you 104 | launch a long job on a cluster. If you submit a ten hours job, 105 | you may want to have intermediary restart points in case 106 | things go wrong. 107 | 108 | """ 109 | for kres in range(self.ninterrestart): 110 | f2d.tend = f2d.t + self.lengthsubint 111 | f2d.loop(joinhis=(kres == self.ninterrestart-1), 112 | keepplotalive=(kres < self.ninterrestart-1)) 113 | self.restart_file = self.template % ( 114 | self.expname, self.nextrestart, self.myrank) 115 | if self.myrank == 0: 116 | print('writing restart %i in %s' % (kres, self.restart_file)) 117 | tend = f2d.tend 118 | t = f2d.t 119 | dt = f2d.dt 120 | kt = f2d.kt 121 | tnextdiag = f2d.output.tnextdiag 122 | tnexthis = f2d.output.tnexthis 123 | self.write(tend, t, dt, kt, tnextdiag, tnexthis, f2d.model.var) 124 | 125 | def get_lastrestart(self): 126 | """ determine the index of the last restart""" 127 | files = glob.glob(self.expdir + '/%s_*_restart_000.nc' % self.expname) 128 | nbrestart = len(files) 129 | if nbrestart == 0: 130 | self.lastrestart = None 131 | else: 132 | k = 0 133 | for f in files: 134 | pos = f.find('restart') 135 | idx = int(f[pos-3:pos-1]) 136 | if idx > k: 137 | k = idx 138 | self.lastrestart = k 139 | 140 | def write(self, tend, t, dt, kt, tnextdiag, tnexthis, var): 141 | """ write the restart file""" 142 | nc = Dataset(self.restart_file, 'w') 143 | 144 | nc.setncattr('tend', tend) 145 | nc.setncattr('t', t) 146 | nc.setncattr('dt', dt) 147 | nc.setncattr('kt', kt) 148 | nc.setncattr('tnextdiag', tnextdiag) 149 | nc.setncattr('tnexthis', tnexthis) 150 | 151 | nc.createDimension('x', self.nxl) 152 | nc.createDimension('y', self.nyl) 153 | 154 | for v in self.varname_list: 155 | nc.createVariable(v, 'd', ('y', 'x')) # save in double precision 156 | z2d = var.get(v) 157 | nc.variables[v][:, :] = z2d[:, :] 158 | 159 | nc.close() 160 | 161 | def read(self, var): 162 | """ read the restart file""" 163 | nc = Dataset(self.restart_file, 'r') 164 | 165 | tend = nc.getncattr('tend') 166 | t = nc.getncattr('t') 167 | dt = nc.getncattr('dt') 168 | kt = nc.getncattr('kt') 169 | tnextdiag = nc.getncattr('tnextdiag') 170 | tnexthis = nc.getncattr('tnexthis') 171 | 172 | for v in self.varname_list: 173 | z2d = var.get(v) 174 | z2d[:, :] = nc.variables[v][:, :] 175 | 176 | nc.close() 177 | return tend, t, dt, kt, tnextdiag, tnexthis 178 | -------------------------------------------------------------------------------- /core/test_euler_log_jobs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvthinker/Fluid2d/75289b2bb1594b52c9e65a16e2800ae7f5b868aa/core/test_euler_log_jobs -------------------------------------------------------------------------------- /core/thermalwind.py: -------------------------------------------------------------------------------- 1 | from param import Param 2 | from operators import Operators 3 | from variables import Var 4 | from timescheme import Timescheme 5 | from importlib import import_module 6 | import fortran_diag as fd 7 | from numpy import sqrt, sum, zeros, tanh 8 | 9 | 10 | class Thermalwind(Param): 11 | """ Thermal wind model 12 | 13 | It provides the step(t,dt) function 14 | and 'var' containing the mode state 15 | """ 16 | 17 | def __init__(self, param, grid): 18 | 19 | self.list_param = ['forcing', 'noslip', 'timestepping', 20 | 'forcing_module', 'additional_tracer', 21 | 'myrank', 22 | 'gravity', 'diffusion', 'Kdiff', 'f0'] 23 | param.copy(self, self.list_param) 24 | 25 | # for potential energy 26 | self.list_param = ['xr', 'yr', 'nh', 27 | 'Lx', 'msk', 'area', 'mpitools', 'dx'] 28 | grid.copy(self, self.list_param) 29 | 30 | # for variables 31 | param.varname_list = ['vorticity', 'psi', 32 | 'u', 'v', 'buoyancy', 'V', 'qE'] 33 | param.tracer_list = ['vorticity', 'buoyancy', 'V'] 34 | param.whosetspsi = ('vorticity') 35 | 36 | if hasattr(self, 'additional_tracer'): 37 | for k in range(len(self.additional_tracer)): 38 | trac = self.additional_tracer[k] 39 | param.varname_list.append(trac) 40 | param.tracer_list.append(trac) 41 | 42 | param.sizevar = [grid.nyl, grid.nxl] 43 | self.var = Var(param) 44 | 45 | # for operators 46 | self.ope = Operators(param, grid) 47 | 48 | # for timescheme 49 | self.tscheme = Timescheme(param, self.var.state) 50 | 51 | if self.forcing: 52 | if self.forcing_module == 'embedded': 53 | print('Warning: check that you have indeed added the forcing to the model') 54 | print('Right below the line : model = f2d.model') 55 | print('you should have the line: model.forc = Forcing(param, grid)') 56 | 57 | pass 58 | else: 59 | try: 60 | f = import_module(self.forcing_module) 61 | 62 | except ImportError: 63 | print('module %s for forcing cannot be found' 64 | % self.forcing_module) 65 | print('make sure file **%s.py** exists' % self.forcing_module) 66 | sys.exit(0) 67 | 68 | self.forc = f.Forcing(param, grid) 69 | 70 | 71 | self.diags = {} 72 | 73 | def step(self, t, dt): 74 | 75 | # 1/ integrate advection 76 | self.tscheme.set(self.dynamics, self.timestepping) 77 | self.tscheme.forward(self.var.state, t, dt) 78 | 79 | # 2/ integrate source 80 | if self.noslip: 81 | self.add_noslip(self.var.state) 82 | 83 | self.set_psi_from_vorticity() 84 | self.compute_pv() 85 | 86 | def compute_pv(self): 87 | qE = self.var.get('qE') 88 | v = self.var.get('V') 89 | b = self.var.get('buoyancy') 90 | 91 | # Ertel PV 92 | y = qE*0. 93 | y[1:-1, 1:-1] = self.ope.jacobian(v+self.f0*self.xr, b) 94 | y *= self.msk 95 | self.ope.fill_halo(y) 96 | qE[:, :] = y 97 | 98 | def dynamics(self, x, t, dxdt): 99 | self.ope.rhs_adv(x, t, dxdt) 100 | self.ope.rhs_thermalwind(x, t, dxdt) # add the r.h.s. terms 101 | 102 | if (self.tscheme.kstage == self.tscheme.kforcing): 103 | if self.forcing: 104 | self.forc.add_forcing(x, t, dxdt) 105 | 106 | if self.diffusion: 107 | self.ope.rhs_diffusion(x, t, dxdt) 108 | 109 | else: 110 | self.ope.invert_vorticity(dxdt, flag='fast') 111 | 112 | def sources(self, x, t, dxdt): 113 | if self.noslip: 114 | self.ope.rhs_noslip(x, t, dxdt) 115 | #self.ope.invert_vorticity(x, flag='full') 116 | 117 | def set_psi_from_vorticity(self): 118 | self.ope.invert_vorticity(self.var.state) 119 | 120 | def diagnostics(self, var, t): 121 | """ should provide at least 'maxspeed' (for cfl determination) """ 122 | 123 | nh = self.nh 124 | u = var.get('u') 125 | v = var.get('v') 126 | vort = var.get('vorticity') 127 | buoy = var.get('buoyancy') 128 | V = var.get('V') 129 | qE = var.get('qE') 130 | qEneg = qE.copy() 131 | qEneg[qE>0] = 0. 132 | 133 | ke, maxu = fd.computekemaxu(self.msk, u, v, self.nh) 134 | 135 | z, z2 = fd.computesumandnorm(self.msk, vort, self.nh) 136 | 137 | b, b2 = fd.computesumandnorm(self.msk, buoy, self.nh) 138 | 139 | vm, v2 = fd.computesumandnorm(self.msk, V, self.nh) 140 | 141 | q, q2 = fd.computesumandnorm(self.msk, qE, self.nh) 142 | qn, qn2 = fd.computesumandnorm(self.msk, qEneg, self.nh) 143 | 144 | # potential energy (minus sign because buoyancy is minus density) 145 | pe = fd.computesum(self.msk, buoy*self.yr, nh) 146 | pe = - self.gravity*pe 147 | 148 | cst = self.mpitools.local_to_global([ 149 | (maxu, 'max'), (ke, 'sum'), 150 | (z, 'sum'), (z2, 'sum'), 151 | (pe, 'sum'), (b, 'sum'), 152 | (b2, 'sum'), (q, 'sum'), 153 | (q2, 'sum'), (qn, 'sum'), 154 | (qn2, 'sum'), (v2, 'sum')]) 155 | 156 | a = self.area 157 | self.diags['maxspeed'] = cst[0] 158 | self.diags['ke'] = (cst[1]) / a 159 | self.diags['keV'] = (0.5*cst[11])/a 160 | self.diags['pe'] = cst[4] / a 161 | self.diags['energy'] = self.diags['ke'] + self.diags['pe'] + self.diags['keV'] 162 | 163 | self.diags['vorticity'] = cst[2] / a 164 | self.diags['enstrophy'] = 0.5*cst[3] / a 165 | 166 | bm = cst[5] / a 167 | self.diags['buoyancy'] = bm 168 | self.diags['brms'] = sqrt(cst[6] / a - bm**2) 169 | 170 | pvm = cst[7] / a 171 | pvneg_mean = cst[9] / a 172 | self.diags['pv_mean'] = pvm 173 | self.diags['pv_std'] = sqrt(cst[8] / a - pvm**2) 174 | self.diags['pvneg_mean'] = pvneg_mean 175 | self.diags['pvneg_std'] = sqrt(cst[10] / a - pvneg_mean**2) 176 | -------------------------------------------------------------------------------- /core/timers.py: -------------------------------------------------------------------------------- 1 | from time import time as clock 2 | 3 | 4 | class Timers(object): 5 | def __init__(self, param): 6 | self.t0 = {} 7 | self.elapse = {} 8 | self.ncalls = {} 9 | 10 | def tic(self, name): 11 | if not(name in self.t0.keys()): 12 | self.elapse[name] = 0. 13 | self.ncalls[name] = 0. 14 | self.t0[name] = clock() 15 | 16 | def toc(self, name): 17 | dt = clock()-self.t0[name] 18 | self.elapse[name] += dt 19 | self.ncalls[name] += 1 20 | 21 | def _print(self): 22 | keys = self.elapse.keys() 23 | for key in sorted(keys): 24 | print('%10s : %6.2f s / %6i calls / %6.2e' % 25 | (key, self.elapse[key], self.ncalls[key], 26 | self.elapse[key]/self.ncalls[key])) 27 | -------------------------------------------------------------------------------- /core/variables.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from param import Param 3 | 4 | 5 | class Var(object): 6 | def __init__(self, param): 7 | """Define a model state variable 8 | 9 | for which all variables are packed together 10 | in a larger single array 11 | the var also has a time axis available """ 12 | 13 | self.list_param = ['sizevar', 'varname_list'] 14 | param.copy(self, self.list_param) 15 | 16 | self.nvar = len(self.varname_list) 17 | 18 | if type(self.sizevar) != list: 19 | print('sizevar has to be a list') 20 | print('Abort!!!') 21 | exit(0) 22 | 23 | self.sizestate = [self.nvar]+self.sizevar 24 | 25 | self.state = np.zeros(self.sizestate) 26 | 27 | def get(self, name): 28 | """ extract variable 'name' from the stack """ 29 | k = self.varname_list.index(name) 30 | return self.state[k] 31 | 32 | 33 | if __name__ == "__main__": 34 | 35 | param = Param() 36 | param.varname_list = ['vort', 'psi', 'u'] 37 | param.sizevar = [4, 2] 38 | 39 | var = Var(param) 40 | 41 | print(np.shape(var.state)) 42 | 43 | vor = var.get('vort') 44 | vor[:, 0] = 1. 45 | print(np.shape(vor)) 46 | 47 | print(var.state) 48 | -------------------------------------------------------------------------------- /docs/experiment_template.py: -------------------------------------------------------------------------------- 1 | # 2 | # TEMPLATE for a fluid2d experiment 3 | # 4 | # with explanations of parameters and list of choices, if should be the case 5 | # 6 | from param import Param 7 | from grid import Grid 8 | from fluid2d import Fluid2d 9 | from numpy import exp,sqrt,pi,cos,random,shape 10 | 11 | 12 | 13 | param = Param('default.xml') # the xml file contains all the defaults values + a doc on variable 14 | param.modelname = 'euler' # advection, euler, boussinesq, quasigeostrophic 15 | param.expname = 'myfirstexp' # string used to name the netcdf files: 16 | # expname_his.nc and expname_diag.nc 17 | 18 | 19 | 20 | # *** domain and resolution *** 21 | param.nx = 32*2 # nb of grid points, should of the form 2**p, 2**p*3 or 2**p*5 22 | param.ny = 32 # idem 23 | param.npy=4 # nb of processors in y: should be a power of 2 24 | param.Lx = 2. # domain size in x [Lx can be dimensional if the user 25 | # wishes] 26 | param.Ly = param.Lx/2 # domain size in y. The model imposes that dx=dy 27 | # => Lx/nx = Ly/ny 28 | 29 | param.geometry = 'closed' # closed, disc, xchannel, ychannel, perio. 30 | # For a more complex domain, adjust the grid.msk once grid is available 31 | 32 | 33 | 34 | # *** time *** 35 | param.tend=4e4 # time of integration [could be dimensional of not] 36 | param.adaptable_dt=True 37 | param.cfl=1. # desired cfl if adaptable_dt=True 38 | param.dt = 1. # desired dt if adaptable_dt=False 39 | param.dtmax=100. # max dt, used for accelerated flows or flows 40 | # dominated by waves, in these case, dtmax requires 41 | # a little bit of tuning 42 | 43 | 44 | 45 | # *** discretization *** 46 | param.order=5 # 1,2,3,4,5 : order of the spatial discretization. Even 47 | # cases are centered fluxes, odd cases are upwinded 48 | # fluxes 49 | param.timestepping='RK3_SSP'# LFAM3, Heun, EF, LF, RK3_SSP, AB2, 50 | # AB3. See core/timestepping.py to see the 51 | # list, you may add your own in there 52 | 53 | 54 | 55 | # *** output *** 56 | param.var_to_save=['pv','psi'] # variables to save in the history. The 57 | # name of variables depends on the 58 | # model 59 | param.list_diag=['ke','pv','pv2'] # diagnostics to save in the 60 | # diag. They are global 61 | # quantities. The name of the 62 | # diagnostics depends on the model 63 | 64 | param.freq_his=50 # time interval between two histories (same unit as tend and dt) 65 | param.freq_diag=10 # time interval between two diagnostics (same unit as tend and dt) 66 | 67 | 68 | 69 | # *** plot *** 70 | param.plot_var='pv' # variable to plot (depends on the model) 71 | param.freq_plot=10 # number of time step between two plots 72 | param.cax=[-1,1] # colorscale if colorscheme='imposed' 73 | param.plot_interactive=False # activate the interactive plot 74 | param.colorscheme='imposed' # 'imposed', 'minmax', 'symmetric' 75 | param.plotting_module='plotting_adv' # name of the plotting 76 | # script. The default one is 77 | # core/plotting.py 78 | 79 | 80 | 81 | # *** physics *** 82 | param.beta=1. # beta parameter (for QG model only) 83 | param.Rd=1. # Rossby deformation radius (for QG only) 84 | param.gravity=1. # for Boussinesq model 85 | param.forcing = True # activate the forcing 86 | param.forcing_module='forcing_dbl_gyre' #python module name where the forcing is defined 87 | param.noslip = False # active the no-slip boundary condition (causes energy dissipation) 88 | param.diffusion=False # activate a diffusion on tracer. 89 | param.additional_tracer=['tracer'] # you may add an additional passive 90 | # tracer, by simply adding this 91 | # line. Example in inverse_cascade.py 92 | 93 | 94 | grid = Grid(param) # define the grid coordinates grid.xr, grid.yr and 95 | # the mask, grid.msk. You may modify the mask just below 96 | 97 | param.Kdiff=0.5e-4*grid.dx # diffusion coefficient (same for all 98 | # tracers), there is a possibility to 99 | # assign a different value for each tracer, 100 | # see the experiment rayleigh_benard.py 101 | # watch out, Kdiff should be adjusted when 102 | # the resolution is changed 103 | 104 | 105 | 106 | 107 | 108 | f2d = Fluid2d(param,grid) # define everything 109 | model = f2d.model 110 | 111 | xr,yr = grid.xr,grid.yr 112 | vor = model.var.get('pv') # way to access the 2D array of a variable, 113 | # vor is 2 array 114 | 115 | 116 | # set an initial tracer field 117 | vor[:] = 1e-2*random.normal(size=shape(vor))*grid.msk 118 | y=vor[:]*1. 119 | model.ope.fill_halo(y) # each variable is surrounded with a halo. Halo 120 | # width is param.nh=3 grid points. The halo is 121 | # used in the periodic case (xchannel, 122 | # ychannel, perio) and when the model is used with MPI 123 | vor[:]=y 124 | 125 | model.add_backgroundpv() # for the QG model 126 | 127 | model.set_psi_from_pv() 128 | 129 | f2d.loop() # launch the model 130 | -------------------------------------------------------------------------------- /docs/fluid2d_schematic.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvthinker/Fluid2d/75289b2bb1594b52c9e65a16e2800ae7f5b868aa/docs/fluid2d_schematic.pdf -------------------------------------------------------------------------------- /docs/model_equations.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Model equations 3 | =============== 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | 9 | The choice of the model's equations is the first one to be made for an 10 | experiment. It is controlled by 11 | 12 | .. code:: 13 | 14 | param.modelname = 'euler' 15 | 16 | The common core of fluid2d's equations is a vorticity formulation 17 | allowed by the incompressibility assumption. The velocity is deduced 18 | from the streamfunction, itself deduced from the vorticity. The model 19 | thus integrates in time a scalar quantity, the vorticity, instead of a 20 | vector quantity, the velocity. Pressure is totally absent from both 21 | the equations and the code. Of course, pressure exists. For all the 22 | flows you can simulate there is always an underlying pressure 23 | field. But it has to be diagnosed and so far the diagnostic is not 24 | implemented (it was in an older version). A nice property of a 25 | vorticity formulation is that the Kelvin's theorem is enforced at every 26 | grid point and at all time. This is a very desirable property. 27 | 28 | 29 | Euler equations 30 | =============== 31 | 32 | This is the model for the dynamics of vorticity 33 | 34 | .. math:: 35 | 36 | \partial_t \omega + J(\psi,\omega) = 0 37 | 38 | \partial^2_{xx}\psi+ \partial^2_{yy} \psi = \omega 39 | 40 | where :math:`\omega` is the vorticity and :math:`\psi` is the 41 | stream-function. The boundary condition for the Poisson equation is 42 | :math:`\psi=0` along solid boundaries. The constant 0 might be replaced 43 | along each island by a given value :math:`\psi_k`. 44 | 45 | There is a possibility to add an explicit viscous term on the r.h.s., 46 | controlled by a diffusion parameter. 47 | 48 | This models allows to study the **dynamics of vorticity** in general from 49 | **interaction of isolated coherent structures** to **2D turbulence**. 50 | 51 | Transport equations 52 | =================== 53 | 54 | In this model the stream-function is frozen. Only passive a tracer :math:`\phi` is transported 55 | 56 | .. math:: 57 | 58 | \partial_t \phi + J(\psi,\phi) = 0 59 | 60 | This model is used to study the **properties of transport per se**. It can 61 | also be used to test the **numerical properties of advection 62 | schemes**. There is a practical on this question. 63 | 64 | Quasi-geostrophic equations 65 | =========================== 66 | 67 | This model is similar to the Euler one except that the Coriolis force is now included, yet hidden in the deformation radius, 68 | 69 | .. math:: 70 | 71 | \partial_t \omega + J(\psi,\omega) = 0 72 | 73 | \partial^2_{xx}\psi+ \partial^2_{yy} \psi -R^{-2}_d \psi - \beta y = \omega 74 | 75 | where :math:`\omega` is now the total potential vorticity (PV), :math:`R_d` is 76 | the Rossby deformation radius and :math:`\beta` is the beta parameter 77 | encoding the Earth's curvature. It is worth noting that quite often the quasi-geostrophic equations are written for the PV anomaly :math:`\omega'` such that 78 | 79 | .. math:: 80 | \omega = \omega' + \beta y 81 | 82 | The evolution equation for the PV anomaly involves the :math:`-\beta 83 | v` source term. To preserve a conservative form for PV, fluid2d uses 84 | the total PV. 85 | 86 | 87 | This model allows to study **wind-driven circulation, Rossby waves, 88 | turbulence on the beta plane** etc. 89 | 90 | Boussinesq equations 91 | ==================== 92 | 93 | This model describes the motion of a stratified fluid in a 2D vertical slice. The equations are the same as Euler, complemented by a transport equation for buoyancy :math:`b`. The vorticity equation is coupled to the buoyancy via the torque of the weight, the :math:`\partial_x b` term 94 | 95 | .. math:: 96 | 97 | \partial_t \omega + J(\psi,\omega) = \partial_x b 98 | 99 | \partial_t b + J(\psi,b) = 0 100 | 101 | \partial^2_{xx}\psi+ \partial^2_{yy} \psi = \omega 102 | 103 | This model allows to study stratified processes such as: **internal 104 | waves, convection, Kelvin-Helmholz instabilities, lee-waves** etc. 105 | 106 | Thermal wind equations 107 | ====================== 108 | 109 | This model describes the secondary (ageostrophic) circulation that develops around an axisymmetric jet. The equations couple the Boussinesq-like dynamics in the vertical plane to the jet speed 110 | 111 | .. math:: 112 | 113 | \partial_t \omega + J(\psi,\omega) = \partial_x b - f\partial_z v 114 | 115 | \partial_t v + J(\psi,v) = f \partial_z\psi 116 | 117 | \partial_t b + J(\psi,b) = 0 118 | 119 | \partial^2_{xx}\psi+ \partial^2_{yy} \psi = \omega 120 | 121 | where :math:`v` is the (horizontal) jet velocity and :math:`f` is the 122 | Coriolis parameter. In the absence of secondary circulation, 123 | i.e. :math:`\psi=0`, the buoyancy and the jet velocity are 124 | in thermal wind-balance (first equation). 125 | 126 | This model allows to study **symmetric instabilities, frontogenesis** 127 | etc. This model is not yet fully debuged. 128 | 129 | 130 | .. _viscous-interaction: 131 | Viscous Interaction 132 | =================== 133 | 134 | The no-slip boundary condition is handled by added a localized source 135 | term :math:`S` along the boundary. The source term is recomputed at each time 136 | step. It is adjusts such that the tangential velocity along the 137 | boundary vanishes. 138 | 139 | Last update: 12/10/2021 140 | -------------------------------------------------------------------------------- /docs/model_numerics.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Model numerics 3 | ============== 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | Spatial discretization 9 | ====================== 10 | 11 | The model is based on a C-grid type discretization. Variables are 12 | staggered. Variables obeying a transport equations are treated in 13 | finite volume form. Fluid2d tags them as *tracers*. They are 14 | discretized at cell centers. Fluid2d's key tracer is the 15 | vorticity. Velocity compontents are defined at cell edges. The 16 | streamfunction is defined at cell corners. Cells can be fluid or 17 | solid. The boundary goes along the cell edges and through cell 18 | corners. Imposing a constant streamfunction along the boundary is 19 | therefore straightforward. The no-flow boundary condition is enforced 20 | by construction, at no cost. 21 | 22 | .. figure:: staggering.png 23 | :align: center 24 | :scale: 70 25 | 26 | Staggering of variables: tracers T are at cell centers, U and V 27 | velocity components at cell edges and streamfunction P at cell 28 | corners. The shown variables have the same logical index (i,j). 29 | All arrays have the same size (same 'shape' in python wording), 30 | regardless of their position. 31 | 32 | The Poisson equation relating vorticity and streamfunction is written 33 | at cell corners. The model uses a bilinear interpolation to send the 34 | r.h.s. at cell corners. 35 | 36 | 37 | Time discretization 38 | =================== 39 | 40 | The model proposes various time schemes: from Euler-forward to RK3, 41 | including LF-AM3 the predictor-corrector scheme of ROMS (the regional 42 | ocean modelling system). All schemes are all explicit. 43 | 44 | It is very easy to implement new time schemes. They are all gathered 45 | in the `core/timescheme.py` module. They have been written to be as 46 | close as a textbook formulation. 47 | 48 | The time scheme is controlled by 49 | 50 | .. code:: 51 | 52 | param.timestepping='RK3_SSP' 53 | 54 | 55 | Advection term 56 | ============== 57 | 58 | The core of fluid2d is the transport equation. It is coded in 59 | volume-flux form. Each tracer quantity can only be transfered from one 60 | cell to another. This guarantees exact conservation. Fluxes are 61 | computed on cell edges using either centered interpolation or upwinded 62 | interpolation. The former correspond to centered discretization 63 | (either 2nd or 4th order), the latter correspond to odd order (1st, 64 | 3rd or 5th). The upwinded discretizations provide a built-in 65 | dissipation that allow to run the code without any explicit 66 | dissipation. 67 | 68 | The flux order is controlled by 69 | 70 | .. code:: 71 | 72 | param.order=5 73 | 74 | 75 | The Poisson equation 76 | ==================== 77 | 78 | It is solved using a home-made parallel geometric multigrid. The 79 | multigrid is stored in the dedicated folder `core/gmg`. The 80 | restriction and interpolation stages take care of the mask so that 81 | each level of the multigrid sees the boundary, if there is one. 82 | 83 | The bulk of the computational time is spent in this module. 84 | -------------------------------------------------------------------------------- /docs/parameters.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvthinker/Fluid2d/75289b2bb1594b52c9e65a16e2800ae7f5b868aa/docs/parameters.pdf -------------------------------------------------------------------------------- /docs/staggering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvthinker/Fluid2d/75289b2bb1594b52c9e65a16e2800ae7f5b868aa/docs/staggering.png -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: fluid2d 2 | channels: 3 | - defaults 4 | dependencies: 5 | - _libgcc_mutex=0.1=main 6 | - blas=1.0=mkl 7 | - brotli=1.0.9=he6710b0_2 8 | - c-ares=1.17.1=h27cfd23_0 9 | - ca-certificates=2021.10.26=h06a4308_2 10 | - certifi=2021.10.8=py37h06a4308_0 11 | - cftime=1.5.0=py37h6323ea4_0 12 | - curl=7.78.0=h1ccaba5_0 13 | - cycler=0.10.0=py37_0 14 | - dbus=1.13.18=hb2f20db_0 15 | - expat=2.4.1=h2531618_2 16 | - fontconfig=2.13.1=h6c09931_0 17 | - fonttools=4.25.0=pyhd3eb1b0_0 18 | - freetype=2.11.0=h70c0345_0 19 | - giflib=5.2.1=h7b6447c_0 20 | - glib=2.69.1=h5202010_0 21 | - gst-plugins-base=1.14.0=h8213a91_2 22 | - gstreamer=1.14.0=h28cd5cc_2 23 | - hdf4=4.2.13=h3ca952b_2 24 | - hdf5=1.10.6=hb1b8bf9_0 25 | - icu=58.2=he6710b0_3 26 | - intel-openmp=2021.4.0=h06a4308_3561 27 | - jpeg=9d=h7f8727e_0 28 | - kiwisolver=1.3.1=py37h2531618_0 29 | - krb5=1.19.2=hac12032_0 30 | - lcms2=2.12=h3be6417_0 31 | - ld_impl_linux-64=2.35.1=h7274673_9 32 | - libcurl=7.78.0=h0b77cf5_0 33 | - libedit=3.1.20210910=h7f8727e_0 34 | - libev=4.33=h7f8727e_1 35 | - libffi=3.3=he6710b0_2 36 | - libgcc-ng=9.1.0=hdf63c60_0 37 | - libgfortran-ng=7.3.0=hdf63c60_0 38 | - libnetcdf=4.6.1=h2053bdc_4 39 | - libnghttp2=1.41.0=hf8bcb03_2 40 | - libpng=1.6.37=hbc83047_0 41 | - libssh2=1.9.0=h1ba5d50_1 42 | - libstdcxx-ng=9.1.0=hdf63c60_0 43 | - libtiff=4.2.0=h85742a9_0 44 | - libuuid=1.0.3=h7f8727e_2 45 | - libwebp=1.2.0=h89dd481_0 46 | - libwebp-base=1.2.0=h27cfd23_0 47 | - libxcb=1.14=h7b6447c_0 48 | - libxml2=2.9.10=hb55368b_3 49 | - lz4-c=1.9.3=h295c915_1 50 | - matplotlib=3.4.3=py37h06a4308_0 51 | - matplotlib-base=3.4.3=py37hbbc1b5f_0 52 | - mkl=2021.4.0=h06a4308_640 53 | - mkl-service=2.4.0=py37h7f8727e_0 54 | - mkl_fft=1.3.1=py37hd3c417c_0 55 | - mkl_random=1.2.2=py37h51133e4_0 56 | - mpi=1.0=mpich 57 | - mpi4py=3.0.3=py37hf046da1_1 58 | - mpich=3.3.2=hc856adb_0 59 | - munkres=1.1.4=py_0 60 | - ncurses=6.3=h7f8727e_2 61 | - netcdf4=1.5.7=py37h0a24e14_0 62 | - numpy=1.21.2=py37h20f2e39_0 63 | - numpy-base=1.21.2=py37h79a1101_0 64 | - olefile=0.46=py37_0 65 | - openssl=1.1.1l=h7f8727e_0 66 | - pcre=8.45=h295c915_0 67 | - pillow=8.4.0=py37h5aabda8_0 68 | - pip=21.0.1=py37h06a4308_0 69 | - pyparsing=3.0.4=pyhd3eb1b0_0 70 | - pyqt=5.9.2=py37h05f1152_2 71 | - python=3.7.11=h12debd9_0 72 | - python-dateutil=2.8.2=pyhd3eb1b0_0 73 | - qt=5.9.7=h5867ecd_1 74 | - readline=8.1=h27cfd23_0 75 | - scipy=1.6.2=py37had2a1c9_1 76 | - setuptools=58.0.4=py37h06a4308_0 77 | - sip=4.19.8=py37hf484d3e_0 78 | - six=1.16.0=pyhd3eb1b0_0 79 | - sqlite=3.36.0=hc218d9a_0 80 | - tk=8.6.11=h1ccaba5_0 81 | - tornado=6.1=py37h27cfd23_0 82 | - wheel=0.37.0=pyhd3eb1b0_1 83 | - xz=5.2.5=h7b6447c_0 84 | - zlib=1.2.11=h7b6447c_3 85 | - zstd=1.4.9=haebb681_0 86 | 87 | 88 | -------------------------------------------------------------------------------- /experiments/Advection/advection_footprint.py: -------------------------------------------------------------------------------- 1 | # footprint of an advection scheme (space+time) 2 | 3 | from param import Param 4 | from grid import Grid 5 | from fluid2d import Fluid2d 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | 9 | 10 | param = Param('default') 11 | param.modelname = 'advection' 12 | param.expname = 'footprint_0' 13 | param.nx = 32*2 14 | param.ny = 32*2 15 | param.npy = 1 16 | param.Lx = 1. 17 | param.Ly = 1. 18 | param.geometry = 'disc' 19 | param.tend = 40. 20 | param.cfl = 1.0 21 | param.order = 5 22 | param.timestepping = 'RK3_SSP' 23 | 24 | param.plot_var = 'tracer' 25 | param.var_to_save = ['tracer'] 26 | param.list_diag = ['mean', 'rms'] 27 | param.freq_his = 10 28 | param.freq_diag = 1 29 | param.freq_plot = 1 30 | param.colorscheme = 'minmax' 31 | param.cax = [-1.2, 1.2] 32 | param.plot_interactive = True 33 | param.adaptable_dt = True 34 | param.dt = .1 35 | param.dtmax = 1e4 36 | param.diffusion = True 37 | param.Kdiff = 0. 38 | 39 | grid = Grid(param) 40 | 41 | 42 | # grid.msk[-60:-40,40:60]=0 43 | # grid.msk[:64,64]=0 44 | 45 | f2d = Fluid2d(param, grid) 46 | model = f2d.model 47 | 48 | xr, yr = grid.xr, grid.yr 49 | # 1/ set the stream function 50 | sigma = 2*param.Lx 51 | state = model.var.get('tracer') 52 | state[:] = 1. # body rotation 53 | state[:] = state[:]*grid.msk 54 | model.set_psi_from_tracer() 55 | 56 | 57 | nh = f2d.nh 58 | 59 | # 2/ set one grid cell to 1 60 | state[:] = 0. 61 | state[nh+param.ny//3, nh+param.nx//3] = 1. 62 | state[:] *= grid.msk 63 | 64 | 65 | # f2d.loop() 66 | 67 | 68 | t = 0. 69 | var = f2d.model.var 70 | 71 | # advect it for one time step only 72 | 73 | f2d.model.diagnostics(var, 0.) 74 | f2d.set_dt(0) 75 | for k in range(1): 76 | f2d.model.step(t, f2d.dt) 77 | 78 | print('everything was ok') 79 | 80 | # look at the result 81 | 82 | z2d = f2d.model.var.get(param.plot_var)[nh:-nh, nh:-nh] 83 | msk = grid.msk[nh:-nh, nh:-nh] 84 | z2d = np.log10(1e-6+abs(z2d)) 85 | 86 | f2d.plotting.set_cax(z2d) 87 | cax = f2d.plotting.cax 88 | z2d[msk == 0] = np.nan 89 | fig = plt.figure() 90 | im = plt.imshow(z2d, vmin=cax[0], vmax=cax[1], origin='lower') 91 | plt.title('combined space-time stencil (in log-scale)') 92 | plt.colorbar(im) 93 | plt.show() 94 | -------------------------------------------------------------------------------- /experiments/Advection/advection_irreversibility.py: -------------------------------------------------------------------------------- 1 | # irreversibility effects for an advection scheme (space+time) 2 | 3 | import numpy as np 4 | from param import Param 5 | from grid import Grid 6 | from fluid2d import Fluid2d 7 | from plotting import Plotting 8 | import matplotlib.pyplot as plt 9 | 10 | 11 | param = Param('default') 12 | param.modelname = 'advection' 13 | param.expname = 'irrev_0' 14 | 15 | # domain and resolution 16 | param.nx = 64*2 17 | param.ny = 64*2 18 | param.npx = 1 19 | param.npy = 1 20 | param.Lx = 1. 21 | param.Ly = 1. 22 | param.geometry = 'perio' 23 | 24 | # time 25 | param.tend = 1. 26 | param.cfl = 1.2 27 | param.adaptable_dt = True 28 | param.dt = .1 29 | # param.dtmax=1e4 30 | 31 | # *** discretization *** 32 | param.order = 5 # 1,2,3,4,5 : order of the spatial discretization. Even 33 | # cases are centered fluxes, odd cases are upwinded 34 | # fluxes 35 | param.timestepping = 'RK3_SSP' # LFAM3, Heun, EF, LF, RK3_SSP, AB2, 36 | # AB3. See core/timestepping.py to see the 37 | # list, you may add your own in there 38 | 39 | # output 40 | param.var_to_save = ['tracer'] 41 | param.list_diag = ['mean', 'rms'] 42 | param.freq_his = 1 43 | param.freq_diag = 1 44 | 45 | # plot 46 | param.plot_var = 'tracer' # 'psi' or 'tracer' 47 | param.freq_plot = 1 48 | param.colorscheme = 'imposed' 49 | param.cax = [-0.2, 2.2] 50 | param.plot_interactive = True 51 | # param.plotting_module='plotting_adv' 52 | 53 | # physics 54 | param.diffusion = False 55 | 56 | 57 | # ********* PARAMETERS CONTROLLING THE FLOW AND THE TRACER ******** 58 | flow_config = 0 # controls the flow 59 | tracer_config = 0 # controls the tracer 60 | 61 | 62 | grid = Grid(param) 63 | param.Kdiff = 1e-3*grid.dx 64 | 65 | # put obstacles: uncomment below 66 | # grid.msk[-60:-40,40:60]=0 67 | # grid.msk[:64,64]=0 68 | 69 | f2d = Fluid2d(param, grid) 70 | model = f2d.model 71 | 72 | 73 | def vortex(x0, y0, sigma): 74 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2) 75 | y = 2-2./(1+np.exp(-x/sigma)) 76 | #y = cos(x/sigma*pi/2) 77 | # y[x>sigma]=0. 78 | return y 79 | 80 | 81 | xr, yr = grid.xr, grid.yr 82 | # 1/ set the stream function 83 | 84 | state = model.var.get('tracer') 85 | u = model.var.get('u') 86 | v = model.var.get('v') 87 | psi = model.var.get('psi') 88 | 89 | 90 | if flow_config == 0: # pure translation 91 | angle = 20. 92 | speed = 0.1 93 | if param.geometry == 'perio': 94 | u[:] = speed*np.cos(angle*np.pi/180.) 95 | v[:] = speed*np.sin(angle*np.pi/180.) 96 | else: 97 | print('param.geometry should be perio') 98 | exit(0) 99 | 100 | if flow_config == 1: # body rotation 101 | state[:] = 1. 102 | state[:] = state[:]*grid.msk 103 | # determine the stream function from the vorticity 104 | model.set_psi_from_tracer() 105 | 106 | if flow_config == 2: # horizontal shear 107 | if param.geometry == 'xchannel': 108 | state[:] = -1. 109 | state[:] = state[:]*grid.msk 110 | # determine the stream function from the vorticity 111 | model.set_psi_from_tracer() 112 | else: 113 | print('param.geometry should be xchannel') 114 | exit(0) 115 | 116 | if flow_config == 3: # single vortex 117 | sigma = grid.dx*5 118 | state[:] = 100.*vortex(0.5, 0.5, sigma) 119 | # determine the stream function from the vorticity 120 | model.set_psi_from_tracer() 121 | 122 | 123 | if flow_config == 4: # quadrupole 124 | if param.geometry == 'perio': 125 | sigma = 5*grid.dx 126 | state[:] = vortex(0.75, 0.75, sigma) # single vortex 127 | state[:] += vortex(0.25, 0.25, sigma) # single vortex 128 | state[:] -= vortex(0.25, 0.75, sigma) # single vortex 129 | state[:] -= vortex(0.75, 0.25, sigma) # single vortex 130 | state *= 100. 131 | # determine the stream function from the vorticity 132 | model.set_psi_from_tracer() 133 | else: 134 | print('param.geometry should be perio') 135 | exit(0) 136 | 137 | 138 | # normalize all flows with 'maxspeed=1' 139 | f2d.model.diagnostics(f2d.model.var, 0.) 140 | maxspeed = f2d.model.diags['maxspeed'] 141 | u = u/maxspeed 142 | v = v/maxspeed 143 | psi = psi/maxspeed 144 | 145 | 146 | # 2/ set an initial tracer field 147 | sigma = 0.05*param.Lx 148 | 149 | 150 | if tracer_config == 0: # single localized patch 151 | state[:] = 2*vortex(0.3, 0.4, sigma) 152 | 153 | if tracer_config == 1: # tiles 154 | state[:] = round(xr*4) % 2 + round(yr*4) % 2 155 | 156 | if tracer_config == 2: # a closed line 157 | x = xr/param.Lx 158 | y = yr/param.Ly 159 | msk = np.zeros_like(yr) 160 | msk[(x > 0.3) & (x < 0.7) & (y > 0.3) & (y < 0.7)] = 1 161 | z = np.roll(msk, -1, axis=1)+np.roll(msk, -1, axis=0) + \ 162 | np.roll(msk, +1, axis=1)+np.roll(msk, +1, axis=0)-4*msk 163 | state[:] = 0. 164 | state[z > 0] = 4. 165 | 166 | 167 | state[:] *= grid.msk 168 | 169 | 170 | # ***** instead of looping in time we take control of the time steps 171 | nh = param.nh 172 | z2d = f2d.model.var.get(param.plot_var)[nh:-nh, nh:-nh] 173 | msk = grid.msk[nh:-nh, nh:-nh] 174 | 175 | t = 0. 176 | var = f2d.model.var 177 | 178 | 179 | # advect it for nite time step only 180 | nite = 1 181 | 182 | 183 | z0 = z2d.copy() # copy initial state 184 | 185 | f2d.loop() 186 | # f2d.model.diagnostics(var,0.) 187 | # f2d.set_dt() 188 | # for k in range(nite): 189 | # f2d.model.step(t,f2d.dt) 190 | 191 | # revert the velocity 192 | var.state[2] *= -1. 193 | var.state[3] *= -1. 194 | 195 | f2d.t = 0 196 | f2d.kt = 0 197 | f2d.loop() 198 | # for k in range(nite): 199 | # f2d.model.step(t,f2d.dt) 200 | 201 | # substract the initial state 202 | z2d = z2d-z0 203 | 204 | print('everything was ok') 205 | 206 | # look at the result 207 | 208 | 209 | plt.ioff() 210 | 211 | maxi = max(abs(z2d.flatten())) 212 | 213 | fig = plt.figure() 214 | im = plt.imshow(z2d, vmin=-maxi, vmax=maxi, origin='lower') 215 | plt.title('Final state - Initial state') 216 | plt.colorbar(im) 217 | plt.show() 218 | -------------------------------------------------------------------------------- /experiments/Advection/all_about_advection.py: -------------------------------------------------------------------------------- 1 | # test all about advection schemes 2 | from param import Param 3 | from grid import Grid 4 | from fluid2d import Fluid2d 5 | import numpy as np 6 | 7 | 8 | param = Param('default') 9 | param.modelname = 'advection' 10 | param.expname = 'advection_0' 11 | 12 | # domain and resolution 13 | param.nx = 64*2 14 | param.ny = 64*2 15 | param.npx = 1 16 | param.npy = 1 17 | param.Lx = 1. 18 | param.Ly = 1. 19 | param.geometry = 'disc' # 'square', 'perio', 'disc', 'xchannel' 20 | 21 | # time 22 | param.tend = 10. 23 | param.cfl = 1.5 24 | param.adaptable_dt = True 25 | param.dt = 1e4 26 | param.dtmax=1e4 27 | param.asselin_cst = 0.1 # to go with the Leap-Frog time stepping 28 | 29 | # *** discretization *** 30 | param.order = 5 # order of the spatial discretization, between 1 and 5 31 | # Even orders are centered fluxes 32 | # Odd orders are upwinded fluxes 33 | 34 | param.timestepping = 'RK3_SSP' # LFAM3, Heun, EF, LF, RK3_SSP, AB2, AB3. 35 | # See core/timestepping.py to see the 36 | # list, you may add your own in there 37 | # 38 | # table of CFL (from Lemarie et al 2015) 39 | # 40 | # caution: their order=4 discretization is not the same as in Fluid2d 41 | # 42 | # |-------------+-------+-------+-------| 43 | # | order | 2 | 3 | 4 | 44 | # |-------------+-------+-------+-------| 45 | # | LFRA nu=0.1 | 0.904 | 0.472 | 0.522 | 46 | # | LFAM3 | 1.587 | 0.871 | 0.916 | 47 | # | AB2 eps=0.1 | 0.503 | 0.554 | 0.29 | 48 | # | AB3 | 0.724 | 0.397 | 0.418 | 49 | # | RK3 | 1.73 | 1.626 | 1 | 50 | # |-------------+-------+-------+-------| 51 | # 52 | 53 | # 54 | # ********* PARAMETERS CONTROLLING THE FLOW AND THE TRACER ******** 55 | # 56 | flow_config = 1 # controls the flow 57 | # 0: translation (it needs param.geometry = 'perio') 58 | # 1: body rotation 59 | # 2: shear 60 | # 3: vortex 61 | 62 | tracer_config = 0 # controls the tracer 63 | # 0: isolated patch, width controlled with patch_width 64 | # 1: tiles 65 | # 2: dx width square contour 66 | patch_width = 0.02*param.Lx 67 | 68 | 69 | # output 70 | param.var_to_save = ['tracer', 'psi'] 71 | param.list_diag = ['mean', 'rms'] 72 | param.freq_his = .1 73 | param.freq_diag = .1 74 | 75 | # plot 76 | param.plot_var = 'tracer' # 'psi' or 'tracer' 77 | param.freq_plot = 30 78 | param.colorscheme = 'imposed' # 'imposed' or 'minmax'=self adjust 79 | param.cax = [-0.2, 1.2] 80 | param.plot_interactive = True 81 | param.generate_mp4 = True 82 | param.plotting_module = 'plotting_adv' 83 | param.plot_psi = True 84 | 85 | # physics 86 | param.diffusion = False 87 | 88 | 89 | grid = Grid(param) 90 | param.Kdiff = 1e-3*grid.dx 91 | 92 | # put obstacles: uncomment below 93 | # grid.msk[-60:-40,40:60]=0 94 | # grid.msk[:64,64]=0 95 | 96 | f2d = Fluid2d(param, grid) 97 | model = f2d.model 98 | 99 | 100 | def gaussian_shape(x0, y0, sigma): 101 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2) 102 | y = 2-2./(1+np.exp(-x/sigma)) 103 | # y = cos(x/sigma*pi/2) 104 | # y[x>sigma]=0. 105 | return y 106 | 107 | 108 | 109 | # 1/ set the stream function 110 | 111 | state = model.var.get('tracer') 112 | u = model.var.get('u') 113 | v = model.var.get('v') 114 | psi = model.var.get('psi') 115 | xr, yr = grid.xr, grid.yr 116 | 117 | if flow_config == 0: # pure translation 118 | assert param.geometry == 'perio', '[ERROR]: param.geometry should be perio' 119 | angle = 0. # with respect to the x axis 120 | speed = 1. 121 | u[:] = speed*np.cos(angle*np.pi/180.) 122 | v[:] = speed*np.sin(angle*np.pi/180.) 123 | 124 | elif flow_config == 1: # body rotation 125 | assert param.geometry != 'perio', '[ERROR]: param.geometry should not be perio' 126 | state[:] = 1. 127 | state[:] = state[:]*grid.msk 128 | 129 | elif flow_config == 2: # horizontal shear 130 | assert param.geometry == 'xchannel', '[ERROR]: param.geometry should be xchannel' 131 | state[:] = -1. 132 | state[:] = state[:]*grid.msk 133 | 134 | elif flow_config == 3: # single vortex 135 | sigma = grid.dx*5 136 | state[:] = 100.*gaussian_shape(0.5, 0.5, sigma) 137 | 138 | elif flow_config == 4: # quadrupole 139 | assert param.geometry == 'perio', '[ERROR]: param.geometry should be perio' 140 | sigma = 5*grid.dx 141 | state[:] = gaussian_shape(0.75, 0.75, sigma) # single vortex 142 | state[:] += gaussian_shape(0.25, 0.25, sigma) # single vortex 143 | state[:] -= gaussian_shape(0.25, 0.75, sigma) # single vortex 144 | state[:] -= gaussian_shape(0.75, 0.25, sigma) # single vortex 145 | 146 | 147 | elif flow_config == 5: # a complicated flow 148 | assert param.geometry in ['perio', 'closed'], '[ERROR]: param.geometry should be perio or closed' 149 | np.random.seed(42) 150 | for k in range(8): 151 | sigma = np.random.uniform(10)*grid.dx 152 | sign = np.random.randint(2)*2-1 153 | x0 = np.random.uniform() 154 | y0 = np.random.uniform() 155 | state[:] += sign*gaussian_shape(x0, y0, sigma)/np.sqrt(sigma) 156 | 157 | else: 158 | raise ValueError("[ERROR]: undefined flow_config") 159 | 160 | model.set_psi_from_tracer() 161 | 162 | 163 | 164 | # 2/ set an initial tracer field 165 | 166 | if tracer_config == 0: # single localized patch 167 | state[:] = 1.2*gaussian_shape(0.3, 0.4, patch_width) 168 | 169 | elif tracer_config == 1: # tiles 170 | state[:] = (np.round(xr*6) % 2 + np.round(yr*6) % 2)/2 171 | 172 | elif tracer_config == 2: # a closed line 173 | x = xr/param.Lx 174 | y = yr/param.Ly 175 | msk = np.zeros_like(yr) 176 | msk[(x > 0.4) & (x < 0.7) & (y > 0.4) & (y < 0.7)] = 1 177 | z = np.roll(msk, -1, axis=1)+np.roll(msk, -1, axis=0) + \ 178 | np.roll(msk, +1, axis=1)+np.roll(msk, +1, axis=0)-4*msk 179 | state[:] = 0. 180 | state[z > 0] = 4. 181 | 182 | else: 183 | raise ValueError("[ERROR]: undefined tracer_config") 184 | 185 | 186 | state[:] *= grid.msk 187 | 188 | dummy = state*1. 189 | grid.fill_halo(dummy) 190 | state[:] = dummy 191 | 192 | f2d.loop() 193 | 194 | maxspeed = f2d.model.diags['maxspeed'] 195 | 196 | print(f"time step : {f2d.dt:.3e}") 197 | print(f"max speed : {maxspeed:.3e}") 198 | print(f"grid size : {grid.dx:.3e}") 199 | print(f"CFL : {param.cfl:.2f}") 200 | -------------------------------------------------------------------------------- /experiments/GravityCurrent/gravitycurrent.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gravity current along a topographic slope 3 | """ 4 | from fluid2d import Fluid2d 5 | from param import Param 6 | from grid import Grid 7 | import numpy as np 8 | 9 | param = Param('default.xml') 10 | param.modelname = 'boussinesq' 11 | param.expname = 'gravcurr_00' 12 | 13 | # domain and resolution 14 | 15 | param.ny = 128 16 | param.nx = param.ny*2 17 | param.npx = 1 18 | param.npy = 1 19 | param.Lx = 2. 20 | param.Ly = 1. 21 | param.geometry = 'closed' 22 | 23 | # time 24 | param.tend = 5. 25 | param.cfl = 1.2 26 | param.adaptable_dt = True 27 | param.dt = .1 28 | param.dtmax = .1 29 | 30 | # discretization 31 | param.order = 5 32 | 33 | 34 | # output 35 | param.plot_var = 'buoyancy' 36 | param.var_to_save = ['vorticity', 'buoyancy', 'psi'] 37 | param.list_diag = 'all' 38 | param.freq_his = .2 39 | param.freq_diag = .1 40 | 41 | # plot 42 | param.plot_interactive = True 43 | param.freq_plot = 10 44 | param.colorscheme = 'imposed' 45 | param.cax = [0, 3] 46 | param.generate_mp4 = True 47 | 48 | # physics 49 | param.gravity = 1. 50 | param.forcing = False 51 | param.diffusion = True 52 | param.noslip = False 53 | 54 | grid = Grid(param) 55 | param.Kdiff = 2e-3*grid.dx**2 56 | 57 | xr, yr = grid.xr, grid.yr 58 | # add a mask 59 | hb = np.exp(-(xr/param.Lx-0.5)**2 * 50)*0.7 60 | grid.msk[(yr < hb)] = 0 61 | grid.finalize_msk() 62 | 63 | f2d = Fluid2d(param, grid) 64 | model = f2d.model 65 | 66 | 67 | buoy = model.var.get('buoyancy') 68 | 69 | 70 | def sigmoid(x, delta): 71 | return 1/(1+np.exp(-(x-0.5)/delta)) 72 | 73 | 74 | def stratif(): 75 | sigma = 3*grid.dx # width of the interface 76 | b = sigmoid(xr/param.Lx, sigma/param.Lx) 77 | return (1-b) * grid.msk 78 | 79 | 80 | # to have a look, exchange the type of flow 81 | buoy[:] = (1-stratif() - 0.5)*grid.msk 82 | 83 | buoy[:] = (yr * (1 + 1*(1+np.tanh((xr-param.Lx/2)/0.1)))) * grid.msk 84 | 85 | # add noise to trigger the instability 86 | noise = np.random.normal(size=np.shape(yr), scale=1.)*grid.msk 87 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 88 | grid.fill_halo(noise) 89 | 90 | buoy += 1e-8*noise 91 | 92 | model.set_psi_from_vorticity() 93 | 94 | 95 | f2d.loop() 96 | -------------------------------------------------------------------------------- /experiments/GyreCirculation/double_gyre.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | 7 | class Forcing: 8 | """ define the forcing """ 9 | 10 | def __init__(self, param, grid): 11 | yr = (grid.yr - param.Ly/2) / param.Ly 12 | 13 | # jet width relative to param.Ly 14 | sigma = 0.1 15 | 16 | # forcing intensity 17 | tau0 = 1e-4 18 | 19 | # type of forcing: comment/uncomment choice A vs. B 20 | 21 | # choice A/ jet-like forcing (localized in y) 22 | # self.forc = tau0 * (yr/sigma)*np.exp(-yr**2/(2*sigma**2)) * grid.msk 23 | 24 | # choice B/ basin-scale forcing: double gyre configuration 25 | self.forc = tau0 * np.sin(yr * np.pi) * grid.msk 26 | 27 | total = grid.domain_integration(self.forc) 28 | self.forc -= (total / grid.area) * grid.msk 29 | 30 | def add_forcing(self, x, t, dxdt): 31 | """ add the forcing term on x[0]=the vorticity """ 32 | dxdt[0] += self.forc 33 | 34 | 35 | param = Param('default.xml') 36 | param.modelname = 'quasigeostrophic' 37 | param.expname = 'dbl_gyre_00' 38 | 39 | # domain and resolution 40 | param.nx = 64*2 41 | param.ny = 64 42 | param.npy = 1 43 | param.Lx = 2. 44 | param.Ly = param.Lx/2 45 | param.geometry = 'closed' 46 | 47 | # time 48 | param.tend = 2000. 49 | param.cfl = 1.5 50 | param.adaptable_dt = True 51 | param.dt = 1. 52 | param.dtmax = 100. 53 | 54 | # discretization 55 | param.order = 5 56 | 57 | 58 | # output 59 | param.var_to_save = ['pv', 'psi', 'pvanom'] 60 | param.list_diag = 'all' 61 | param.freq_his = 50 62 | param.freq_diag = 10 63 | 64 | # plot 65 | param.plot_var = 'pv' 66 | param.freq_plot = 10 67 | a = 0.5 68 | param.cax = [-a, a] 69 | param.plot_interactive = True 70 | param.colorscheme = 'imposed' 71 | param.generate_mp4 = True 72 | 73 | # physics 74 | param.beta = 1. 75 | param.Rd = 1. # 2*grid.dx 76 | param.forcing = True 77 | param.noslip = False 78 | param.diffusion = False 79 | 80 | grid = Grid(param) 81 | param.Kdiff = 0.5e-4*grid.dx 82 | 83 | # add an island 84 | # grid.msk[28:32,34:38]=0 85 | # grid.finalize_msk() 86 | 87 | 88 | f2d = Fluid2d(param, grid) 89 | model = f2d.model 90 | 91 | # set the forcing 92 | model.forc = Forcing(param, grid) 93 | 94 | xr, yr = grid.xr, grid.yr 95 | vor = model.var.get('pv') 96 | 97 | 98 | # set an initial tracer field 99 | 100 | vor[:] = 1e-2*np.random.normal(size=np.shape(vor))*grid.msk 101 | y = vor[:]*1. 102 | model.ope.fill_halo(y) 103 | vor[:] = y 104 | 105 | model.add_backgroundpv() 106 | 107 | model.set_psi_from_pv() 108 | 109 | f2d.loop() 110 | -------------------------------------------------------------------------------- /experiments/InterfacialWave/interfacialwave.py: -------------------------------------------------------------------------------- 1 | """ 2 | interfacial wave 3 | 4 | for a small 'amplitude' this is a gentle oscillation 5 | 6 | for a larger 'amplitude' the wave breaks 7 | 8 | """ 9 | from fluid2d import Fluid2d 10 | from param import Param 11 | from grid import Grid 12 | from numpy import exp, sqrt, pi, cos, sin, where, random, shape, tanh, cumsum, cosh 13 | import numpy as np 14 | from restart import Restart 15 | from island import Island 16 | 17 | param = Param('default.xml') 18 | param.modelname = 'boussinesq' 19 | param.expname = 'Wave_NH' 20 | 21 | # domain and resolution 22 | ratio = 2 23 | param.ny = 64*2 24 | param.nx = param.ny/ratio 25 | param.Ly = 1. 26 | param.Lx = param.Ly/ratio 27 | param.npx = 1 28 | param.npy = 1 29 | param.geometry = 'closed' 30 | 31 | param.hydroepsilon = 1. 32 | 33 | # time 34 | param.tend = 10 35 | param.cfl = 0.3 36 | param.adaptable_dt = False 37 | param.dt = 2e-2 38 | param.dtmax = 1. 39 | 40 | # discretization 41 | param.order = 5 42 | param.timestepping = 'RK3_SSP' # 'LFAM3' 43 | 44 | # output 45 | param.var_to_save = ['vorticity', 'psi', 46 | 'u', 'v', 'buoyancy', 'banom', 'tracer'] 47 | param.list_diag = ['ke', 'pe', 'energy', 'brms', 'vorticity', 'enstrophy'] 48 | param.freq_plot = 5 49 | param.freq_his = .2 50 | param.freq_diag = .1 51 | 52 | # plot 53 | param.plot_interactive = True 54 | param.plot_var = 'buoyancy' 55 | param.cax = np.asarray([-0.1, 1.1]) 56 | param.colorscheme = 'imposed' 57 | param.generate_mp4 = True 58 | param.cmap = 'RdGy_r' 59 | 60 | # physics 61 | param.forcing = False 62 | param.noslip = False 63 | param.diffusion = False 64 | param.forcing = False 65 | param.additional_tracer = ['tracer'] 66 | 67 | param.gravity = 1. 68 | 69 | nh = param.nh 70 | 71 | grid = Grid(param) 72 | 73 | 74 | f2d = Fluid2d(param, grid) 75 | model = f2d.model 76 | 77 | xr, yr = grid.xr, grid.yr 78 | vor = model.var.get('vorticity') 79 | buoy = model.var.get('buoyancy') 80 | 81 | # control parameters of the experiment 82 | N2 = 1. # Brunt Vaisala frequency squared 83 | 84 | 85 | # linear stratification 86 | amplitude = 0.02 87 | width = 0.05 88 | thickness = 3*grid.dx # interface thickness 89 | 90 | # sine perturbation 91 | x = amplitude*sin(pi*(grid.xr/param.Lx-0.5)) 92 | 93 | # localized perturbation on the left 94 | #x = amplitude*np.exp(-(grid.xr/param.Lx)**2/(2*width**2)) 95 | buoy[:, :] = 0.5*(1+tanh((grid.yr0+x)/thickness))*grid.msk * N2 96 | 97 | 98 | model.set_psi_from_vorticity() 99 | 100 | # 2/ set an initial tracer field 101 | 102 | 103 | def vortex(x0, y0, sigma): 104 | x = sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2) 105 | y = 2-2./(1+exp(-x/sigma)) 106 | return y 107 | 108 | 109 | sigma = 0.01*param.Lx 110 | 111 | state = model.var.get('tracer') 112 | 113 | tracer_config = 1 114 | 115 | if tracer_config == 0: # single localized patch 116 | state[:, :] = 2*vortex(0.3, 0.4, sigma) 117 | 118 | if tracer_config == 1: # tiles 119 | state[:, :] = np.round(xr*6) % 2 + np.round(yr*6) % 2 120 | 121 | if tracer_config == 2: # a closed line 122 | x = xr/param.Lx 123 | y = yr/param.Ly 124 | msk = np.zeros_like(yr) 125 | msk[(x > 0.4) & (x < 0.7) & (y > 0.4) & (y < 0.7)] = 1 126 | z = np.roll(msk, -1, axis=1)+np.roll(msk, -1, axis=0) + \ 127 | np.roll(msk, +1, axis=1)+np.roll(msk, +1, axis=0)-4*msk 128 | state[:, :] = 0. 129 | state[z > 0] = 4. 130 | 131 | 132 | state[:, :] *= grid.msk 133 | 134 | 135 | f2d.loop() 136 | -------------------------------------------------------------------------------- /experiments/Internal_IVP/internalwave.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | from restart import Restart 6 | from island import Island 7 | 8 | param = Param('default.xml') 9 | param.modelname = 'boussinesq' 10 | param.expname = 'Internal_0' 11 | 12 | # domain and resolution 13 | ratio = 2 14 | param.ny = 64*2 15 | param.nx = param.ny*ratio 16 | param.Ly = 1. 17 | param.Lx = param.Ly*ratio 18 | param.npx = 1 19 | param.npy = 1 20 | param.geometry = 'closed' 21 | 22 | param.hydroepsilon = 1 23 | 24 | # time 25 | param.tend = 5. 26 | param.cfl = 0.2 27 | param.adaptable_dt = False 28 | param.dt = 1e-2 29 | param.dtmax = 1. 30 | 31 | # discretization 32 | param.order = 5 33 | param.timestepping = 'RK3_SSP' # 'LFAM3' 34 | 35 | # output 36 | param.var_to_save = ['vorticity', 'psi', 'u', 'v', 'buoyancy', 'banom'] 37 | param.list_diag = ['ke', 'pe', 'energy', 'brms', 'vorticity', 'enstrophy'] 38 | param.freq_plot = 5 39 | param.freq_his = .2 40 | param.freq_diag = .1 41 | 42 | # plot 43 | param.plot_interactive = True 44 | param.plot_var = 'banom' 45 | param.cax = np.asarray([-0.1, 0.1]) 46 | param.colorscheme = 'imposed' # 'fixed' 47 | param.generate_mp4 = True 48 | 49 | # physics 50 | param.forcing = False 51 | param.noslip = False 52 | param.diffusion = False 53 | param.forcing = False 54 | 55 | param.gravity = 1. 56 | 57 | nh = param.nh 58 | 59 | grid = Grid(param) 60 | 61 | 62 | f2d = Fluid2d(param, grid) 63 | model = f2d.model 64 | 65 | xr, yr = grid.xr, grid.yr 66 | 67 | vor = model.var.get('vorticity') 68 | buoy = model.var.get('buoyancy') 69 | 70 | # control parameters of the experiment 71 | N2 = 10. # Brunt Vaisala frequency squared 72 | nlayers = 10 73 | background_stratif = 'continuous' 74 | delta = param.Lx*0.03 75 | delta = grid.dx*4 76 | amplitude = .2 77 | 78 | if background_stratif == 'continuous': 79 | buoy[:, :] = grid.yr * grid.msk * N2 80 | 81 | elif background_stratif == 'layered': 82 | 83 | y = grid.yr*nlayers/param.Ly 84 | buoy[:, :] = np.floor(y) * (param.Ly/nlayers)*N2*grid.msk 85 | else: 86 | print('background stratification is undefined') 87 | exit(0) 88 | model.bref[:, :] = buoy 89 | 90 | y = -grid.yr0 91 | perturb = amplitude*np.exp(-(grid.yr0**2+grid.xr0**2)/(2*delta**2)) 92 | buoy += perturb 93 | 94 | # we perturb the fluid by introducing a localized dipole of vorticity 95 | x = grid.xr0 96 | #vor[:, :] = perturb*np.sin(x*2*pi*2)/delta * grid.msk 97 | 98 | 99 | model.set_psi_from_vorticity() 100 | 101 | 102 | f2d.loop() 103 | -------------------------------------------------------------------------------- /experiments/Internal_forced/forcedinternal.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | from restart import Restart 6 | from island import Island 7 | 8 | param = Param('default.xml') 9 | param.modelname = 'boussinesq' 10 | param.expname = 'ForcedInternalWave' 11 | 12 | # domain and resolution 13 | ratio = 1 14 | param.ny = 64*2 15 | param.nx = param.ny*ratio 16 | param.Ly = 1. 17 | param.Lx = param.Ly*ratio 18 | param.npx = 1 19 | param.npy = 1 20 | param.geometry = 'closed' 21 | 22 | param.hydroepsilon = 1. 23 | 24 | # time 25 | param.tend = 50. 26 | param.cfl = 0.1 27 | param.adaptable_dt = False 28 | param.dt = 1e-2 29 | param.dtmax = .1 30 | 31 | # discretization 32 | param.order = 5 33 | param.timestepping = 'RK3_SSP' # 'LFAM3' 34 | 35 | # output 36 | param.var_to_save = ['vorticity', 'psi', 'u', 'v', 'buoyancy', 'tracer'] 37 | param.list_diag = ['ke', 'pe', 'energy', 'brms', 'vorticity', 'enstrophy'] 38 | param.freq_plot = 20 39 | param.freq_his = .2 40 | param.freq_diag = .1 41 | 42 | # plot 43 | param.plot_interactive = True 44 | param.plot_var = 'banom' 45 | param.cax = np.asarray([-0.1, 0.1])*1e-1 46 | param.colorscheme = 'imposed' 47 | 48 | # physics 49 | param.noslip = False 50 | param.diffusion = False 51 | param.forcing = True 52 | param.forcing_module = 'forcing' 53 | param.additional_tracer = ['tracer'] 54 | param.generate_mp4 = True 55 | 56 | param.gravity = 1. 57 | 58 | nh = param.nh 59 | 60 | grid = Grid(param) 61 | xr, yr = grid.xr, grid.yr 62 | 63 | # add a topography (mask) 64 | # hb = exp(-(xr/param.Lx-0.2)**2 * 50)*0.35 65 | # grid.msk[(yr < hb)] = 0 66 | # grid.finalize_msk() 67 | 68 | 69 | f2d = Fluid2d(param, grid) 70 | model = f2d.model 71 | 72 | xr, yr = grid.xr, grid.yr 73 | 74 | buoy = model.var.get('buoyancy') 75 | 76 | # control parameters of the experiment 77 | N2 = 1. # Brunt Vaisala frequency squared 78 | 79 | 80 | # stratification 81 | 82 | # continuous 83 | buoy[:, :] = grid.yr * grid.msk * N2 84 | 85 | # exponentially decreasing stratification (promoting breaking) 86 | y = grid.yr/param.Ly 87 | # buoy[:, :] = np.exp(y*4)/np.exp(4)*param.Ly * grid.msk * N2 88 | 89 | nlayers = 30 90 | # layers 91 | #buoy[:, :] = np.floor(grid.yr*nlayers/param.Ly)*(param.Ly/nlayers)*N2*grid.msk 92 | 93 | model.bref[:, :] = buoy 94 | 95 | 96 | model.set_psi_from_vorticity() 97 | 98 | # 2/ set an initial tracer field 99 | 100 | 101 | def vortex(x0, y0, sigma): 102 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2) 103 | y = 2-2./(1+np.exp(-x/sigma)) 104 | return y 105 | 106 | 107 | sigma = 0.01*param.Lx 108 | 109 | state = model.var.get('tracer') 110 | 111 | tracer_config = 1 112 | 113 | if tracer_config == 0: # single localized patch 114 | state[:, :] = 2*vortex(0.3, 0.4, sigma) 115 | 116 | if tracer_config == 1: # tiles 117 | state[:, :] = np.round(xr*6) % 2 + np.round(yr*6) % 2 118 | 119 | if tracer_config == 2: # a closed line 120 | x = xr/param.Lx 121 | y = yr/param.Ly 122 | msk = np.zeros_like(yr) 123 | msk[(x > 0.4) & (x < 0.7) & (y > 0.4) & (y < 0.7)] = 1 124 | z = np.roll(msk, -1, axis=1)+np.roll(msk, -1, axis=0) + \ 125 | np.roll(msk, +1, axis=1)+np.roll(msk, +1, axis=0)-4*msk 126 | state[:, :] = 0. 127 | state[z > 0] = 4. 128 | 129 | 130 | state[:, :] *= grid.msk 131 | 132 | 133 | f2d.loop() 134 | -------------------------------------------------------------------------------- /experiments/Internal_forced/forcing.py: -------------------------------------------------------------------------------- 1 | from param import Param 2 | import numpy as np 3 | 4 | 5 | class Forcing(Param): 6 | """ define the forcing """ 7 | 8 | def __init__(self, param, grid): 9 | 10 | self.list_param = ['deltab', 'dt'] 11 | param.copy(self, self.list_param) 12 | 13 | self.list_param = ['j0', 'npy', 'nh'] 14 | grid.copy(self, self.list_param) 15 | 16 | xr = (grid.xr-param.Lx*.5)/param.Lx 17 | yr = (grid.yr-param.Ly*.5)/param.Ly 18 | 19 | nh = param.nh 20 | self.forc = yr*0. 21 | N2 = 1. 22 | angle = 25. # in degrees 23 | # angle close to 0° => phase propagates horizontally, 24 | # beams are vertical 25 | # angle close to 90° => phase propagates vertically, 26 | # beams are horizontal 27 | self.period = 2*np.pi/(np.sqrt(N2)*np.cos(angle*np.pi/180)) 28 | amplitude = .1 29 | spacing = 0.02 30 | width = .02 31 | y0 = 0. 32 | 33 | def gaussian(x0, y0, delta): 34 | d2 = (xr-x0)**2 + (yr-y0)**2 35 | return np.exp(-d2/(2*delta**2)) 36 | 37 | self.forc[:, :] = (gaussian(-spacing/2, y0, width) 38 | - gaussian(+spacing/2, y0, width)) 39 | self.forc *= grid.msk*amplitude/width 40 | 41 | def add_forcing(self, x, t, dxdt,coef=None): 42 | """ add the forcing term on x[0]=the vorticity """ 43 | dxdt[0] += self.forc * np.sin(2*np.pi*t/self.period) 44 | 45 | # attempt to have a forcing white noise in time 46 | # coef = np.random.uniform()*2-1 47 | # dxdt[0] += self.forc * coef 48 | -------------------------------------------------------------------------------- /experiments/KelvinHelmholtz/kelvin_helmholtz.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'boussinesq' 8 | param.expname = 'khi' 9 | 10 | # domain and resolution 11 | ratio = 2 12 | param.ny = 64*2 13 | param.nx = param.ny*ratio 14 | param.Ly = 1. 15 | param.Lx = 1*ratio 16 | param.npx = 1 17 | param.npy = 1 18 | param.geometry = 'xchannel' 19 | 20 | # time 21 | param.tend = 5. 22 | param.cfl = 1.5 23 | param.adaptable_dt = True 24 | param.dt = 0.01 25 | param.dtmax = 0.02 26 | 27 | # discretization 28 | param.order = 5 29 | param.timestepping = 'RK3_SSP' 30 | 31 | # output 32 | param.var_to_save = ['vorticity', 'psi', 'u', 'v', 'buoyancy', 'banom'] 33 | param.list_diag = 'all' 34 | param.freq_plot = 5 35 | param.freq_his = .25 36 | param.freq_diag = .1 37 | 38 | # plot 39 | param.plot_interactive = True 40 | param.plot_var = 'buoyancy' 41 | param.cax = [-8., 8.] 42 | param.colorscheme = 'imposed' 43 | param.generate_mp4 = True 44 | param.cmap = 'inferno' 45 | 46 | # physics 47 | param.forcing = False 48 | param.noslip = False 49 | param.diffusion = False 50 | param.forcing = False 51 | 52 | param.gravity = 1. 53 | 54 | nh = param.nh 55 | 56 | grid = Grid(param) 57 | 58 | 59 | f2d = Fluid2d(param, grid) 60 | model = f2d.model 61 | 62 | xr, yr = grid.xr, grid.yr 63 | vor = model.var.get('vorticity') 64 | buoy = model.var.get('buoyancy') 65 | 66 | # control parameters of the experiment 67 | N = 4. # Brunt Vaisala frequency squared 68 | S = 20. # vertical shear 69 | 70 | # linear stratification 71 | buoy[:, :] = grid.yr0*grid.msk*N**2 72 | model.bref[:] = buoy 73 | 74 | print('Ri = %4.2f' % (N**2/S**2)) 75 | 76 | # we take the mean jet profile as a tanh 77 | # U = tanh( grid.yr0 * sqrt(S2) ) 78 | # but we control it via the vorticity distribution 79 | 80 | vor[:, :] = -S / np.cosh(grid.yr0 * S)**2.*grid.msk 81 | 82 | 83 | # add noise to trigger the instability 84 | np.random.seed(42) # set the seed to ensure reproducibility 85 | noise = np.random.normal(size=np.shape(yr))*grid.msk 86 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 87 | grid.fill_halo(noise) 88 | 89 | 90 | vor[:, :] += 1e0*noise 91 | 92 | vor *= grid.msk 93 | 94 | 95 | model.set_psi_from_vorticity() 96 | 97 | 98 | f2d.loop() 99 | -------------------------------------------------------------------------------- /experiments/Leewave/leewave.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | from restart import Restart 6 | from island import Island 7 | 8 | param = Param('default.xml') 9 | param.modelname = 'boussinesq' 10 | param.expname = 'LW_0' 11 | 12 | # domain and resolution 13 | ratio = 2 14 | param.ny = 64*2 15 | param.nx = param.ny*ratio 16 | param.Ly = 1. 17 | param.Lx = param.Ly*ratio 18 | param.npx = 1 19 | param.npy = 1 20 | param.geometry = 'xchannel' 21 | 22 | # time 23 | param.tend = 10. 24 | param.cfl = 0.9 25 | param.adaptable_dt = True 26 | param.dt = 1e-2 27 | param.dtmax = 1. 28 | 29 | # discretization 30 | param.order = 5 31 | param.timestepping = 'RK3_SSP' 32 | 33 | # output 34 | param.var_to_save = ['vorticity', 'psi', 'u', 'v', 'buoyancy', 'banom'] 35 | param.list_diag = ['ke', 'pe', 'vorticity', 'enstrophy'] 36 | param.freq_plot = 20 37 | param.freq_his = .5 38 | param.freq_diag = .1 39 | 40 | # plot 41 | param.plot_interactive = True 42 | param.plot_var = 'v' 43 | param.cax = [-.25, .25] 44 | param.colorscheme = 'imposed' 45 | param.generate_mp4 = True 46 | param.plot_psi = True 47 | 48 | # physics 49 | param.forcing = False 50 | param.noslip = False 51 | param.diffusion = False 52 | param.forcing = False 53 | param.isisland = True 54 | 55 | param.gravity = 1. 56 | 57 | nh = param.nh 58 | 59 | 60 | grid = Grid(param) 61 | 62 | # control parameters of the experiment 63 | N2 = 10. # Brunt Vaisala frequency squared 64 | hseamount = 0.1 65 | Lseamount = 0.1 66 | U = .5 67 | 68 | # add a seamount 69 | z = grid.yr-hseamount*np.exp(-(grid.xr0+0.5)**2/(Lseamount**2/2)) 70 | grid.msk[z <= 0] = 0 71 | 72 | 73 | if grid.j0 == param.npy-1: 74 | msk = grid.msk.copy()*0 75 | msk[-nh:-1, :] = 1 76 | idx = np.where(msk == 1) 77 | grid.island.add(idx, -U) 78 | 79 | 80 | grid.finalize_msk() 81 | 82 | 83 | f2d = Fluid2d(param, grid) 84 | model = f2d.model 85 | 86 | xr, yr = grid.xr, grid.yr 87 | 88 | buoy = model.var.get('buoyancy') 89 | 90 | 91 | # linear stratification 92 | buoy[:, :] = grid.yr0*grid.msk*N2 93 | model.bref[:, :] = buoy 94 | 95 | 96 | model.set_psi_from_vorticity() 97 | 98 | 99 | f2d.loop() 100 | -------------------------------------------------------------------------------- /experiments/LockExchange/fdse_lab_exp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Numerical experiment of the gravity current done in the lab 3 | at FDSE, with Paul Billant 4 | """ 5 | from fluid2d import Fluid2d 6 | from param import Param 7 | from grid import Grid 8 | import numpy as np 9 | 10 | param = Param('default.xml') 11 | param.modelname = 'boussinesq' 12 | param.expname = 'gravcurr' 13 | 14 | # domain and resolution 15 | 16 | scaling = 1. 17 | 18 | param.ny = 64 19 | param.nx = 8*param.ny 20 | param.npx = 1 21 | param.npy = 1 22 | param.Lx = 8. 23 | param.Ly = 1. 24 | param.geometry = 'closed' 25 | 26 | # time 27 | param.tend = 15./scaling 28 | param.cfl = .5 29 | param.adaptable_dt = True 30 | param.dt = .1 31 | param.dtmax = .1 32 | 33 | # discretization 34 | param.order = 5 35 | 36 | 37 | # output 38 | param.plot_var = 'buoyancy' 39 | param.var_to_save = ['vorticity', 'buoyancy', 'psi'] 40 | param.list_diag = 'all' 41 | param.freq_his = .1/scaling 42 | param.freq_diag = .1/scaling 43 | 44 | # plot 45 | param.plot_interactive = False 46 | param.freq_plot = 10 47 | param.colorscheme = 'imposed' 48 | param.cax = [0, 3] 49 | param.generate_mp4 = True 50 | 51 | # physics 52 | param.gravity = 1. 53 | param.forcing = False 54 | param.diffusion = False 55 | param.noslip = False 56 | 57 | grid = Grid(param) 58 | param.Kdiff = 2e-3*grid.dx**2 59 | 60 | xr, yr = grid.xr, grid.yr 61 | # add a mask 62 | #hb = np.exp(-(xr/param.Lx-0.5)**2 * 50)*0.1 63 | #grid.msk[(yr < hb)] = 0 64 | grid.finalize_msk() 65 | 66 | f2d = Fluid2d(param, grid) 67 | model = f2d.model 68 | 69 | 70 | rho = model.var.get('buoyancy') 71 | 72 | 73 | # # 74 | # sigma = 3*grid.dx 75 | # buoy[:] = -0.5*(np.tanh( (xr-param.Lx*0.9)/sigma)+1) 76 | # buoy *= grid.msk 77 | 78 | dx = grid.dx 79 | rho[:] = 0.5-0.5*np.tanh((xr-0.85*param.Lx)/(dx/2)) 80 | ycut = np.tanh((yr-param.Lx*0.1)/(dx/2)) 81 | rho[ycut>0] = 1 82 | #rho *= 0.25 83 | 84 | rho *= scaling**2 85 | #model.set_psi_from_vorticity() 86 | 87 | 88 | f2d.loop() 89 | -------------------------------------------------------------------------------- /experiments/LockExchange/lockexchange.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lock exchange experiment 3 | """ 4 | from fluid2d import Fluid2d 5 | from param import Param 6 | from grid import Grid 7 | import numpy as np 8 | 9 | param = Param('default.xml') 10 | param.modelname = 'boussinesq' 11 | param.expname = 'lockexch' 12 | 13 | # domain and resolution 14 | 15 | param.ny = 64 16 | param.nx = param.ny*8 17 | param.npx = 1 18 | param.npy = 1 19 | param.Lx = 8. 20 | param.Ly = 1. 21 | param.geometry = 'closed' 22 | 23 | # time 24 | param.tend = 8. 25 | param.cfl = 1. 26 | param.adaptable_dt = True 27 | param.dt = .1 28 | param.dtmax = .1 29 | 30 | # discretization 31 | param.order = 5 32 | 33 | 34 | # output 35 | param.plot_var = 'buoyancy' 36 | param.var_to_save = ['vorticity', 'buoyancy', 'psi'] 37 | param.list_diag = 'all' 38 | param.freq_his = .2 39 | param.freq_diag = .1 40 | 41 | # plot 42 | param.plot_interactive = True 43 | param.freq_plot = 10 44 | param.colorscheme = 'imposed' 45 | param.cax = [-1, 1] 46 | param.generate_mp4 = False 47 | 48 | # physics 49 | param.gravity = 1. 50 | param.forcing = False 51 | param.diffusion = False 52 | param.noslip = False 53 | # by default hydroepsilon == 1 -> non-hydrostatic case 54 | # set it to value << 1 to switch toward a more hydrostatic case 55 | #param.hydroepsilon = 0.01 56 | 57 | grid = Grid(param) 58 | param.Kdiff = 2e-3*grid.dx**2 59 | 60 | xr, yr = grid.xr, grid.yr 61 | grid.finalize_msk() 62 | 63 | f2d = Fluid2d(param, grid) 64 | model = f2d.model 65 | 66 | 67 | buoy = model.var.get('buoyancy') 68 | 69 | 70 | x0 = param.Lx*0.5 71 | width = 3*grid.dx 72 | buoy[:] = -np.tanh((xr-x0)/width) * grid.msk 73 | 74 | 75 | f2d.loop() 76 | -------------------------------------------------------------------------------- /experiments/QG_channel/channel.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | """ Reentrant channel with prescribed transport through the 7 | channel. The transport is controlled via psi0, the streamfunction at 8 | the North Wall -- psi=0 on the South Wall. An island with psi=psi0/2 9 | is also set in the middle of the channel. Even though there is no 10 | explicit forcing, this setup creates an island wake. Control parameters 11 | include: 12 | 13 | - Rd/L, relative size of the deformation radius beta Rd^2 L / psi0, 14 | - relative importance of the jet speed vs Rossby wave speed """ 15 | 16 | param = Param('default.xml') 17 | param.modelname = 'quasigeostrophic' 18 | param.expname = 'channel' 19 | 20 | # domain and resolution 21 | param.nx = 64*4 22 | param.ny = param.nx/4 23 | param.npy = 1 24 | param.Lx = 4. 25 | param.Ly = param.Lx/4 26 | param.geometry = 'xchannel' 27 | 28 | # time 29 | param.tend = 5000. 30 | param.cfl = 1.2 31 | param.adaptable_dt = True 32 | param.dt = 1. 33 | param.dtmax = 100. 34 | 35 | # discretization 36 | param.order = 5 37 | 38 | 39 | # output 40 | param.var_to_save = ['pv', 'u', 'v', 'psi', 'pvanom'] 41 | param.list_diag = 'all' 42 | param.freq_his = 20 43 | param.freq_diag = 5. 44 | 45 | # plot 46 | param.plot_var = 'pv' 47 | param.freq_plot = 10 48 | a = 0.5 49 | param.cax = [-a, a] 50 | param.plot_interactive = True 51 | param.colorscheme = 'imposed' 52 | param.generate_mp4 = False 53 | 54 | # physics 55 | param.beta = 1. 56 | param.Rd = .1 57 | param.forcing = False 58 | param.forcing_module = 'forcing' # not yet implemented 59 | param.noslip = False 60 | param.diffusion = False 61 | param.isisland = True 62 | 63 | psi0 = -5e-4 # this sets psi on the Northern wall (psi=0 on the Southern wall) 64 | 65 | grid = Grid(param) 66 | 67 | nh = grid.nh 68 | 69 | 70 | def disc(param, grid, x0, y0, sigma): 71 | """ function to set up a circular island 72 | note that this function returns the indices, not the mask """ 73 | xr, yr = grid.xr, grid.yr 74 | r = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2) 75 | return np.where(r <= sigma) 76 | 77 | 78 | sigma = 0.08 79 | idx = disc(param, grid, 0.125*param.Lx, 0.5, sigma) 80 | grid.msk[idx] = 0 81 | grid.msknoslip[idx] = 0 82 | grid.island.add(idx, 0.) 83 | 84 | if grid.j0 == 0: 85 | msk = grid.msk.copy()*0 86 | msk[:nh, :] = 1 87 | idx = np.where(msk == 1) 88 | grid.island.add(idx, -psi0*.5) 89 | 90 | if grid.j0 == param.npy-1: 91 | msk = grid.msk.copy()*0 92 | msk[-nh:-1, :] = 1 93 | idx = np.where(msk == 1) 94 | grid.island.add(idx, psi0*.5) 95 | 96 | # tell the code to deactivate the no-slip along the outer walls 97 | if grid.j0 == 0: 98 | grid.msknoslip[:nh, :] = 1 99 | if grid.j0 == param.npy-1: 100 | grid.msknoslip[-nh:, :] = 1 101 | 102 | param.Kdiff = 0.5e-4*grid.dx 103 | 104 | f2d = Fluid2d(param, grid) 105 | model = f2d.model 106 | 107 | xr, yr = grid.xr, grid.yr 108 | yr0 = grid.yr0 109 | 110 | pv = model.var.get('pv') 111 | 112 | # first, let's put some noise on the pv 113 | np.random.seed(1) # to ensure reproducibility of the results 114 | y = 1e-4*np.random.normal(size=np.shape(pv))*grid.msk 115 | model.ope.fill_halo(y) 116 | pv[:] = y 117 | 118 | # to ensure that psi(y) varies linearly between the South and the North wall 119 | # the pv anomaly needs to be set to the correct value 120 | # since pvanom = d^2psi/dy^2 - Rd^-2 psi 121 | # we see that to have psi(y)=a*y we need to set 122 | # pvanom = -Rd^-2 * psi 123 | # this is what does the next line 124 | pv -= param.Rd**(-2) * psi0*yr0/param.Ly*grid.msk 125 | 126 | # now we add the background planetary pv 'beta*y' 127 | model.add_backgroundpv() 128 | 129 | model.set_psi_from_pv() 130 | 131 | f2d.loop() 132 | -------------------------------------------------------------------------------- /experiments/QGbasic/turbqg.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'quasigeostrophic' 8 | param.expname = 'turb_qg' 9 | 10 | # domain and resolution 11 | param.nx = 64*2 12 | param.ny = 64 13 | param.npy = 1 14 | param.Lx = 2. 15 | param.Ly = param.Lx/2 16 | param.geometry = 'xchannel' 17 | 18 | # time 19 | param.tend = 5000. 20 | param.cfl = 0.6 21 | param.adaptable_dt = True 22 | param.dt = 1. 23 | param.dtmax = 10. 24 | 25 | # discretization 26 | param.order = 5 27 | 28 | # output 29 | param.var_to_save = ['pv', 'psi', 'u', 'v'] 30 | param.list_diag = ['ke', 'pv', 'pv2'] 31 | param.freq_plot = 5 32 | param.freq_his = 10 33 | param.freq_diag = 1 34 | 35 | # plot 36 | param.plot_interactive = True 37 | param.plot_psi = True 38 | param.cmap = 'inferno' 39 | param.plot_var = 'pv' 40 | param.cax = np.array([-1, 1])*.5 41 | param.colorscheme = 'imposed' 42 | 43 | # physics 44 | param.beta = 1. 45 | param.Rd = 50. # 2*grid.dx 46 | param.forcing = False 47 | param.noslip = False 48 | param.diffusion = False 49 | # 50 | # you may activate the forcing and use the forcing below 51 | # it's a white noise forcing with some time correlation 52 | # such forcing is often used in turbulence studies 53 | # param.forcing_module='forcing_euler' 54 | 55 | grid = Grid(param) 56 | param.Kdiff = 5e-4*grid.dx 57 | 58 | f2d = Fluid2d(param, grid) 59 | model = f2d.model 60 | 61 | 62 | xr, yr = grid.xr, grid.yr 63 | pv = model.var.get('pv') 64 | 65 | amplitude = 1. 66 | 67 | # set an initial small scale random vorticity field (white noise) 68 | noise = np.random.normal(size=np.shape(yr))*grid.msk 69 | # noise=model.ope.gmg.generate_random_large_scale_field() 70 | 71 | grid.fill_halo(noise) 72 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 73 | pv[:] = noise*amplitude 74 | 75 | sigma = 2*grid.dx 76 | Bu = (param.Rd / sigma)**2 77 | U = min(1., Bu)*np.abs(amplitude)*sigma 78 | Lbeta = np.sqrt(U/param.beta) 79 | lamb = np.abs(amplitude)/(param.beta*sigma) 80 | Rh = (Lbeta/sigma)**2 81 | 82 | print('='*40) 83 | print('Burger Bu = %f' % Bu) 84 | print('Rhines Rh = %f' % Rh) 85 | print('Lambda La = %f' % lamb) 86 | print('U = %f' % U) 87 | print('Lbeta = %f' % Lbeta) 88 | print('='*40) 89 | 90 | model.add_backgroundpv() 91 | 92 | model.set_psi_from_pv() 93 | f2d.loop() 94 | -------------------------------------------------------------------------------- /experiments/QGbasic/vortex_betaplane.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'quasigeostrophic' 8 | param.expname = 'vortex_beta' 9 | 10 | # domain and resolution 11 | param.nx = 64*2 12 | param.ny = 64 13 | param.npy = 1 14 | param.Lx = 2. 15 | param.Ly = param.Lx/2 16 | param.geometry = 'xchannel' 17 | 18 | # time 19 | param.tend = 10e3 20 | param.cfl = 1. 21 | param.adaptable_dt = True 22 | param.dt = 1. 23 | param.dtmax = 100. 24 | 25 | # discretization 26 | param.order = 5 27 | 28 | 29 | # output 30 | param.var_to_save = ['pv', 'psi'] 31 | param.list_diag = ['ke', 'pv', 'pv2'] 32 | param.freq_his = 50 33 | param.freq_diag = 10 34 | 35 | # plot 36 | param.plot_var = 'pv' 37 | param.freq_plot = 10 38 | a = 0.5 39 | param.cax = [-a, a] 40 | param.plot_interactive = True 41 | param.colorscheme = 'imposed' 42 | param.generate_mp4 = False 43 | 44 | # physics 45 | param.beta = 1. 46 | param.Rd = 0.01 # 2*grid.dx 47 | param.forcing = False 48 | param.noslip = False 49 | param.diffusion = False 50 | 51 | grid = Grid(param) 52 | param.Kdiff = 0.1e-3*grid.dx 53 | 54 | # add an island 55 | # grid.msk[28:32,34:38]=0 56 | # grid.finalize() 57 | 58 | 59 | f2d = Fluid2d(param, grid) 60 | model = f2d.model 61 | 62 | xr, yr = grid.xr, grid.yr 63 | vor = model.var.get('pv') 64 | 65 | 66 | def vortex(param, grid, x0, y0, sigma, vortex_type, ratio=1): 67 | xr, yr = grid.xr, grid.yr 68 | # ratio controls the ellipticity, ratio=1 is a disc 69 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2*ratio**2) 70 | 71 | y = x.copy()*0. 72 | 73 | if vortex_type in ('gaussian', 'cosine', 'step'): 74 | if vortex_type == 'gaussian': 75 | y = np.exp(-x**2/(sigma**2)) 76 | 77 | if vortex_type == 'cosine': 78 | y = np.cos(x/sigma*pi/2) 79 | y[x > sigma] = 0. 80 | 81 | if vortex_type == 'step': 82 | y[x <= sigma] = 1. 83 | else: 84 | print('this kind of vortex (%s) is not defined' % vortex_type) 85 | 86 | return y 87 | 88 | # 2/ set an initial tracer field 89 | 90 | 91 | vtype = 'gaussian' 92 | # vortex width 93 | sigma = 0.08*param.Lx 94 | 95 | vortex_config = 'single' 96 | 97 | if vortex_config == 'single': 98 | vtype = 'gaussian' 99 | sigma = 0.05*param.Lx 100 | amplitude = -0.2 101 | vor[:] = amplitude*vortex(param, grid, 0.5, 0.5, sigma, vtype, ratio=1) 102 | 103 | 104 | model.add_backgroundpv() 105 | 106 | model.set_psi_from_pv() 107 | 108 | f2d.loop() 109 | -------------------------------------------------------------------------------- /experiments/QGbasic/vortex_betaplane_v2.py: -------------------------------------------------------------------------------- 1 | from param import Param 2 | from grid import Grid 3 | from fluid2d import Fluid2d 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'quasigeostrophic' 8 | param.expname = 'vortex_beta_0' 9 | 10 | # domain and resolution 11 | param.nx = 64*4 12 | param.ny = param.nx//2 13 | param.npy = 1 14 | param.Lx = 2. 15 | param.Ly = param.Lx/2 16 | param.geometry = 'xchannel' 17 | 18 | # time 19 | param.tend = 1e4 20 | param.cfl = 0.8 21 | param.adaptable_dt = True 22 | param.dt = 1. 23 | param.dtmax = 100. 24 | 25 | # discretization 26 | param.order = 5 27 | 28 | # output 29 | param.ageostrophic = True 30 | param.var_to_save = ['pv', 'psi', 'u', 'v', 'vorticity', 'ua', 'va'] 31 | param.list_diag = ['ke', 'pv', 'pv2'] 32 | param.freq_his = 2 33 | param.freq_diag = 1 34 | 35 | # plot 36 | param.plot_var = 'pv' 37 | param.freq_plot = 10 38 | a = 0.2 39 | param.cax = [-a, a] 40 | param.plot_interactive = True 41 | param.colorscheme = 'symmetric' 42 | param.generate_mp4 = False 43 | 44 | # physics 45 | param.beta = 1. 46 | param.Rd = 10. # 2*grid.dx 47 | param.forcing = False 48 | param.noslip = False 49 | param.diffusion = False 50 | 51 | grid = Grid(param) 52 | param.Kdiff = 0.1e-3*grid.dx 53 | 54 | # add an island 55 | # grid.msk[28:32,34:38]=0 56 | # grid.finalize() 57 | 58 | 59 | f2d = Fluid2d(param, grid) 60 | model = f2d.model 61 | 62 | xr, yr = grid.xr, grid.yr 63 | vor = model.var.get('pv') 64 | 65 | 66 | def vortex(param, grid, 67 | x0, y0, sigma, 68 | vortex_type, 69 | ratio=1): 70 | 71 | xr, yr = grid.xr, grid.yr 72 | # ratio controls the ellipticity, ratio=1 is a disc 73 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2*ratio**2) 74 | 75 | y = x.copy()*0. 76 | 77 | if vortex_type in ('gaussian', 'cosine', 'step'): 78 | if vortex_type == 'gaussian': 79 | y = np.exp(-x**2/(sigma**2)) 80 | 81 | if vortex_type == 'cosine': 82 | y = np.cos(x/sigma*np.pi/2) 83 | y[x > sigma] = 0. 84 | 85 | if vortex_type == 'step': 86 | y[x <= sigma] = 1. 87 | else: 88 | print('this kind of vortex (%s) is not defined' % vortex_type) 89 | 90 | return y 91 | 92 | 93 | # 2/ set an initial tracer field 94 | vtype = 'gaussian' 95 | # vortex width 96 | sigma = 0.02*param.Lx 97 | amplitude = -.5 98 | 99 | vortex_config = 'dipole' 100 | 101 | if vortex_config == 'single': 102 | vor[:] = amplitude*vortex(param, grid, 0.5, 0.5, sigma, vtype, ratio=1) 103 | 104 | elif vortex_config == 'dipole': 105 | d = 8*sigma/param.Lx 106 | vor[:] += -amplitude*vortex(param, grid, 0.5, 0.5-d/2, sigma, vtype) 107 | vor[:] += +amplitude*vortex(param, grid, 0.5, 0.5+d/2, sigma, vtype) 108 | 109 | Bu = (param.Rd / sigma)**2 110 | U = min(1., Bu)*np.abs(amplitude)*sigma 111 | Lbeta = np.sqrt(U/param.beta) 112 | lamb = np.abs(amplitude)/(param.beta*sigma) 113 | Rh = (Lbeta/sigma)**2 114 | 115 | print('='*40) 116 | print('Burger Bu = %f' % Bu) 117 | print('Rhines Rh = %f' % Rh) 118 | print('Lambda La = %f' % lamb) 119 | print('U = %f' % U) 120 | print('Lbeta = %f' % Lbeta) 121 | print('='*40) 122 | 123 | model.add_backgroundpv() 124 | 125 | model.set_psi_from_pv() 126 | 127 | f2d.loop() 128 | -------------------------------------------------------------------------------- /experiments/QGbasic/vortex_on_topography.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'quasigeostrophic' 8 | param.expname = 'vortex_topo' 9 | 10 | # domain and resolution 11 | ratio = 2 12 | param.nx = 64*4 13 | param.ny = param.nx//ratio 14 | param.npx = 1 15 | param.npy = 1 16 | param.Lx = 2. 17 | param.Ly = param.Lx/ratio 18 | param.geometry = 'perio' 19 | 20 | # time 21 | param.tend = 50. 22 | param.cfl = 0.8 23 | param.adaptable_dt = True 24 | param.dt = 1. 25 | param.dtmax = 100. 26 | 27 | # discretization 28 | param.order = 5 29 | param.flux_splitting_method = 'minmax' 30 | 31 | # output 32 | param.ageostrophic = True 33 | param.var_to_save = ['pv', 'psi', 'u', 'v', 'pvanom', 'vorticity', 34 | 'btorque', 'ua', 'va'] 35 | param.list_diag = ['ke', 'pv', 'pv2'] 36 | param.freq_his = 1 37 | param.freq_diag = .5 38 | 39 | # plot 40 | param.plot_var = 'pv' 41 | param.freq_plot = 10 42 | a = 4. 43 | param.cax = [-a, a] 44 | param.plot_interactive = True 45 | param.plot_psi = True 46 | param.plot_pvback = True 47 | param.colorscheme = 'symmetric' 48 | param.imshow_interpolation = 'bilinear' 49 | param.generate_mp4 = True 50 | 51 | # physics 52 | param.beta = 1. 53 | param.Rd = 0.1 # 2*grid.dx 54 | param.bottom_torque = True 55 | param.forcing = False 56 | param.forcing_module = 'forcing_dipoles' 57 | param.noslip = False 58 | param.diffusion = False 59 | 60 | grid = Grid(param) 61 | param.Kdiff = 0.1e-3*grid.dx 62 | 63 | # add an island 64 | # grid.msk[28:32,34:38]=0 65 | # grid.finalize() 66 | 67 | 68 | f2d = Fluid2d(param, grid) 69 | model = f2d.model 70 | 71 | xr, yr = grid.xr, grid.yr 72 | vor = model.var.get('pv') 73 | 74 | 75 | def vortex(param, grid, 76 | x0, y0, sigma, 77 | vortex_type, 78 | ratio=1): 79 | 80 | xr, yr = grid.xr, grid.yr 81 | # ratio controls the ellipticity, ratio=1 is a disc 82 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2*ratio**2) 83 | 84 | y = x.copy()*0. 85 | 86 | if vortex_type in ('gaussian', 'cosine', 'step'): 87 | if vortex_type == 'gaussian': 88 | y = np.exp(-x**2/(sigma**2)) 89 | 90 | if vortex_type == 'cosine': 91 | y = np.cos(x/sigma*np.pi/2) 92 | y[x > sigma] = 0. 93 | 94 | if vortex_type == 'step': 95 | y[x <= sigma] = 1. 96 | else: 97 | print('this kind of vortex (%s) is not defined' % vortex_type) 98 | 99 | return y 100 | 101 | 102 | # 2/ set an initial tracer field 103 | vtype = 'cosine' 104 | # vortex width 105 | sigma = 0.06*param.Lx 106 | amplitude = -1. 107 | 108 | vortex_config = 'dipole' 109 | topo_config = 'seamount' 110 | 111 | remove_pvtopo = True 112 | 113 | """Set the topography""" 114 | 115 | if topo_config == 'seamount': 116 | # define a seamount 117 | vtype = 'gaussian' 118 | sigma = 0.08*param.Lx 119 | htopo = -vortex(param, grid, 0.2, 0.5, sigma, vtype) 120 | sigma = 0.03*param.Lx 121 | htopo += 3*vortex(param, grid, 0.22, 0.46, sigma, vtype) 122 | 123 | elif topo_config == 'margin': 124 | htopo = 0.3*(1+np.tanh((yr/param.Ly-0.85)/.1)) 125 | 126 | elif topo_config == 'canyon': 127 | steep = 0.02 128 | stepx = 1+0.2*(np.tanh((xr/param.Lx-0.2)/steep) 129 | - np.tanh((xr/param.Lx-0.3)/steep)) 130 | htopo = 0.3*(1+np.tanh((yr/param.Ly+0.5*(stepx-2))/.2)) 131 | 132 | elif topo_config == 'ridge': 133 | htopo = 0.3*(np.exp(-(yr/param.Ly-0.6)**2/(2*.05**2))) 134 | 135 | 136 | """Set the PV distribution""" 137 | 138 | if vortex_config == 'single': 139 | x0, y0 = 0.5, 0.6 140 | vor[:] = amplitude*vortex(param, grid, x0, y0, sigma, vtype, ratio=1) 141 | 142 | elif vortex_config == 'dipole': 143 | d = 4*sigma/param.Lx 144 | x0, y0 = 0.4, 0.5 145 | vor[:] += -amplitude*vortex(param, grid, x0, y0-d/2, sigma, vtype) 146 | vor[:] += +amplitude*vortex(param, grid, x0, y0+d/2, sigma, vtype) 147 | 148 | elif vortex_config == 'dipole2': 149 | d = 4*sigma/param.Lx 150 | vor[:] += -amplitude*vortex(param, grid, 0.5-d/2, 0.2, sigma, vtype) 151 | vor[:] += +amplitude*vortex(param, grid, 0.5+d/2, 0.2, sigma, vtype) 152 | 153 | elif vortex_config == 'jet': 154 | sigma = 0.05 155 | yy = (yr-0.5*param.Ly)/sigma 156 | vor[:] = amplitude*yy*np.exp(-yy**2/2) 157 | 158 | elif vortex_config == 'followtopo': 159 | vor[:] = htopo*amplitude 160 | 161 | elif vortex_config == 'random': 162 | # set an initial small scale random vorticity field (white noise) 163 | noise = np.random.normal(size=np.shape(yr))*grid.msk 164 | # noise=model.ope.gmg.generate_random_large_scale_field() 165 | 166 | grid.fill_halo(noise) 167 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 168 | vor[:] = noise*amplitude*grid.msk 169 | 170 | 171 | if remove_pvtopo: 172 | vor[:] -= htopo 173 | 174 | 175 | Bu = (param.Rd / sigma)**2 176 | U = min(1., Bu)*np.abs(amplitude)*sigma 177 | Lbeta = np.sqrt(U/param.beta) 178 | lamb = np.abs(amplitude)/(param.beta*sigma) 179 | Rh = (Lbeta/sigma)**2 180 | 181 | print('='*40) 182 | print('Burger Bu = %f' % Bu) 183 | print('Rhines Rh = %f' % Rh) 184 | print('Lambda La = %f' % lamb) 185 | print('U = %f' % U) 186 | print('Lbeta = %f' % Lbeta) 187 | print('='*40) 188 | 189 | 190 | # hack the background PV = by-pass the default choice 191 | # (meridional gradient) and set it to htopo 192 | pvback = f2d.model.pvback 193 | pvback[:, :] += htopo 194 | vor += pvback 195 | 196 | f2d.plotting.pvback = pvback 197 | 198 | # default way 199 | # model.add_backgroundpv() 200 | 201 | model.set_psi_from_pv() 202 | 203 | f2d.loop() 204 | -------------------------------------------------------------------------------- /experiments/RayleighBenard/conv_atmos.py: -------------------------------------------------------------------------------- 1 | """ 2 | Convective boundary layer 3 | 4 | ingredients: 5 | 6 | - linear reference buoyancy profile (stable) 7 | - uniform heating from the bottom 8 | - no cooling in the column 9 | 10 | - viscosity and diffusion (taken equal) 11 | """ 12 | from fluid2d import Fluid2d 13 | from param import Param 14 | from grid import Grid 15 | import numpy as np 16 | 17 | param = Param('default.xml') 18 | param.modelname = 'boussinesq' 19 | param.expname = 'RB_128' 20 | 21 | # domain and resolution 22 | param.nx = 128*2 23 | param.ny = 128 24 | param.npx = 1 25 | param.Lx = 2. 26 | param.Ly = 1. 27 | param.geometry = 'xchannel' 28 | 29 | # time 30 | param.tend = 25. 31 | param.cfl = 0.9 # <- 1.0 is too large 32 | 33 | assert param.cfl <= 0.9 34 | 35 | param.adaptable_dt = True 36 | param.dt = 1e2 37 | param.dtmax = 0.2 38 | param.exacthistime = True 39 | 40 | # discretization 41 | param.order = 5 42 | 43 | # output 44 | param.plot_var = 'buoyancy' 45 | param.var_to_save = ['vorticity', 'buoyancy', 'v', 'psi'] 46 | param.list_diag = 'all' 47 | param.freq_his = 0.2 48 | param.freq_diag = 0.2 49 | 50 | # plot 51 | param.plot_interactive = True 52 | param.freq_plot = 10 53 | param.colorscheme = 'imposed' 54 | param.cax = [0, 2] 55 | param.generate_mp4 = False 56 | 57 | # physics 58 | param.gravity = 1. 59 | param.forcing = True 60 | param.forcing_module = 'embedded' 61 | param.diffusion = True 62 | param.noslip = True 63 | 64 | grid = Grid(param) 65 | # Prandtl number is Kvorticity / Kbuoyancy 66 | 67 | prandtl = 1. 68 | 69 | visco = .05*grid.dy 70 | diffus = visco / prandtl 71 | 72 | 73 | param.Kdiff = {} 74 | param.Kdiff['vorticity'] = visco 75 | param.Kdiff['buoyancy'] = diffus 76 | 77 | param.heatflux = 1e-2 78 | 79 | class Forcing: 80 | """ define the forcing """ 81 | 82 | def __init__(self, param, grid): 83 | 84 | self.list_param = ['deltab', 'dt'] 85 | param.copy(self, self.list_param) 86 | 87 | self.list_param = ['j0', 'npy', 'nh'] 88 | grid.copy(self, self.list_param) 89 | 90 | 91 | Q = param.heatflux 92 | nh = param.nh 93 | self.nh = nh 94 | self.forc = np.zeros_like(grid.yr) 95 | 96 | if grid.j0 == 0: 97 | self.forc[nh, :] = +Q 98 | 99 | self.forc *= grid.msk 100 | 101 | # transform the surface flux into a volume flux 102 | self.forc *= (1./grid.dx) 103 | self.dz = grid.dy 104 | self.K = param.Kdiff['buoyancy'] 105 | 106 | def add_forcing(self, x, t, dxdt,coef=1): 107 | """ add the forcing term on x[0]=the vorticity """ 108 | dxdt[4] += self.forc*coef 109 | nh=self.nh 110 | dxdt[4][-nh-1] += self.K*(x[4][-nh-1]-x[4][-nh-2])/self.dz**2*coef 111 | 112 | 113 | f2d = Fluid2d(param, grid) 114 | model = f2d.model 115 | 116 | model.forc = Forcing(param, grid) 117 | 118 | xr, zr = grid.xr, grid.yr 119 | 120 | buoy = model.var.get('buoyancy') 121 | # linear reference stratification 122 | buoy[:] = zr*2 123 | 124 | # add noise to trigger the instability 125 | np.random.seed(42) 126 | noise = np.random.normal(size=np.shape(xr))*grid.msk 127 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 128 | grid.fill_halo(noise) 129 | 130 | buoy += 1e-3*noise 131 | 132 | model.set_psi_from_vorticity() 133 | 134 | f2d.loop() 135 | #print(buoy.shape, nh) 136 | -------------------------------------------------------------------------------- /experiments/RayleighBenard/diag_conv.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import matplotlib.text as text 5 | from netCDF4 import Dataset 6 | from param import Param 7 | 8 | plt.ion() 9 | plt.close("all") 10 | 11 | expname = "RB_128" 12 | 13 | defaultparam = Param("default.xml") 14 | fluid2d_datadir = os.path.expanduser(defaultparam.datadir) 15 | 16 | exp_dir = f"{fluid2d_datadir}/{expname}" 17 | 18 | diag_file = f"{expname}_diag.nc" 19 | his_file = f"{expname}_his.nc" 20 | 21 | with Dataset(f"{exp_dir}/{diag_file}") as nc: 22 | ke = nc.variables["ke"][:] 23 | time_diag = nc.variables["t"][:] 24 | pe = nc.variables["pe"][:] 25 | 26 | with Dataset(f"{exp_dir}/{his_file}") as nc: 27 | attrs = nc.ncattrs() 28 | param ={name: nc.getncattr(name) for name in attrs} 29 | 30 | variables_names = [name for name in nc.variables] 31 | print(f"Variables in {his_file}:") 32 | print(f" {variables_names}") 33 | 34 | x = nc.variables["x"][:] 35 | z = nc.variables["y"][:] 36 | time = nc.variables["t"][:] 37 | deltat = param['freq_his'] 38 | print(f"{len(time)} snapshots from time from: 0 to {time[-1]}, every {deltat}") 39 | 40 | w = nc.variables["v"][:] 41 | b = nc.variables["buoyancy"][:] 42 | 43 | 44 | 45 | kt = 100 46 | fig, ax = plt.subplots(1,3,sharey=True,figsize=(12,5)) 47 | im0=ax[0].contourf(x,z,w[kt]) 48 | ax[0].set_xlabel("X") 49 | ax[0].set_ylabel("Z") 50 | ax[0].set_title(f"w / time={time[kt]:.1f}") 51 | plt.colorbar(im0,ax=ax[0]) 52 | 53 | im1= ax[1].contourf(x,z,b[kt]) 54 | ax[1].set_xlabel("X") 55 | ax[1].set_title(f"buoyancy") 56 | plt.colorbar(im1,ax=ax[1]) 57 | 58 | ax[2].plot(np.mean(b[0],axis=-1),z,"k:") 59 | ax[2].plot(np.mean(b[kt],axis=-1),z) 60 | ax[2].set_xlabel("b") 61 | ax[2].set_title(r"b / horizontal average ") 62 | 63 | 64 | nz = len(z) 65 | wsigma = np.zeros((nz,)) 66 | wplume = np.zeros((nz,)) 67 | for kz in range(nz): 68 | w_level = w[kt,kz] 69 | wsigma[kz] = np.std(w_level) 70 | threshold = 2*wsigma[kz] 71 | wp = w_level[np.abs(w_level)>threshold] 72 | wplume[kz] = np.mean(wp) 73 | plt.figure(2) 74 | plt.plot(wsigma,z,label="std") 75 | plt.plot(wplume,z,label="plume") 76 | plt.ylabel("z") 77 | plt.xlabel("w") 78 | plt.legend() 79 | plt.grid() 80 | 81 | 82 | H_plume = 0.4 # estimated from figure(2) 83 | Lz = param["Ly"] 84 | kz_plume = int((H_plume/Lz)*nz) 85 | 86 | wmax = 0.3 87 | nbins = 100 88 | bins=np.linspace(-wmax,wmax,nbins+1) 89 | plt.figure(3) 90 | plt.hist(w[kt,:kz_plume].ravel(),bins,density=True) 91 | plt.xlabel("w") 92 | plt.ylabel("p.d.f.") 93 | -------------------------------------------------------------------------------- /experiments/RayleighBenard/forcing_rayleigh.py: -------------------------------------------------------------------------------- 1 | from param import Param 2 | 3 | 4 | class Forcing: 5 | """ define the forcing """ 6 | 7 | def __init__(self, param, grid): 8 | 9 | self.list_param = ['deltab', 'dt'] 10 | param.copy(self, self.list_param) 11 | 12 | self.list_param = ['j0', 'npy', 'nh'] 13 | grid.copy(self, self.list_param) 14 | 15 | yr = (grid.yr-param.Ly*.5)/param.Ly 16 | 17 | Q = 1e-2 18 | nh = param.nh 19 | self.forc = yr*0. 20 | 21 | type_forcing = 'coolroof' 22 | #type_forcing = 'horizontalconvection' 23 | 24 | # type_forcing = 'prescribed_buoyancy' 25 | 26 | if type_forcing == 'coolroof': 27 | if grid.j0 == grid.npy-1: 28 | self.forc[-nh-1, :] = -Q 29 | if grid.j0 == 0: 30 | self.forc[nh, :] = +Q 31 | 32 | elif type_forcing == 'spreadcooling': 33 | # bottom heating 34 | if grid.j0 == 0: 35 | self.forc[nh, :] = +Q 36 | # uniform cooling 37 | self.forc -= Q/param.ny 38 | 39 | elif type_forcing == 'horizontalconvection': 40 | if grid.j0 == grid.npy-1: 41 | xr = grid.xr 42 | self.forc[xr < param.Lx/2] = Q 43 | self.forc[xr > param.Lx/2] = -Q 44 | self.forc[:-nh-1, :] = 0. 45 | 46 | self.type_forcing = type_forcing 47 | 48 | self.forc *= grid.msk 49 | 50 | # transform the surface flux into a volume flux 51 | self.forc *= (1./grid.dx) 52 | 53 | def add_forcing(self, x, t, dxdt, coef=1.): 54 | """ add the forcing term on x[0]=the vorticity """ 55 | if self.type_forcing == 'prescribed_buoyancy': 56 | # 57 | # for this experiment you need to use the fixed time step 58 | # whose value is controlled by max(visco,diffus) 59 | # 60 | if self.j0 == self.npy-1: 61 | dxdt[4][-self.nh-1, :] -= (x[4][-self.nh-1, :] 62 | + self.deltab*.5)/self.dt 63 | if self.j0 == 0: 64 | dxdt[4][self.nh, :] -= (x[4][self.nh, :] 65 | - self.deltab*.5)/self.dt 66 | 67 | else: 68 | dxdt[4] += self.forc 69 | dxdt[4] *= coef 70 | -------------------------------------------------------------------------------- /experiments/RayleighBenard/horiz_convection.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'boussinesq' 8 | param.expname = 'HorConv' 9 | 10 | # domain and resolution 11 | param.nx = 64*4 12 | param.ny = param.nx/4 13 | param.npx = 1 14 | param.Lx = 4. 15 | param.Ly = 1. 16 | param.geometry = 'closed' 17 | 18 | # time 19 | param.tend = 4000. 20 | param.cfl = 0.4 21 | param.adaptable_dt = True 22 | param.dt = .1 23 | param.dtmax = .1 24 | param.exacthistime = False 25 | 26 | # discretization 27 | param.order = 5 28 | 29 | # output 30 | param.plot_var = 'buoyancy' 31 | param.var_to_save = ['vorticity', 'buoyancy', 'v', 'psi'] 32 | param.list_diag = 'all' 33 | param.freq_his = 20 34 | param.freq_diag = 1 35 | 36 | # plot 37 | param.plot_interactive = True 38 | param.freq_plot = 50 39 | param.colorscheme = 'imposed' 40 | param.cax = [-.5, .5] 41 | param.generate_mp4 = True 42 | 43 | # physics 44 | param.gravity = 1. 45 | param.forcing = True 46 | #param.forcing_module = 'forcing_rayleigh' 47 | param.diffusion = True 48 | param.noslip = False 49 | 50 | grid = Grid(param) 51 | # Prandtl number is Kvorticity / Kbuoyancy 52 | 53 | prandtl = 1. 54 | 55 | 56 | deltab = 600 # this is directly the Rayleigh number is L=visco=diffus=1 57 | 58 | param.deltab = deltab # make it known to param 59 | 60 | L = param.Ly 61 | visco = .002*grid.dy 62 | diffus = visco / prandtl 63 | 64 | # Rayleigh number is 65 | Ra = deltab * L ** 3 / (visco * diffus) 66 | print('Rayleigh number is %4.1f' % Ra) 67 | 68 | param.Kdiff = {} 69 | param.Kdiff['vorticity'] = visco 70 | param.Kdiff['buoyancy'] = diffus # param.Kdiff['vorticity'] / prandtl 71 | 72 | # time step is imposed by the diffusivity 73 | # param.dt = 0.25*grid.dx**2 / max(visco, diffus) 74 | # param.dtmax = param.dt 75 | # print('dt = %f' % param.dt) 76 | 77 | 78 | class Forcing: 79 | """ define the forcing """ 80 | 81 | def __init__(self, param, grid): 82 | 83 | self.list_param = ['deltab', 'dt'] 84 | param.copy(self, self.list_param) 85 | 86 | self.list_param = ['j0', 'npy', 'nh'] 87 | grid.copy(self, self.list_param) 88 | 89 | yr = (grid.yr-param.Ly*.5)/param.Ly 90 | 91 | Q = 1e-2 92 | nh = param.nh 93 | self.forc = yr*0. 94 | 95 | type_forcing = 'horizontalconvection_v2' 96 | 97 | if type_forcing == 'coolroof': 98 | if grid.j0 == grid.npy-1: 99 | self.forc[-nh-1, :] = -Q 100 | if grid.j0 == 0: 101 | self.forc[nh, :] = +Q 102 | 103 | elif type_forcing == 'spreadcooling': 104 | # bottom heating 105 | if grid.j0 == 0: 106 | self.forc[nh, :] = +Q 107 | # uniform cooling 108 | self.forc -= Q/param.ny 109 | 110 | elif type_forcing == 'horizontalconvection': 111 | if grid.j0 == grid.npy-1: 112 | xr = grid.xr 113 | self.forc[xr < param.Lx/2] = Q 114 | self.forc[xr > param.Lx/2] = -Q 115 | self.forc[:-nh-1, :] = 0. 116 | 117 | elif type_forcing == 'horizontalconvection_v2': 118 | if grid.j0 == grid.npy-1: 119 | xr = grid.xr 120 | x0 = param.Lx*3/4 121 | coef = 1.#0.5/0.8 122 | self.forc[xr < x0] = Q*coef 123 | self.forc[xr > x0] = -Q*coef*3 124 | self.forc[:-nh-1, :] = 0. 125 | 126 | self.type_forcing = type_forcing 127 | 128 | self.forc *= grid.msk 129 | 130 | # transform the surface flux into a volume flux 131 | self.forc *= (1./grid.dx) 132 | 133 | def add_forcing(self, x, t, dxdt, coef=1.): 134 | """ add the forcing term on x[0]=the vorticity """ 135 | if self.type_forcing == 'prescribed_buoyancy': 136 | # 137 | # for this experiment you need to use the fixed time step 138 | # whose value is controlled by max(visco,diffus) 139 | # 140 | if self.j0 == self.npy-1: 141 | dxdt[4][-self.nh-1, :] -= (x[4][-self.nh-1, :] 142 | + self.deltab*.5)/self.dt 143 | if self.j0 == 0: 144 | dxdt[4][self.nh, :] -= (x[4][self.nh, :] 145 | - self.deltab*.5)/self.dt 146 | 147 | else: 148 | dxdt[4] += self.forc 149 | dxdt[4] *= coef 150 | 151 | 152 | 153 | f2d = Fluid2d(param, grid) 154 | model = f2d.model 155 | 156 | if param.forcing: 157 | model.forc = Forcing(param, grid) 158 | 159 | 160 | xr, yr = grid.xr, grid.yr 161 | 162 | buoy = model.var.get('buoyancy') 163 | # add noise to trigger the instability 164 | noise = np.random.normal(size=np.shape(yr))*grid.msk 165 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 166 | grid.fill_halo(noise) 167 | 168 | buoy += 1e-1*noise 169 | 170 | model.set_psi_from_vorticity() 171 | 172 | f2d.loop() 173 | -------------------------------------------------------------------------------- /experiments/RayleighBenard/rayleigh_benard.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'boussinesq' 8 | param.expname = 'RB' 9 | 10 | # domain and resolution 11 | param.nx = 64*2 12 | param.ny = param.nx/4 13 | param.npx = 1 14 | param.Lx = 4. 15 | param.Ly = 1. 16 | param.geometry = 'xchannel' 17 | 18 | # time 19 | param.tend = 40. 20 | param.cfl = 1. 21 | param.adaptable_dt = True 22 | param.dt = .1 23 | param.dtmax = .1 24 | param.exacthistime = False 25 | 26 | # discretization 27 | param.order = 5 28 | param.aparab = 0.02 29 | 30 | # output 31 | param.plot_var = 'buoyancy' 32 | param.var_to_save = ['vorticity', 'buoyancy', 'v', 'psi'] 33 | param.list_diag = 'all' 34 | param.freq_his = 1 35 | param.freq_diag = 1 36 | 37 | # plot 38 | param.plot_interactive = True 39 | param.freq_plot = 10 40 | param.colorscheme = 'imposed' 41 | param.cax = [-.5, .5] 42 | param.generate_mp4 = True 43 | 44 | # physics 45 | param.gravity = 1. 46 | param.forcing = True 47 | param.forcing_module = 'forcing_rayleigh' 48 | param.diffusion = True 49 | param.noslip = True 50 | 51 | grid = Grid(param) 52 | # Prandtl number is Kvorticity / Kbuoyancy 53 | 54 | prandtl = 1. 55 | 56 | 57 | deltab = 600 # this is directly the Rayleigh number is L=visco=diffus=1 58 | 59 | param.deltab = deltab # make it known to param 60 | 61 | L = param.Ly 62 | visco = .002*grid.dy 63 | diffus = visco / prandtl 64 | 65 | # Rayleigh number is 66 | Ra = deltab * L ** 3 / (visco * diffus) 67 | print('Rayleigh number is %4.1f' % Ra) 68 | 69 | param.Kdiff = {} 70 | param.Kdiff['vorticity'] = visco 71 | param.Kdiff['buoyancy'] = diffus # param.Kdiff['vorticity'] / prandtl 72 | 73 | # time step is imposed by the diffusivity 74 | # param.dt = 0.25*grid.dx**2 / max(visco, diffus) 75 | # param.dtmax = param.dt 76 | # print('dt = %f' % param.dt) 77 | 78 | f2d = Fluid2d(param, grid) 79 | model = f2d.model 80 | 81 | xr, yr = grid.xr, grid.yr 82 | 83 | buoy = model.var.get('buoyancy') 84 | # add noise to trigger the instability 85 | noise = np.random.normal(size=np.shape(yr))*grid.msk 86 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 87 | grid.fill_halo(noise) 88 | 89 | buoy += 1e-1*noise 90 | 91 | model.set_psi_from_vorticity() 92 | 93 | f2d.loop() 94 | -------------------------------------------------------------------------------- /experiments/SQG/vortex.py: -------------------------------------------------------------------------------- 1 | from param import Param 2 | from grid import Grid 3 | from fluid2d import Fluid2d 4 | import numpy as np 5 | 6 | param = Param("toto") 7 | param.modelname = 'sqg' 8 | param.expname = 'sqg_vortex' 9 | 10 | # domain and resolution 11 | param.nx = 64*2 12 | param.ny = param.nx 13 | param.npy = 1 14 | param.Lx = 1. 15 | param.Ly = 1. 16 | param.geometry = 'perio' 17 | 18 | # time 19 | param.tend = 40 20 | param.cfl = 0.8 21 | param.adaptable_dt = True 22 | param.dt = 1. 23 | param.dtmax = 100. 24 | 25 | # discretization 26 | param.order = 5 27 | 28 | # output 29 | param.ageostrophic = False 30 | param.var_to_save = ['pv', 'psi', 'u', 'v', 'vorticity'] 31 | param.list_diag = ['pv', 'pv2'] 32 | param.freq_his = 1 33 | param.freq_diag = 1 34 | 35 | # plot 36 | param.plot_var = 'pv' 37 | param.freq_plot = 10 38 | a = 0.2 39 | param.cax = [-a, a] 40 | param.plot_interactive = False 41 | param.colorscheme = 'symmetric' 42 | param.generate_mp4 = False 43 | 44 | # physics 45 | param.beta = 0. 46 | param.Rd = 10. # 2*grid.dx 47 | param.forcing = False 48 | param.noslip = False 49 | param.diffusion = False 50 | 51 | grid = Grid(param) 52 | param.Kdiff = 0.1e-3*grid.dx 53 | 54 | 55 | 56 | f2d = Fluid2d(param, grid) 57 | model = f2d.model 58 | 59 | xr, yr = grid.xr, grid.yr 60 | vor = model.var.get('pv') 61 | 62 | 63 | def vortex(param, grid, 64 | x0, y0, sigma, 65 | vortex_type, 66 | ratio=1): 67 | 68 | xr, yr = grid.xr, grid.yr 69 | # ratio controls the ellipticity, ratio=1 is a disc 70 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2*ratio**2) 71 | 72 | y = x.copy()*0. 73 | 74 | if vortex_type in ('gaussian', 'cosine', 'step'): 75 | if vortex_type == 'gaussian': 76 | y = np.exp(-x**2/(sigma**2)) 77 | 78 | if vortex_type == 'cosine': 79 | y = np.cos(x/sigma*np.pi/2) 80 | y[x > sigma] = 0. 81 | 82 | if vortex_type == 'step': 83 | y[x <= sigma] = 1. 84 | else: 85 | print('this kind of vortex (%s) is not defined' % vortex_type) 86 | 87 | return y 88 | 89 | 90 | # 2/ set an initial tracer field 91 | vtype = 'gaussian' 92 | # vortex width 93 | sigma = 0.1*param.Lx 94 | amplitude = -.5 95 | 96 | vortex_config = 'dipole' 97 | 98 | if vortex_config == 'single': 99 | vor[:] = amplitude*vortex(param, grid, 0.5, 0.5, sigma, vtype, ratio=1) 100 | 101 | elif vortex_config == 'dipole': 102 | d = 3*sigma/param.Lx 103 | vor[:] += +amplitude*vortex(param, grid, 0.5, 0.5-d/2, sigma, vtype) 104 | vor[:] += +amplitude*vortex(param, grid, 0.5, 0.5+d/2, sigma, vtype) 105 | 106 | 107 | model.set_psi_from_pv() 108 | 109 | f2d.loop() 110 | -------------------------------------------------------------------------------- /experiments/ShearInstab/shear_instability.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'euler' 8 | param.expname = 'shear_instab' 9 | 10 | # domain and resolution 11 | param.nx = 64*2*2 12 | param.ny = 64*2 13 | param.Lx = 2. 14 | param.Ly = param.Lx/2 15 | param.geometry = 'xchannel' 16 | 17 | # time 18 | param.tend = 50. 19 | param.cfl = 1.5 20 | param.adaptable_dt = True 21 | param.dt = 1. 22 | param.dtmax = 10. 23 | 24 | # discretization 25 | param.order = 5 26 | 27 | # output 28 | param.var_to_save = ['vorticity', 'psi', 'u', 'v', 'tracer'] 29 | param.list_diag = 'all' 30 | param.freq_plot = 10 31 | param.freq_his = .5 32 | param.freq_diag = .1 33 | 34 | # plot 35 | param.plot_interactive = True 36 | param.plot_var = 'vorticity' 37 | param.plot_psi = False 38 | param.cax = [-1.1, 1.1] 39 | param.colorscheme = 'imposed' 40 | param.generate_mp4 = True 41 | 42 | # physics 43 | param.forcing = False 44 | param.noslip = False 45 | param.diffusion = False 46 | param.additional_tracer = ['tracer'] 47 | 48 | grid = Grid(param) 49 | param.Kdiff = 2e-4*grid.dx 50 | 51 | # it's time to modify the mask and add obstacles if you wish, 0 is land 52 | # grid.msk[:55,35]=0 53 | # grid.msk[:,:4]=0 54 | # grid.finalize_msk() 55 | 56 | f2d = Fluid2d(param, grid) 57 | model = f2d.model 58 | 59 | xr, yr = grid.xr, grid.yr 60 | vor = model.var.get('vorticity') 61 | 62 | 63 | def vortex(x0, y0, sigma): 64 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2) 65 | y = 4./(1+np.exp(x/(sigma/2))) 66 | # y = cos(x/sigma*pi/2) 67 | # y[x>sigma]=0. 68 | return y 69 | 70 | 71 | # 2/ set an initial shear 72 | sigma = 0.07 73 | yy = (yr/param.Ly-0.5) 74 | 75 | # comment/uncomment choice A vs. B vs. C 76 | 77 | # choice A/ corresponding to a gaussian shaped jet 78 | # vor[:] = (yy/sigma)*exp( - (yy/sigma)**2/2 ) 79 | 80 | # choice B/ or a cosine shaped jet 81 | # vor[:] = np.sin(yy*np.pi/sigma) 82 | # vor[abs(yy/sigma) > 1] = 0. 83 | 84 | # choice C/ or a piecewise constant jet 85 | vor[:] = np.sign(yy) 86 | vor[abs(yy/sigma) > 1] = 0. 87 | 88 | # add noise to trigger the instability 89 | np.random.seed(42) 90 | noise = np.random.normal(size=np.shape(yr))*grid.msk 91 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 92 | grid.fill_halo(noise) 93 | 94 | vor += noise*1e-2 95 | 96 | model.set_psi_from_vorticity() 97 | 98 | state = model.var.get('tracer') 99 | state[:] = np.round(xr*6) % 2 + np.round(yr*6) % 2 100 | 101 | f2d.loop() 102 | -------------------------------------------------------------------------------- /experiments/SymmetricInstability/plotting_SI.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | font = {'size': 16} 3 | 4 | matplotlib.use('TkAgg') 5 | matplotlib.rc('font', **font) 6 | 7 | import os 8 | import subprocess 9 | from numpy import nan 10 | import matplotlib.pyplot as plt 11 | from param import Param 12 | from sys import platform as _platform 13 | 14 | 15 | class Plotting(Param): 16 | def __init__(self, param, grid, var, diag): 17 | self.list_param = ['nh', 'cax', 'plot_var', 'colorscheme', 18 | 'Lx', 'Ly', 'expname', 'nx', 'ny', 19 | 'imshow_interpolation', 20 | 'plot_psi', 'cmap', 'plot_pvback', 21 | 'plot_ua', 'nh', 'expdir', 22 | 'generate_mp4', 'myrank', 'modelname'] 23 | 24 | param.copy(self, self.list_param) 25 | 26 | nh = self.nh 27 | self.msk = grid.msk[nh:-nh, nh:-nh] 28 | self.qE = var.get('qE')[nh:-nh, nh:-nh] 29 | self.buoy = var.get('buoyancy')[nh:-nh, nh:-nh] 30 | self.Vjet = var.get('V')[nh:-nh, nh:-nh] 31 | self.vor = var.get('vorticity')[nh:-nh, nh:-nh] 32 | 33 | def create_fig(self, t): 34 | fig, axes = plt.subplots( 35 | nrows=2, ncols=2, figsize=(12, 8), facecolor='white') 36 | for i in range(2): 37 | for j in range(2): 38 | # axes[j, i].hold(True) 39 | axes[j, i].set_ylabel('Y') 40 | axes[j, i].set_xlabel('X') 41 | 42 | self.time_str = 'time = %-6.2f' 43 | self.txt = plt.annotate(self.time_str % t, xy=( 44 | 0.5, .9), xycoords='figure fraction', fontsize=16) 45 | # axes[0,0].set_title( self.time_str%t ) 46 | 47 | self.axes = axes 48 | 49 | plt.ion() 50 | 51 | fs = 16 52 | axes[0, 0].set_title('V', fontsize=fs) 53 | axes[0, 1].set_title('b', fontsize=fs) 54 | axes[1, 0].set_title('Ertel PV', fontsize=fs) 55 | axes[1, 1].set_title(r'$\omega$', fontsize=fs) 56 | 57 | self.im0 = axes[0, 0].imshow(self.Vjet, 58 | vmin=self.cax[0], vmax=self.cax[1], 59 | cmap=plt.get_cmap('jet'), origin='lower', 60 | interpolation='nearest') 61 | 62 | self.im1 = axes[0, 1].imshow(self.buoy, 63 | vmin=self.cax[0], vmax=self.cax[1], 64 | cmap=plt.get_cmap('jet'), origin='lower', 65 | interpolation='nearest') 66 | 67 | self.im2 = axes[1, 0].imshow(self.qE, 68 | vmin=self.cax[0], vmax=self.cax[1], 69 | cmap=plt.get_cmap('jet'), origin='lower', 70 | interpolation='nearest') 71 | 72 | self.im3 = axes[1, 1].imshow(self.vor, 73 | vmin=self.cax[0], vmax=self.cax[1], 74 | cmap=plt.get_cmap('jet'), origin='lower', 75 | interpolation='nearest') 76 | 77 | #cb = plt.colorbar(self.im0) 78 | 79 | fig.show() 80 | fig.canvas.draw() 81 | self.fig = fig 82 | 83 | if self.generate_mp4 and (self.myrank == 0): 84 | canvas_width, canvas_height = fig.canvas.get_width_height() 85 | # Open an ffmpeg process 86 | outf = '%s/%s_%s.mp4' % (self.expdir, self.expname, self.plot_var) 87 | self.mp4file = outf 88 | videoencoder = None 89 | for v in ['avconv', 'ffmpeg']: 90 | if subprocess.call(['which', v], stdout=subprocess.PIPE) == 0: 91 | videoencoder = v 92 | 93 | if videoencoder is None: 94 | print('\n') 95 | print('Neither avconv or ffmpeg was found') 96 | print('Install one of them or set param.generate_mp4 = False') 97 | exit(0) 98 | 99 | cmdstring = (videoencoder, 100 | '-y', '-r', '30', # overwrite, 30fps 101 | # size of image string 102 | '-s', '%dx%d' % (canvas_width, canvas_height), 103 | '-pix_fmt', 'argb', # format 104 | '-f', 'rawvideo', 105 | # tell ffmpeg to expect raw video from the pipe 106 | '-i', '-', 107 | '-vcodec', 'libx264', outf) # output encoding 108 | 109 | devnull = open(os.devnull, 'wb') 110 | self.process = subprocess.Popen(cmdstring, 111 | stdin=subprocess.PIPE, 112 | stdout=devnull, 113 | stderr=devnull) 114 | 115 | def update_fig(self, t, dt, kt): 116 | 117 | self.txt.set_text(self.time_str % t) 118 | 119 | self.im0.set_array(self.Vjet) 120 | self.set_cax(self.Vjet) 121 | self.im0.set_clim(vmin=self.cax[0], vmax=self.cax[1]) 122 | 123 | self.im1.set_array(self.buoy) 124 | self.set_cax(self.buoy) 125 | self.im1.set_clim(vmin=self.cax[0], vmax=self.cax[1]) 126 | 127 | self.im2.set_array(self.qE) 128 | self.set_cax(self.qE) 129 | self.im2.set_clim(vmin=self.cax[0], vmax=self.cax[1]) 130 | 131 | self.im3.set_array(self.vor) 132 | self.set_cax(self.vor) 133 | self.im3.set_clim(vmin=self.cax[0], vmax=self.cax[1]) 134 | 135 | # self.fig.canvas.draw() does not work with MacOSX, instead 136 | # use plt.pause() BUT plt.pause() prevents to use the other windows 137 | # during the animation 138 | if _platform in ["linux", "linux2"]: 139 | self.fig.canvas.draw() 140 | plt.pause(1e-4) 141 | else: 142 | plt.pause(1e-4) 143 | 144 | if self.generate_mp4: 145 | string = self.fig.canvas.tostring_argb() 146 | self.process.stdin.write(string) 147 | 148 | def finalize(self): 149 | # Finish up 150 | if self.generate_mp4: 151 | self.process.communicate() 152 | 153 | def set_cax(self, z): 154 | if self.colorscheme == 'minmax': 155 | self.cax = [min(z.ravel()), max(z.ravel())] 156 | if self.colorscheme == 'symmetric': 157 | mm = max(abs(z.ravel())) 158 | self.cax = [-mm, +mm] 159 | if self.colorscheme == 'imposed': 160 | # self.cax is already defined 161 | pass 162 | -------------------------------------------------------------------------------- /experiments/SymmetricInstability/symmetric_instab.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | import numpy as np 3 | from param import Param 4 | from grid import Grid 5 | from scipy.special import erf 6 | from restart import Restart 7 | from island import Island 8 | 9 | param = Param('default.xml') 10 | param.modelname = 'thermalwind' 11 | param.expname = 'SI' 12 | 13 | 14 | # domain and resolution 15 | ratio = 2 16 | param.ny = 64*2 17 | param.nx = param.ny*ratio 18 | param.Ly = 1. 19 | param.Lx = param.Ly*ratio 20 | param.npx = 1 21 | param.npy = 1 22 | param.geometry = 'closed' 23 | 24 | # time 25 | param.tend = 150. 26 | param.cfl = 1.0 27 | param.adaptable_dt = True 28 | param.dt = .1 29 | param.dtmax = 2. 30 | 31 | # discretization 32 | param.order = 3 33 | param.timestepping = 'RK3_SSP' 34 | 35 | # output 36 | param.var_to_save = ['vorticity', 'psi', 37 | 'V', 'buoyancy', 'qE', 'tracer'] # qE is Ertel PV 38 | param.list_diag = 'all' 39 | param.freq_plot = 4 40 | param.freq_his = 5. 41 | param.freq_diag = .1 42 | param.diag_fluxes = True 43 | 44 | # plot 45 | param.generate_mp4 = True 46 | param.plot_interactive = True 47 | param.plot_var = 'qE' 48 | param.cax = np.asarray([-0.1, .1])*2 49 | param.colorscheme = 'imposed' 50 | # comment param.plotting_module to restore the default plotting 51 | param.plotting_module = 'plotting_SI' 52 | 53 | # physics 54 | param.forcing = False 55 | param.noslip = False 56 | param.diffusion = False 57 | param.forcing = False 58 | param.generate_mp4 = True 59 | 60 | param.gravity = 1. 61 | param.f0 = 0.1 62 | param.additional_tracer = ['tracer'] 63 | 64 | 65 | nh = param.nh 66 | 67 | grid = Grid(param) 68 | 69 | param.Kdiff = 5e-1*grid.dx**2 70 | 71 | f2d = Fluid2d(param, grid) 72 | model = f2d.model 73 | 74 | xr, yr = grid.xr, grid.yr 75 | vor = model.var.get('vorticity') 76 | buoy = model.var.get('buoyancy') 77 | V = model.var.get('V') 78 | qE = model.var.get('qE') 79 | trac = model.var.get('tracer') 80 | 81 | sigma = 0.2 82 | delta = .2 83 | V0 = .2 # jet speed 84 | 85 | N2 = .15 # vertical buoyancy gradient 86 | delta = .4 87 | sigma = .4 88 | 89 | # horizontal buoyancy gradient (with the gaussian jet below) 90 | M2 = param.f0*V0/delta 91 | 92 | # condition for SI, q>0 (Stone 1966, Taylor-Ferrari 2009) 93 | q = M2**2/param.f0**2 - N2 94 | 95 | # geostrophic Richardson number (Whitt & Thomas 2013) 96 | Rig = param.f0**2 * N2 / M2**2 97 | 98 | # Richardson angle (Thomas et al 2012) 99 | phiRiB = np.arctan(- 1/Rig)*180/np.pi 100 | print('Richardson angle : %g' % phiRiB) 101 | 102 | x = (grid.xr0/sigma) 103 | 104 | # derivative of erf(x) is exp(-x^2) * 2 / sqrt(pi) 105 | 106 | # thermal wind balance reads \partial_x b - f \partial_z V = 0 (if V is along y) 107 | 108 | # background stratification 109 | z = grid.yr0/param.Ly 110 | 111 | bback = N2*z # <- uniform stratification 112 | #bback[z<0] = 10*N2*z[z<0] 113 | 114 | config = 'symmetric' 115 | 116 | if config == 'centrifugal_surface': 117 | # surface intensified jet 118 | z = (grid.yr-param.Ly)/delta 119 | V[:, :] = V0*np.exp(-x**2) * np.exp(z) 120 | buoy[:, :] = V0*(sigma/delta)*(param.f0) * erf(x) * \ 121 | (np.sqrt(pi)/2) * np.exp(z) + bback 122 | 123 | elif config == 'centrifugal_interior': 124 | # it's centrifugal instability because the vertical component of the vorticity is negative (Thomas et al 2012) 125 | 126 | # interior jet (with max speed at mid-depth) 127 | z = (grid.yr-param.Ly*.5)/delta 128 | V[:, :] = V0*np.exp(-x**2) * np.exp(-z**2) 129 | buoy[:, :] = V0*(sigma/delta)*(param.f0) * erf(x) * \ 130 | (np.sqrt(np.pi)/2) * (-2*z)*np.exp(-z**2) + bback 131 | 132 | elif config == 'noname': 133 | z = (grid.yr-param.Ly*.5)/delta 134 | # V[:, :] = V0*np.tanh(x) * exp(-z**2) 135 | # buoy[:, :] = V0 * np.log(np.cosh(x)) * (-2*z)*exp(-z**2) + bback 136 | V[:, :] = V0/np.cosh(x)**2 * np.exp(-z**2) 137 | buoy[:, :] = (V0*param.f0) * np.tanh(x) * (-2*z)*np.exp(-z**2) + bback 138 | 139 | elif config == 'symmetric': 140 | z = grid.yr0/delta 141 | x = (grid.xr0)/sigma 142 | alpha = 1. 143 | V[:, :] = V0*(1+np.tanh(z))*(1-alpha*2*x*np.exp(-x**2)) 144 | buoy[:, :] = (V0*param.f0) / np.cosh(z)**2 * \ 145 | (x+alpha*np.exp(-x**2)) + bback 146 | 147 | elif config == 'noname2': 148 | def phi(x_, z_): 149 | return np.exp(-.5*(x_+z_)**2) 150 | z = grid.yr0 151 | x = grid.xr0 152 | eps = 0.1*grid.dx 153 | 154 | V0 = .5 155 | V[:, :] = V0*(phi(x+eps, z)-phi(x-eps, z))/(2*eps) + 0.5*x 156 | buoy[:, :] = (V0*param.f0)*(phi(x, z+eps)-phi(x, z-eps))/(2*eps)+bback 157 | 158 | else: 159 | z = grid.yr0/delta 160 | x = grid.xr0/sigma 161 | alpha = 5. 162 | phi = x + alpha*z 163 | V[:, :] = V0*phi 164 | buoy[:, :] = (V0*param.f0) * (alpha+0.5*x**2) + bback 165 | 166 | V *= grid.msk 167 | buoy *= grid.msk 168 | 169 | 170 | vor[:, :] = 0. 171 | 172 | # add noise to trigger the instability 173 | np.random.seed(1) 174 | noise = np.random.normal(size=np.shape(yr))*grid.msk 175 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 176 | grid.fill_halo(noise) 177 | 178 | vor[:, :] += 1e-1*noise*grid.msk 179 | 180 | vor *= grid.msk 181 | 182 | 183 | model.set_psi_from_vorticity() 184 | 185 | model.compute_pv() 186 | # set the tracer equals to initial PV! 187 | trac[:, :] = qE 188 | 189 | f2d.loop() 190 | -------------------------------------------------------------------------------- /experiments/TaylorInstability/taylor_instability.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'boussinesq' 8 | param.expname = 'taylorinst' 9 | 10 | # domain and resolution 11 | param.nx = 64*2 12 | 13 | param.ny = param.nx 14 | param.npx = 1 15 | param.npy = 1 16 | param.Lx = 1. 17 | param.Ly = param.Lx 18 | param.geometry = 'xchannel' 19 | 20 | # time 21 | param.tend = 10. 22 | param.cfl = 1.5 23 | param.adaptable_dt = True 24 | param.dt = 0.004 25 | param.dtmax = 1e-2 26 | 27 | # discretization 28 | param.order = 5 29 | 30 | # output 31 | param.plot_var = 'buoyancy' 32 | param.var_to_save = ['vorticity', 'buoyancy', 'psi'] 33 | param.list_diag = 'all' 34 | param.freq_his = 1. 35 | param.freq_diag = 1. 36 | 37 | # plot 38 | param.plot_interactive = True 39 | param.freq_plot = 10 40 | param.colorscheme = 'imposed' 41 | param.cax = [-.6, .6] 42 | param.generate_mp4 = True 43 | 44 | # physics 45 | param.gravity = 1. 46 | param.forcing = False 47 | param.diffusion = False 48 | param.noslip = False 49 | 50 | grid = Grid(param) 51 | 52 | xr, yr = grid.xr, grid.yr 53 | Lx, Ly = param.Lx, param.Ly 54 | 55 | # to set a wall with a small aperture, uncomment 56 | # grid.msk[((xr < 0.4*Lx) | (xr > 0.6*Lx)) & 57 | # (yr > 0.5-grid.dx) & (yr < 0.5+grid.dx)] = 0 58 | # grid.msknoslip = grid.msk.copy() 59 | # grid.finalize_msk() 60 | 61 | param.Kdiff = 2e-3*grid.dx 62 | 63 | 64 | f2d = Fluid2d(param, grid) 65 | model = f2d.model 66 | 67 | xr, yr = grid.xr, grid.yr 68 | buoy = model.var.get('buoyancy') 69 | 70 | 71 | def sigmoid(x, delta): 72 | return 1/(1+np.exp(-(x-0.5)/delta)) 73 | 74 | 75 | def stratif(): 76 | sigma = grid.dx/2 # width of the interface 77 | b = sigmoid(yr/param.Ly, sigma/param.Lx) 78 | return b * grid.msk 79 | 80 | 81 | buoy[:] = (1-stratif() - 0.5)*grid.msk 82 | # add noise to trigger the instability 83 | noise = np.random.normal(size=np.shape(yr), scale=1.)*grid.msk 84 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 85 | grid.fill_halo(noise) 86 | 87 | buoy += 1e-3*noise 88 | 89 | model.set_psi_from_vorticity() 90 | 91 | 92 | f2d.loop() 93 | -------------------------------------------------------------------------------- /experiments/Twodim_turbulence/freedecay/freedecay.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'euler' 8 | param.expname = 'freedecay' 9 | 10 | # domain and resolution 11 | param.nx = 64*2 12 | param.ny = param.nx 13 | param.Ly = param.Lx 14 | param.npx = 1 15 | param.npy = 1 16 | param.geometry = 'perio' 17 | 18 | # time 19 | param.tend = 1200. 20 | param.cfl = 1.2 21 | param.adaptable_dt = True 22 | param.dt = .05 23 | param.dtmax = 10. 24 | 25 | # discretization 26 | param.order = 5 27 | 28 | # output 29 | param.var_to_save = ['vorticity', 'psi', 'tracer'] 30 | param.list_diag = 'all' 31 | param.freq_plot = 5 32 | param.freq_his = 10 33 | param.freq_diag = 1 34 | param.exacthistime = False 35 | 36 | # plot 37 | param.plot_interactive = True 38 | param.plot_var = 'vorticity' 39 | param.plot_psi = False 40 | param.cmap = 'RdYlBu_r' # 'Spectral' 41 | # change the color scale if you change the magnitude of the vorticity 42 | param.cax = np.array([-1, 1])*.25 43 | param.colorscheme = 'imposed' 44 | param.generate_mp4 = True 45 | 46 | # physics 47 | param.forcing = False 48 | param.noslip = False 49 | param.diffusion = False 50 | # 51 | # add a passive tracer 52 | param.additional_tracer = ['tracer'] 53 | 54 | grid = Grid(param) 55 | param.Kdiff = 5e-4*grid.dx 56 | 57 | f2d = Fluid2d(param, grid) 58 | model = f2d.model 59 | 60 | 61 | xr, yr = grid.xr, grid.yr 62 | vor = model.var.get('vorticity') 63 | trac = model.var.get('tracer') 64 | 65 | # set an initial small scale random vorticity field (white noise) 66 | np.random.seed(42) 67 | 68 | 69 | def set_x_and_k(n, L): 70 | k = ((n//2+np.arange(n)) % n) - n//2 71 | return (np.arange(n)+0.5)*L/n, 2*np.pi*k/L 72 | 73 | 74 | _, kx = set_x_and_k(param.nx, np.pi) 75 | _, ky = set_x_and_k(param.ny, np.pi) 76 | kkx, kky = np.meshgrid(kx, ky) 77 | kk = np.sqrt(kkx**2 + kky**2) 78 | 79 | # k0 is the wave-number of the spectral peak 80 | k0 = param.nx*0.48 81 | dk = 1 82 | 83 | phase = np.random.normal(size=(param.ny, param.nx))*2*np.pi 84 | hnoise = np.exp(-(kk-k0)**2/(2*dk))*np.exp(1j*phase) 85 | 86 | noise = np.zeros_like(vor) 87 | nh = grid.nh 88 | noise[nh:-nh, nh:-nh] = 1e3*np.real(np.fft.ifft2(hnoise)) 89 | 90 | 91 | grid.fill_halo(noise) 92 | 93 | magnitude = 1. # change the magnitude here 94 | vor[:] = magnitude * noise # also change param.cax, the color axis 95 | 96 | # modifiy slightly the vorticity, on just one grid point! 97 | # vor[10, 10] += 1. 98 | 99 | # initialize the passive tracer with square tiles 100 | # better idea? => do it, but watch out the syntax 101 | # trac[:] = blabla (a 2D array) 102 | trac[:] = np.round(xr*6) % 2 + np.round(yr*6) % 2 103 | 104 | 105 | model.set_psi_from_vorticity() 106 | model.diagnostics(model.var, 0) 107 | energy = model.diags['ke'] 108 | print('energy=', energy) 109 | # uncomment below to rescale the vorticity with an intrinsic time scale 110 | # vor = vor*param.Lx/np.sqrt(energy) 111 | model.set_psi_from_vorticity() 112 | f2d.loop() 113 | -------------------------------------------------------------------------------- /experiments/Twodim_turbulence/spectrum_analysis.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import matplotlib.text as text 5 | from netCDF4 import Dataset 6 | from param import Param 7 | import matplotlib as mpl 8 | 9 | plt.ion() 10 | plt.close("all") 11 | 12 | 13 | mpl.rcParams['image.cmap'] = 'RdBu_r' 14 | mpl.rcParams['figure.figsize'] = [12.0, 8.0] 15 | mpl.rcParams['figure.dpi'] = 80 16 | mpl.rcParams['savefig.dpi'] = 100 17 | 18 | mpl.rcParams['font.size'] = 14 19 | mpl.rcParams['legend.fontsize'] = 'large' 20 | mpl.rcParams['figure.titlesize'] = 'medium' 21 | 22 | mpl.rcParams['lines.linewidth'] = 2 23 | mpl.rcParams['image.origin'] = 'lower' 24 | mpl.rcParams['image.interpolation'] = 'nearest' 25 | 26 | # set your experiment name here 27 | expname = "turb2d_forced_ls" # "turb2d_forced_ss", "freedecay" 28 | 29 | defaultparam = Param("default.xml") 30 | fluid2d_datadir = os.path.expanduser(defaultparam.datadir) 31 | 32 | exp_dir = f"{fluid2d_datadir}/{expname}" 33 | 34 | diag_file = f"{expname}_diag.nc" 35 | his_file = f"{expname}_his.nc" 36 | 37 | with Dataset(f"{exp_dir}/{diag_file}") as nc: 38 | ke = nc.variables["ke"][:] 39 | time_diag = nc.variables["t"][:] 40 | 41 | with Dataset(f"{exp_dir}/{his_file}") as nc: 42 | attrs = nc.ncattrs() 43 | param = {name: nc.getncattr(name) for name in attrs} 44 | 45 | variables_names = [name for name in nc.variables] 46 | print(f"Variables in {his_file}:") 47 | print(f" {variables_names}") 48 | 49 | x = nc.variables["x"][:] 50 | y = nc.variables["y"][:] 51 | time = nc.variables["t"][:] 52 | deltat = param['freq_his'] 53 | print( 54 | f"{len(time)} snapshots from time from: 0 to {time[-1]}, every {deltat}") 55 | 56 | psi = nc.variables["psi"][:] 57 | vor = nc.variables["vorticity"][:] 58 | 59 | 60 | def spec2d(nx, kk, phi, alpha=0): 61 | hphi = np.abs(np.fft.fft2(phi))**2 62 | if alpha > 0: 63 | hphi *= kk**alpha 64 | spec = np.zeros((nx//2,)) 65 | for i in range(nx//2): 66 | spec[i] = np.sum(hphi[(kk >= i) & (kk < (i+1))]) 67 | return spec 68 | 69 | 70 | def set_x_and_k(n, L): 71 | k = ((n//2+np.arange(n)) % n) - n//2 72 | return (np.arange(n)+0.5)*L/n, 2*np.pi*k/L 73 | 74 | 75 | ny, nx = np.shape(psi[0]) 76 | _, kx = set_x_and_k(nx, 2*np.pi) 77 | _, ky = set_x_and_k(ny, 2*np.pi) 78 | kkx, kky = np.meshgrid(kx, ky) 79 | kk = np.sqrt(kkx**2 + kky**2) 80 | 81 | 82 | # In the cell below we generate a random field with a very peaked power spectrum density (peak at $k_0$, width $dk$). The field has a white noise random phase and a gaussian distribution in $k$. 83 | # 84 | # This how the initial conditions and the forcing are generated in the Fluid2d scripts 85 | 86 | 87 | # change these values and observe the resulting random field 88 | k0 = nx//2 # peak 89 | dk = .1 # width 90 | 91 | fig, ax = plt.subplots(1, 2) 92 | # white noise phase 93 | phase = np.random.normal(size=(ny, nx))*2*np.pi 94 | # gaussian distribution 95 | hnoise = np.exp(-(kk-k0)**2/(2*dk**2))*np.exp(1j*phase) 96 | noise = np.real(np.fft.ifft2(hnoise)) 97 | ax[0].imshow(noise) 98 | ax[0].set_title('Random field (k=%.0f / dk=%.1f)' % (k0, dk)) 99 | ax[1].set_xlabel('X') 100 | ax[1].set_ylabel('Y') 101 | 102 | h = spec2d(nx, kk, noise) 103 | ax[1].loglog(h) 104 | ax[1].set_xlabel('wavenumber k') 105 | ax[1].set_ylabel('power spectrum density') 106 | plt.tight_layout() 107 | 108 | 109 | # let's plot the Kinetic Energy spectrum 110 | # in Fourier space KE = k^2 |psi|^2 * 0.5 111 | # because |u| = k |psi| 112 | plt.figure(figsize=(12, 8)) 113 | for k, t in enumerate(time[::4]): 114 | h = spec2d(nx, kk, psi[k], alpha=2) 115 | plt.loglog(h, label='t=%.0f' % t) 116 | plt.axis([1, 100, 1e-3, 1e4]) 117 | plt.grid() 118 | k = np.arange(1., nx//2) 119 | plt.plot(k, 2e2 * k**(-3), color='k') 120 | # the coefficient set the vertical position of the line 121 | plt.plot(k, 4e2 * k**(-5./3), color='k') 122 | plt.text(50, 10, r'$k^{-5/3}$', fontsize=16) 123 | plt.text(10, .01, r'$k^{-3}$', fontsize=16) 124 | plt.legend() 125 | plt.xlabel('k') 126 | plt.ylabel('P.S.D.') 127 | 128 | 129 | plt.figure() 130 | # we plot here a snapshot of vorticity 131 | plt.imshow(vor[-1], extent=[0, 1, 0, 1]) 132 | plt.xlabel('X') 133 | plt.ylabel('Y') 134 | plt.title('vorticity (t = %.0f)' % time[-1]) 135 | plt.colorbar() 136 | # plt.savefig('turb2d.png') 137 | -------------------------------------------------------------------------------- /experiments/Twodim_turbulence/turb2d_forced/energy_cascade.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'euler' 8 | param.expname = 'turb2d_forced_ss' 9 | 10 | # domain and resolution 11 | param.nx = 64*2 12 | param.ny = param.nx 13 | param.Ly = param.Lx 14 | param.npx = 1 15 | param.npy = 1 16 | param.geometry = 'perio' 17 | 18 | # time 19 | param.tend = 500. 20 | param.cfl = 1.2 21 | param.adaptable_dt = True 22 | param.dt = .05 23 | param.dtmax = 10. 24 | 25 | # discretization 26 | param.order = 5 27 | 28 | # output 29 | param.var_to_save = ['vorticity', 'psi', 'tracer'] 30 | param.list_diag = 'all' 31 | param.freq_plot = 5 32 | param.freq_his = 20 33 | param.freq_diag = 10 34 | param.exacthistime = True 35 | 36 | # plot 37 | param.plot_interactive = True 38 | param.plot_var = 'vorticity' 39 | param.plot_psi = False 40 | param.cmap = 'Spectral' 41 | param.cax = np.array([-1, 1])*.3 42 | param.colorscheme = 'imposed' 43 | param.generate_mp4 = True 44 | 45 | # physics 46 | param.forcing = True 47 | param.forcing_module = 'embedded' 48 | param.tau = 1. 49 | param.k0 = 0.5*param.nx 50 | param.noslip = False 51 | param.diffusion = False 52 | param.decay = False 53 | # 54 | # add a passive tracer 55 | param.additional_tracer = ['tracer'] 56 | 57 | 58 | class Forcing(Param): 59 | """ define the forcing """ 60 | 61 | def __init__(self, param, grid): 62 | self.list_param = ['sizevar', 'tau', 'k0'] 63 | param.copy(self, self.list_param) 64 | 65 | self.list_param = ['nh', 'msk', 'fill_halo', 'nx', 'ny', 66 | 'domain_integration', 'area'] 67 | grid.copy(self, self.list_param) 68 | 69 | def set_x_and_k(n, L): 70 | k = ((n//2+np.arange(n)) % n) - n//2 71 | return (np.arange(n)+0.5)*L/n, 2*np.pi*k/L 72 | 73 | self.t0 = 0. 74 | 75 | _, kx = set_x_and_k(param.nx, np.pi) 76 | _, ky = set_x_and_k(param.ny, np.pi) 77 | kkx, kky = np.meshgrid(kx, ky) 78 | self.kk = np.sqrt(kkx**2 + kky**2) 79 | 80 | k0 = self.k0 81 | dk = .1 82 | 83 | # the forcing has a gaussian spectrum 84 | self.amplitude = 1e3*np.exp(-(self.kk-k0)**2/(2*dk**2)) 85 | self.forc = np.zeros(self.sizevar) 86 | 87 | def add_forcing(self, x, t, dxdt): 88 | """ add the forcing term on x[0]=the vorticity """ 89 | dt = t-self.t0 90 | self.t0 = t 91 | gamma = dt/self.tau 92 | nh = self.nh 93 | 94 | # white noise phase 95 | phase = np.random.normal(size=(self.ny, self.nx))*2*np.pi 96 | hnoise = self.amplitude*np.exp(1j*phase) 97 | 98 | forc = np.real(np.fft.ifft2(hnoise)) 99 | # time filter to introduce some time correlation in the forcing 100 | self.forc[nh:-nh, nh:-nh] = gamma*forc + \ 101 | self.forc[nh:-nh, nh:-nh]*(1-gamma) 102 | self.fill_halo(self.forc) 103 | # impose zero net vorticity (otherwise the solver does not converge) 104 | self.forc -= self.domain_integration(self.forc) / self.area 105 | 106 | # add the forcing to the vorticity tendency 107 | dxdt[0] += self.forc 108 | 109 | 110 | grid = Grid(param) 111 | param.Kdiff = 5e-4*grid.dx 112 | 113 | f2d = Fluid2d(param, grid) 114 | model = f2d.model 115 | model.forc = Forcing(param, grid) 116 | 117 | xr, yr = grid.xr, grid.yr 118 | vor = model.var.get('vorticity') 119 | trac = model.var.get('tracer') 120 | 121 | # set an initial small scale random vorticity field (white noise) 122 | np.random.seed(42) 123 | 124 | 125 | def set_x_and_k(n, L): 126 | k = ((n//2+np.arange(n)) % n) - n//2 127 | return (np.arange(n)+0.5)*L/n, 2*np.pi*k/L 128 | 129 | 130 | _, kx = set_x_and_k(param.nx, np.pi) 131 | _, ky = set_x_and_k(param.ny, np.pi) 132 | kkx, kky = np.meshgrid(kx, ky) 133 | kk = np.sqrt(kkx**2 + kky**2) 134 | 135 | k0 = param.k0 136 | dk = 1 137 | 138 | phase = np.random.normal(size=(param.ny, param.nx))*2*np.pi 139 | 140 | hnoise = np.exp(-(kk-k0)**2/(2*dk**2))*np.exp(1j*phase) 141 | 142 | noise = np.zeros_like(vor) 143 | nh = grid.nh 144 | noise[nh:-nh, nh:-nh] = 1e3*np.real(np.fft.ifft2(hnoise)) 145 | 146 | grid.fill_halo(noise) 147 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 148 | vor[:] = noise*grid.msk 149 | 150 | # initialize the passive tracer with square tiles (better idea?) 151 | trac[:] = np.round(xr*6) % 2 + np.round(yr*6) % 2 152 | 153 | 154 | model.set_psi_from_vorticity() 155 | model.diagnostics(model.var, 0) 156 | energy = model.diags['ke'] 157 | print('energy=', energy) 158 | vor = vor*param.Lx/np.sqrt(energy) 159 | vor = vor/1e3 160 | model.set_psi_from_vorticity() 161 | f2d.loop() 162 | -------------------------------------------------------------------------------- /experiments/Twodim_turbulence/turb2d_forced/enstrophy_cascade.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'euler' 8 | param.expname = 'turb2d_forced_ls' 9 | 10 | # domain and resolution 11 | param.nx = 64*2 12 | param.ny = param.nx 13 | param.Ly = param.Lx 14 | param.npx = 1 15 | param.npy = 1 16 | param.geometry = 'perio' 17 | 18 | # time 19 | param.tend = 500. 20 | param.cfl = 1.2 21 | param.adaptable_dt = True 22 | param.dt = .05 23 | param.dtmax = 10. 24 | 25 | # discretization 26 | param.order = 5 27 | 28 | # output 29 | param.var_to_save = ['vorticity', 'psi', 'tracer'] 30 | param.list_diag = 'all' 31 | param.freq_plot = 5 32 | param.freq_his = 20 33 | param.freq_diag = 10 34 | param.exacthistime = True 35 | 36 | # plot 37 | param.plot_interactive = True 38 | param.plot_var = 'vorticity' 39 | param.plot_psi = False 40 | param.cmap = 'Spectral' 41 | param.cax = np.array([-1, 1])*.3 42 | param.colorscheme = 'imposed' 43 | param.generate_mp4 = True 44 | 45 | # physics 46 | param.forcing = True 47 | param.forcing_module = 'embedded' 48 | param.tau = 1. 49 | param.k0 = 3. 50 | param.noslip = False 51 | param.diffusion = False 52 | param.decay = False 53 | # 54 | # add a passive tracer 55 | param.additional_tracer = ['tracer'] 56 | 57 | 58 | class Forcing(Param): 59 | """ define the forcing """ 60 | 61 | def __init__(self, param, grid): 62 | self.list_param = ['sizevar', 'tau', 'k0'] 63 | param.copy(self, self.list_param) 64 | 65 | self.list_param = ['nh', 'msk', 'fill_halo', 'nx', 'ny', 66 | 'domain_integration', 'area'] 67 | grid.copy(self, self.list_param) 68 | 69 | def set_x_and_k(n, L): 70 | k = ((n//2+np.arange(n)) % n) - n//2 71 | return (np.arange(n)+0.5)*L/n, 2*np.pi*k/L 72 | 73 | self.t0 = 0. 74 | 75 | _, kx = set_x_and_k(param.nx, np.pi) 76 | _, ky = set_x_and_k(param.ny, np.pi) 77 | kkx, kky = np.meshgrid(kx, ky) 78 | self.kk = np.sqrt(kkx**2 + kky**2) 79 | 80 | k0 = self.k0 81 | dk = .1 82 | 83 | # the forcing has a gaussian spectrum 84 | self.amplitude = 1e3*np.exp(-(self.kk-k0)**2/(2*dk**2)) 85 | self.forc = np.zeros(self.sizevar) 86 | 87 | def add_forcing(self, x, t, dxdt): 88 | """ add the forcing term on x[0]=the vorticity """ 89 | dt = t-self.t0 90 | self.t0 = t 91 | gamma = dt/self.tau 92 | nh = self.nh 93 | 94 | # white noise phase 95 | phase = np.random.normal(size=(self.ny, self.nx))*2*np.pi 96 | hnoise = self.amplitude*np.exp(1j*phase) 97 | 98 | forc = np.real(np.fft.ifft2(hnoise)) 99 | # time filter to introduce some time correlation in the forcing 100 | self.forc[nh:-nh, nh:-nh] = gamma*forc + \ 101 | self.forc[nh:-nh, nh:-nh]*(1-gamma) 102 | self.fill_halo(self.forc) 103 | # impose zero net vorticity (otherwise the solver does not converge) 104 | self.forc -= self.domain_integration(self.forc) / self.area 105 | 106 | # add the forcing to the vorticity tendency 107 | dxdt[0] += self.forc 108 | 109 | 110 | grid = Grid(param) 111 | param.Kdiff = 5e-4*grid.dx 112 | 113 | f2d = Fluid2d(param, grid) 114 | model = f2d.model 115 | model.forc = Forcing(param, grid) 116 | 117 | xr, yr = grid.xr, grid.yr 118 | vor = model.var.get('vorticity') 119 | trac = model.var.get('tracer') 120 | 121 | # set an initial large scale random vorticity field 122 | np.random.seed(42) 123 | 124 | 125 | def set_x_and_k(n, L): 126 | k = ((n//2+np.arange(n)) % n) - n//2 127 | return (np.arange(n)+0.5)*L/n, 2*np.pi*k/L 128 | 129 | 130 | _, kx = set_x_and_k(param.nx, np.pi) 131 | _, ky = set_x_and_k(param.ny, np.pi) 132 | kkx, kky = np.meshgrid(kx, ky) 133 | kk = np.sqrt(kkx**2 + kky**2) 134 | 135 | k0 = param.k0 136 | dk = 1 137 | 138 | phase = np.random.normal(size=(param.ny, param.nx))*2*np.pi 139 | 140 | hnoise = np.exp(-(kk-k0)**2/(2*dk**2))*np.exp(1j*phase) 141 | 142 | noise = np.zeros_like(vor) 143 | nh = grid.nh 144 | noise[nh:-nh, nh:-nh] = 1e3*np.real(np.fft.ifft2(hnoise)) 145 | 146 | grid.fill_halo(noise) 147 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 148 | vor[:] = noise*grid.msk 149 | 150 | # initialize the passive tracer with square tiles (better idea?) 151 | trac[:] = np.round(xr*6) % 2 + np.round(yr*6) % 2 152 | 153 | 154 | model.set_psi_from_vorticity() 155 | model.diagnostics(model.var, 0) 156 | energy = model.diags['ke'] 157 | print('energy=', energy) 158 | vor = vor*param.Lx/np.sqrt(energy) 159 | vor = vor/1e3 160 | model.set_psi_from_vorticity() 161 | f2d.loop() 162 | -------------------------------------------------------------------------------- /experiments/VonKarman/karman_street.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | from restart import Restart 6 | from island import Island 7 | 8 | param = Param('default.xml') 9 | param.modelname = 'euler' 10 | param.expname = 'karman_0' 11 | 12 | # domain and resolution 13 | ratio = 2 14 | param.ny = 64*2 15 | param.nx = param.ny*ratio 16 | param.Ly = 1. 17 | param.Lx = param.Ly*ratio 18 | param.npx = 1 19 | param.npy = 1 20 | param.geometry = 'xchannel' 21 | 22 | # time 23 | param.tend = 600 24 | param.cfl = 1.2 25 | param.adaptable_dt = True 26 | param.dt = 1e-2 27 | param.dtmax = 1. 28 | 29 | # discretization 30 | param.order = 3 31 | param.timestepping = 'RK3_SSP' 32 | 33 | # output 34 | param.var_to_save = ['vorticity', 'psi', 'u'] 35 | param.list_diag = 'all' 36 | param.freq_plot = 10 37 | param.freq_his = .5 38 | param.freq_diag = .1 39 | 40 | # plot 41 | param.plot_interactive = True 42 | param.plot_var = 'vorticity' 43 | param.plot_psi = True 44 | param.cax = [-10, 10] 45 | param.colorscheme = 'imposed' 46 | 47 | # physics 48 | param.forcing = False 49 | param.noslip = True 50 | param.diffusion = True 51 | param.isisland = True 52 | param.spongelayer = True 53 | param.decay = False 54 | 55 | 56 | # param.additional_tracer = ['dye', 'age'] 57 | 58 | nh = param.nh 59 | 60 | grid = Grid(param) 61 | 62 | 63 | def disc(param, grid, x0, y0, sigma): 64 | xr, yr = grid.xr, grid.yr 65 | r = np.sqrt((xr-x0)**2+(yr-y0)**2) 66 | # add a fin 67 | # r[66:68, 30:60] = 0 68 | return np.where(r <= sigma) 69 | 70 | 71 | def square(param, grid, x0, y0, sigma): 72 | xr, yr = grid.xr, grid.yr 73 | rx = np.abs(xr-param.Lx*x0)/sigma 74 | ry = np.abs(yr-param.Ly*y0)/sigma 75 | return np.where((rx < 1) & (ry < 1)) 76 | 77 | 78 | psi0 = 0.2 # this sets the inflow intensity 79 | sigma = 0.08 80 | idx = disc(param, grid, 0.5*param.Ly, 0.5, sigma) 81 | grid.msk[idx] = 0 82 | grid.msknoslip[idx] = 0 83 | grid.island.add(idx, 0.) 84 | 85 | if grid.j0 == 0: 86 | msk = grid.msk.copy()*0 87 | msk[:nh, :] = 1 88 | idx = np.where(msk == 1) 89 | grid.island.add(idx, psi0*.5) 90 | 91 | if grid.j0 == param.npy-1: 92 | msk = grid.msk.copy()*0 93 | msk[-nh:-1, :] = 1 94 | idx = np.where(msk == 1) 95 | grid.island.add(idx, -psi0*.5) 96 | 97 | # tell the code to deactivate the no-slip along the outer walls 98 | if grid.j0 == 0: 99 | grid.msknoslip[:nh, :] = 1 100 | 101 | if grid.j0 == param.npy-1: 102 | grid.msknoslip[-nh:, :] = 1 103 | 104 | 105 | param.Kdiff = 20e-3*grid.dx # regular street, with Heun and up3 106 | param.Kdiff = 5e-3*grid.dx # regular street, single vortices only 107 | 108 | f2d = Fluid2d(param, grid) 109 | model = f2d.model 110 | 111 | xr, yr = grid.xr, grid.yr 112 | vor = model.var.get('vorticity') 113 | 114 | 115 | # add noise to trigger the instability 116 | noise = np.random.normal(size=np.shape(yr))*grid.msk 117 | grid.fill_halo(noise) 118 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 119 | 120 | vor += 1e-1*noise*grid.msk 121 | 122 | vor *= grid.msk 123 | 124 | 125 | model.set_psi_from_vorticity() 126 | 127 | f2d.loop() 128 | -------------------------------------------------------------------------------- /experiments/doublediffusion/doublediffusion.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | 6 | param = Param('default.xml') 7 | param.modelname = 'boussinesqTS' 8 | # if you want to not store on $HOME/data/fluid2d, uncomment 9 | # param.datadir = '/workdir/myname/data/fluid2d' 10 | param.expname = 'dbl_diff' 11 | 12 | # domain and resolution 13 | param.nx = 64*2 14 | 15 | param.ny = param.nx*2 16 | param.npx = 1 17 | param.npy = 1 18 | param.Lx = 1. 19 | param.Ly = param.Lx*2 20 | param.geometry = 'xchannel' 21 | 22 | # time 23 | param.tend = 100. 24 | param.cfl = 1. 25 | param.adaptable_dt = True 26 | param.dt = 0.01 27 | param.dtmax = 1e-2 28 | 29 | # discretization 30 | param.order = 5 31 | param.relaxation = 'tridiagonal' 32 | 33 | # output 34 | param.plot_var = 'S' 35 | param.var_to_save = ['vorticity', 'density', 'T', 'S', 'psi'] 36 | param.list_diag = 'all' 37 | param.freq_his = 1. 38 | param.freq_diag = 2e-1 39 | 40 | # plot 41 | param.plot_interactive = True 42 | param.freq_plot = 10 43 | param.colorscheme = 'minmax' 44 | param.cax = np.asarray([-10., 10.]) 45 | param.generate_mp4 = True 46 | 47 | # physics 48 | param.gravity = 1. 49 | param.forcing = True # set it to True to have r.h.s on prognostic variables 50 | param.diffusion = True 51 | param.noslip = False 52 | param.alphaT = 1. 53 | param.betaS = 1. 54 | # also modify param.Kdiff with these new tracers 55 | # param.additional_tracer = ['mytracer1', 'phyto'] 56 | 57 | 58 | grid = Grid(param) 59 | 60 | xr, yr = grid.xr, grid.yr 61 | Lx, Ly = param.Lx, param.Ly 62 | 63 | 64 | K0 = 2e-2*grid.dx 65 | param.Kdiff = {'vorticity': K0*7., 'T': K0, 'S': K0/50.} 66 | # add my own attributes (used in the forcing) 67 | setattr(param, 'dTdz', 50.) 68 | setattr(param, 'dSdz', 50.) 69 | 70 | class Forcing: 71 | """ define the forcing """ 72 | 73 | def __init__(self, param, grid): 74 | 75 | self.list_param = ['deltab', 'dt'] 76 | param.copy(self, self.list_param) 77 | 78 | self.list_param = ['j0', 'npy', 'nh'] 79 | grid.copy(self, self.list_param) 80 | 81 | # this is the z coordinate (from -param.Ly/2 to param.Ly/2) 82 | z = grid.yr0 83 | dz = grid.dy 84 | 85 | KT = param.Kdiff['T'] 86 | KS = param.Kdiff['S'] 87 | 88 | dTdz = param.dTdz 89 | dSdz = param.dSdz 90 | 91 | self.FluxT = KT*dTdz/dz 92 | self.FluxS = KS*dSdz/dz 93 | print('Temperature flux: %.2g' % self.FluxT) 94 | 95 | def add_forcing(self, x, t, dxdt, coef=1.): 96 | """ add the forcing term on 97 | - the temperature 'T' (x[6]) 98 | - the salinity 'S' (x[7]) 99 | """ 100 | 101 | if self.j0 == self.npy-1: 102 | # add the forcing to the uppermost row 103 | # yet, inside the domain (the halo has 104 | # a width of nh points) and because 105 | # of MPI domain decomposition do that only 106 | # subdomains on the top row 107 | dxdt[6][-self.nh-1, :] += self.FluxT 108 | dxdt[7][-self.nh-1, :] += self.FluxS 109 | 110 | if self.j0 == 0: 111 | # same but for the bottom boundary condition 112 | dxdt[6][self.nh, :] -= self.FluxT 113 | dxdt[7][self.nh, :] -= self.FluxS 114 | 115 | 116 | 117 | 118 | 119 | f2d = Fluid2d(param, grid) 120 | model = f2d.model 121 | if param.forcing: 122 | model.forc = Forcing(param, grid) 123 | 124 | # let's define the initial state 125 | 126 | x, z = grid.xr0, grid.yr0 127 | # temp, salt and vor are 'pointers' to model arrays 128 | temp = model.var.get('T') 129 | salt = model.var.get('S') 130 | vor = model.var.get('vorticity') 131 | 132 | delta = 0.02*param.Lx 133 | C = 50. 134 | 135 | zz = z/delta 136 | 137 | # assign the values to temp / the [:, :] is VERY important 138 | temp[:, :] = param.dTdz*z 139 | salt[:, :] = param.dSdz*z 140 | 141 | 142 | RaT = 2*param.gravity*param.alphaT*C*param.Ly**3/(param.Kdiff['vorticity']*param.Kdiff['T']) 143 | print('RaT = %.3g' % RaT) 144 | 145 | 146 | # add noise to trigger the instability 147 | np.random.seed(42) 148 | noise = np.random.normal(size=np.shape(yr), scale=1.)*grid.msk 149 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 150 | 151 | grid.fill_halo(noise) 152 | 153 | vor[:, :] = 1e-2*noise 154 | 155 | model.set_density() 156 | model.set_psi_from_vorticity() 157 | 158 | f2d.loop() 159 | -------------------------------------------------------------------------------- /experiments/vorticity_dynamics/axisymm.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | import wavepackets as wp 6 | 7 | # If the code immediately stops with 8 | 9 | # Traceback (most recent call last): 10 | # File "vortex.py", line 1, in 11 | # from param import Param 12 | # ImportError: No module named param 13 | 14 | # it means that you forgot to do 15 | # source activate.sh in your terminal 16 | 17 | 18 | param = Param('default.xml') 19 | param.modelname = 'euler' 20 | param.expname = 'axisymm_00' 21 | 22 | # domain and resolution 23 | param.nx = 64*2 24 | param.ny = param.nx 25 | param.Ly = param.Lx 26 | param.npx = 1 27 | param.npy = 1 28 | param.geometry = 'closed' 29 | 30 | # time 31 | param.tend = 20. 32 | param.cfl = 1.2 33 | param.adaptable_dt = True 34 | param.dt = 0.01 35 | param.dtmax = 100 36 | 37 | # discretization 38 | param.order = 5 39 | param.timestepping = 'RK3_SSP' 40 | param.exacthistime = True 41 | 42 | # output 43 | param.var_to_save = ['vorticity', 'psi', 'tracer'] 44 | param.list_diag = 'all' 45 | param.freq_plot = 10 46 | param.freq_his = .2 47 | param.freq_diag = 0.02 48 | 49 | # plot 50 | param.freq_plot = 10 51 | param.plot_interactive = True 52 | param.plot_psi = True 53 | param.plot_var = 'vorticity' 54 | param.cax = np.array([-2, 2.])*2 55 | param.colorscheme = 'imposed' 56 | param.generate_mp4 = True 57 | 58 | # physics 59 | param.noslip = False 60 | param.diffusion = False 61 | param.additional_tracer = ['tracer'] 62 | 63 | grid = Grid(param) 64 | 65 | param.Kdiff = 5e-2*grid.dx 66 | 67 | xr, yr = grid.xr, grid.yr 68 | 69 | 70 | f2d = Fluid2d(param, grid) 71 | model = f2d.model 72 | 73 | 74 | vor = model.var.get('vorticity') 75 | 76 | 77 | def vortex(param, grid, x0, y0, sigma, 78 | vortex_type, ratio=1): 79 | """Setup a compact distribution of vorticity 80 | 81 | at location x0, y0 vortex, width is sigma, vortex_type controls 82 | the radial vorticity profile, ratio controls the x/y aspect ratio 83 | (for ellipses) 84 | 85 | """ 86 | xr, yr = grid.xr, grid.yr 87 | # ratio controls the ellipticity, ratio=1 is a disc 88 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2*ratio**2) 89 | 90 | y = x.copy()*0. 91 | 92 | if vortex_type in ('gaussian', 'cosine', 'step', 'square', 'triangle'): 93 | if vortex_type == 'gaussian': 94 | y = np.exp(-x**2/(sigma**2)) 95 | 96 | elif vortex_type == 'cosine': 97 | y = np.cos(x/sigma*np.pi/2) 98 | y[x > sigma] = 0. 99 | 100 | elif vortex_type == 'step': 101 | y[x <= sigma] = 1. 102 | 103 | elif vortex_type == 'square': 104 | y[msk] = wp.square(xr-param.Lx*x0, yr-param.Ly*y0, sigma) 105 | 106 | elif vortex_type == 'triangle': 107 | y = wp.triangle(xr-param.Lx*x0, yr-param.Ly*y0, sigma) 108 | else: 109 | raise ValueError('this kind of vortex (%s) is not defined' % vortex_type) 110 | 111 | return y 112 | 113 | 114 | # 2/ set an initial tracer field 115 | vtype = 'triangle' 116 | # vortex width 117 | sigma = 0.2*param.Lx 118 | 119 | vortex_config = 'single' 120 | 121 | if vortex_config == 'single': 122 | vor[:] = vortex(param, grid, 0.5, 0.5, sigma, 123 | vtype, ratio=1) 124 | 125 | 126 | elif vortex_config == 'dipole': 127 | x0 = 0.5 128 | vor[:] = -vortex(param, grid, x0, 0.42, sigma, 129 | vtype, ratio=1) 130 | vor[:] += vortex(param, grid, x0, 0.58, sigma, 131 | vtype, ratio=1) 132 | 133 | elif vortex_config == 'corotating': 134 | dist = 0.25*param.Lx 135 | vor[:] = vortex(param, grid, 0.5, 0.5+dist/2, sigma, vtype) 136 | vor[:] += vortex(param, grid, 0.5, 0.5-dist/2, sigma, vtype) 137 | 138 | 139 | 140 | vor[:] = vor*grid.msk 141 | 142 | if False: 143 | np.random.seed(1) # this guarantees the results reproducibility 144 | noise = np.random.normal(size=np.shape(yr))*grid.msk 145 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 146 | grid.fill_halo(noise) 147 | 148 | noise_amplitude = 1e-3 149 | 150 | vor += noise*noise_amplitude 151 | 152 | model.set_psi_from_vorticity() 153 | 154 | state = model.var.get('tracer') 155 | state[:] = np.round(xr*6) % 2 + np.round(yr*6) % 2 156 | state *= grid.msk 157 | 158 | # % normalization of the vorticity so that enstrophy == 1. 159 | model.diagnostics(model.var, 0) 160 | enstrophy = model.diags['enstrophy'] 161 | # print('enstrophy = %g' % enstrophy) 162 | vor[:] = vor[:] / np.sqrt(enstrophy) 163 | model.set_psi_from_vorticity() 164 | 165 | 166 | f2d.loop() 167 | -------------------------------------------------------------------------------- /experiments/vorticity_dynamics/merging.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | import wavepackets as wp 6 | 7 | # If the code immediately stops with 8 | 9 | # Traceback (most recent call last): 10 | # File "vortex.py", line 1, in 11 | # from param import Param 12 | # ImportError: No module named param 13 | 14 | # it means that you forgot to do 15 | # source activate.sh in your terminal 16 | 17 | 18 | param = Param('default.xml') 19 | param.modelname = 'euler' 20 | param.expname = 'merging_00' 21 | 22 | # domain and resolution 23 | param.nx = 64*2 24 | param.ny = param.nx 25 | param.Ly = param.Lx 26 | param.npx = 1 27 | param.npy = 1 28 | param.geometry = 'closed' 29 | 30 | # time 31 | param.tend = 20. 32 | param.cfl = 1.2 33 | param.adaptable_dt = True 34 | param.dt = 0.01 35 | param.dtmax = 100 36 | 37 | # discretization 38 | param.order = 5 39 | param.timestepping = 'RK3_SSP' 40 | param.exacthistime = True 41 | 42 | # output 43 | param.var_to_save = ['vorticity', 'psi', 'tracer'] 44 | param.list_diag = 'all' 45 | param.freq_plot = 10 46 | param.freq_his = .2 47 | param.freq_diag = 0.02 48 | 49 | # plot 50 | param.freq_plot = 10 51 | param.plot_interactive = True 52 | param.plot_psi = True 53 | param.plot_var = 'vorticity' 54 | param.cax = np.array([-2, 2.])*2 55 | param.colorscheme = 'imposed' 56 | param.generate_mp4 = True 57 | 58 | # physics 59 | param.noslip = False 60 | param.diffusion = False 61 | param.additional_tracer = ['tracer'] 62 | 63 | grid = Grid(param) 64 | 65 | param.Kdiff = 5e-2*grid.dx 66 | 67 | xr, yr = grid.xr, grid.yr 68 | 69 | 70 | f2d = Fluid2d(param, grid) 71 | model = f2d.model 72 | 73 | 74 | vor = model.var.get('vorticity') 75 | 76 | 77 | def vortex(param, grid, x0, y0, sigma, 78 | vortex_type, ratio=1): 79 | """Setup a compact distribution of vorticity 80 | 81 | at location x0, y0 vortex, width is sigma, vortex_type controls 82 | the radial vorticity profile, ratio controls the x/y aspect ratio 83 | (for ellipses) 84 | 85 | """ 86 | xr, yr = grid.xr, grid.yr 87 | # ratio controls the ellipticity, ratio=1 is a disc 88 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2*ratio**2) 89 | 90 | y = x.copy()*0. 91 | 92 | if vortex_type in ('gaussian', 'cosine', 'step', 'square', 'triangle'): 93 | if vortex_type == 'gaussian': 94 | y = np.exp(-x**2/(sigma**2)) 95 | 96 | elif vortex_type == 'cosine': 97 | y = np.cos(x/sigma*np.pi/2) 98 | y[x > sigma] = 0. 99 | 100 | elif vortex_type == 'step': 101 | y[x <= sigma] = 1. 102 | 103 | elif vortex_type == 'square': 104 | y[msk] = wp.square(xr-param.Lx*x0, yr-param.Ly*y0, sigma) 105 | 106 | elif vortex_type == 'triangle': 107 | y = wp.triangle(xr-param.Lx*x0, yr-param.Ly*y0, sigma) 108 | else: 109 | raise ValueError('this kind of vortex (%s) is not defined' % vortex_type) 110 | 111 | return y 112 | 113 | 114 | # 2/ set an initial tracer field 115 | vtype = 'gaussian' 116 | # vortex width 117 | sigma = 0.05*param.Lx 118 | 119 | vortex_config = 'corotating' 120 | 121 | if vortex_config == 'single': 122 | vor[:] = vortex(param, grid, 0.5, 0.5, sigma, 123 | vtype, ratio=1) 124 | 125 | 126 | elif vortex_config == 'dipole': 127 | x0 = 0.5 128 | vor[:] = -vortex(param, grid, x0, 0.42, sigma, 129 | vtype, ratio=1) 130 | vor[:] += vortex(param, grid, x0, 0.58, sigma, 131 | vtype, ratio=1) 132 | 133 | elif vortex_config == 'corotating': 134 | dist = 4*sigma 135 | vor[:] = vortex(param, grid, 0.5, 0.5+dist/2, sigma, vtype) 136 | vor[:] += vortex(param, grid, 0.5, 0.5-dist/2, sigma, vtype) 137 | 138 | 139 | 140 | vor[:] = vor*grid.msk 141 | 142 | if False: 143 | np.random.seed(1) # this guarantees the results reproducibility 144 | noise = np.random.normal(size=np.shape(yr))*grid.msk 145 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 146 | grid.fill_halo(noise) 147 | 148 | noise_amplitude = 1e-3 149 | 150 | vor += noise*noise_amplitude 151 | 152 | model.set_psi_from_vorticity() 153 | 154 | state = model.var.get('tracer') 155 | state[:] = np.round(xr*6) % 2 + np.round(yr*6) % 2 156 | state *= grid.msk 157 | 158 | # % normalization of the vorticity so that enstrophy == 1. 159 | model.diagnostics(model.var, 0) 160 | enstrophy = model.diags['enstrophy'] 161 | # print('enstrophy = %g' % enstrophy) 162 | vor[:] = vor[:] / np.sqrt(enstrophy) 163 | model.set_psi_from_vorticity() 164 | 165 | 166 | f2d.loop() 167 | -------------------------------------------------------------------------------- /experiments/vorticity_dynamics/vortex.py: -------------------------------------------------------------------------------- 1 | from fluid2d import Fluid2d 2 | from param import Param 3 | from grid import Grid 4 | import numpy as np 5 | import wavepackets as wp 6 | 7 | # If the code immediately stops with 8 | 9 | # Traceback (most recent call last): 10 | # File "vortex.py", line 1, in 11 | # from param import Param 12 | # ImportError: No module named param 13 | 14 | # it means that you forgot to do 15 | # source activate.sh in your terminal 16 | 17 | 18 | param = Param('default.xml') 19 | param.modelname = 'euler' 20 | param.expname = 'dipole_00' 21 | 22 | # domain and resolution 23 | param.nx = 64*2 24 | param.ny = param.nx 25 | param.Ly = param.Lx 26 | param.npx = 1 27 | param.npy = 1 28 | param.geometry = 'closed' 29 | 30 | # time 31 | param.tend = 20. 32 | param.cfl = 1.2 33 | param.adaptable_dt = True 34 | param.dt = 0.01 35 | param.dtmax = 100 36 | 37 | # discretization 38 | param.order = 5 39 | param.timestepping = 'RK3_SSP' 40 | param.exacthistime = True 41 | 42 | # output 43 | param.var_to_save = ['vorticity', 'psi', 'tracer'] 44 | param.list_diag = 'all' 45 | param.freq_plot = 10 46 | param.freq_his = .2 47 | param.freq_diag = 0.02 48 | 49 | # plot 50 | param.freq_plot = 10 51 | param.plot_interactive = True 52 | param.plot_psi = True 53 | param.plot_var = 'vorticity' 54 | param.cax = np.array([-2, 2.])*5 55 | param.colorscheme = 'imposed' 56 | param.generate_mp4 = True 57 | 58 | # physics 59 | # activate noslip to see the viscous interaction with the wall 60 | param.noslip = False 61 | param.diffusion = False 62 | param.additional_tracer = ['tracer'] 63 | 64 | grid = Grid(param) 65 | 66 | param.Kdiff = 5e-2*grid.dx 67 | 68 | xr, yr = grid.xr, grid.yr 69 | 70 | 71 | f2d = Fluid2d(param, grid) 72 | model = f2d.model 73 | 74 | 75 | vor = model.var.get('vorticity') 76 | 77 | 78 | def vortex(param, grid, x0, y0, sigma, 79 | vortex_type, ratio=1): 80 | """Setup a compact distribution of vorticity 81 | 82 | at location x0, y0 vortex, width is sigma, vortex_type controls 83 | the radial vorticity profile, ratio controls the x/y aspect ratio 84 | (for ellipses) 85 | 86 | """ 87 | xr, yr = grid.xr, grid.yr 88 | # ratio controls the ellipticity, ratio=1 is a disc 89 | x = np.sqrt((xr-param.Lx*x0)**2+(yr-param.Ly*y0)**2*ratio**2) 90 | 91 | y = x.copy()*0. 92 | 93 | if vortex_type in ('gaussian', 'cosine', 'step', 'square', 'triangle'): 94 | if vortex_type == 'gaussian': 95 | y = np.exp(-x**2/(sigma**2)) 96 | 97 | elif vortex_type == 'cosine': 98 | y = np.cos(x/sigma*np.pi/2) 99 | y[x > sigma] = 0. 100 | 101 | elif vortex_type == 'step': 102 | y[x <= sigma] = 1. 103 | 104 | elif vortex_type == 'square': 105 | y[msk] = wp.square(xr-param.Lx*x0, yr-param.Ly*y0, sigma) 106 | 107 | elif vortex_type == 'triangle': 108 | y = wp.triangle(xr-param.Lx*x0, yr-param.Ly*y0, sigma) 109 | else: 110 | raise ValueError('this kind of vortex (%s) is not defined' % vortex_type) 111 | 112 | return y 113 | 114 | 115 | # 2/ set an initial tracer field 116 | vtype = 'gaussian' 117 | # vortex width 118 | sigma = 0.05*param.Lx 119 | 120 | vortex_config = 'dipole' 121 | 122 | if vortex_config == 'single': 123 | vor[:] = vortex(param, grid, 0.5, 0.5, sigma, 124 | vtype, ratio=1) 125 | 126 | 127 | elif vortex_config == 'dipole': 128 | x0 = 0.5 129 | vor[:] = -vortex(param, grid, x0, 0.42, sigma, 130 | vtype, ratio=1) 131 | vor[:] += vortex(param, grid, x0, 0.58, sigma, 132 | vtype, ratio=1) 133 | 134 | elif vortex_config == 'corotating': 135 | dist = 4*sigma 136 | vor[:] = vortex(param, grid, 0.5, 0.5+dist/2, sigma, vtype) 137 | vor[:] += vortex(param, grid, 0.5, 0.5-dist/2, sigma, vtype) 138 | 139 | 140 | 141 | vor[:] = vor*grid.msk 142 | 143 | if False: 144 | np.random.seed(1) # this guarantees the results reproducibility 145 | noise = np.random.normal(size=np.shape(yr))*grid.msk 146 | noise -= grid.domain_integration(noise)*grid.msk/grid.area 147 | grid.fill_halo(noise) 148 | 149 | noise_amplitude = 1e-3 150 | 151 | vor += noise*noise_amplitude 152 | 153 | model.set_psi_from_vorticity() 154 | 155 | state = model.var.get('tracer') 156 | state[:] = np.round(xr*6) % 2 + np.round(yr*6) % 2 157 | state *= grid.msk 158 | 159 | # % normalization of the vorticity so that enstrophy == 1. 160 | model.diagnostics(model.var, 0) 161 | enstrophy = model.diags['enstrophy'] 162 | # print('enstrophy = %g' % enstrophy) 163 | vor[:] = vor[:] / np.sqrt(enstrophy) 164 | model.set_psi_from_vorticity() 165 | 166 | 167 | f2d.loop() 168 | -------------------------------------------------------------------------------- /experiments/vorticity_dynamics/wavepackets.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def square(xr, yr, sigma): 4 | phi = np.zeros_like(xr) 5 | s2 = sigma/2. 6 | phi[(xr >= -s2) & (xr <= s2) & (yr >= -s2) & (yr <= s2)] = 1. 7 | return phi 8 | 9 | def triangle(xr, yr, sigma): 10 | j = np.exp(1j*2*np.pi/3) 11 | z = xr + 1j*yr 12 | z = (z/sigma) 13 | 14 | p1x = np.real(j**0) 15 | p1y = np.imag(j**0) 16 | p2x = np.real(j**1) 17 | p2y = np.imag(j**1) 18 | p0x = np.real(j**2) 19 | p0y = np.imag(j**2) 20 | px = np.real(z) 21 | py = np.imag(z) 22 | 23 | area = 0.5*(-p1y*p2x + p0y*(-p1x + p2x) + p0x*(p1y - p2y) + p1x*p2y) 24 | s = 1/(2*area)*(p0y*p2x - p0x*p2y + (p2y - p0y)*px + (p0x - p2x)*py) 25 | t = 1/(2*area)*(p0x*p1y - p0y*p1x + (p0y - p1y)*px + (p1x - p0x)*py) 26 | 27 | phi = np.zeros_like(xr) 28 | phi[(t > 0) & (s > 0) & (1-t-s > 0)] = 1. 29 | 30 | return phi 31 | -------------------------------------------------------------------------------- /gallery/DW_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvthinker/Fluid2d/75289b2bb1594b52c9e65a16e2800ae7f5b868aa/gallery/DW_0.png -------------------------------------------------------------------------------- /gallery/DW_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvthinker/Fluid2d/75289b2bb1594b52c9e65a16e2800ae7f5b868aa/gallery/DW_1.png -------------------------------------------------------------------------------- /gallery/TI_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvthinker/Fluid2d/75289b2bb1594b52c9e65a16e2800ae7f5b868aa/gallery/TI_0.png -------------------------------------------------------------------------------- /gallery/gallery.rst: -------------------------------------------------------------------------------- 1 | .. _gallery: 2 | 3 | ======================= 4 | Gallery of fluid movies 5 | ======================= 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | - Dipole wall interaction. **How friction dramatically impacts 11 | fluid-structure interaction.** 12 | 13 | - :download:`without friction ` (free-slip boundary condition) 14 | 15 | .. figure:: DW_1.png 16 | :scale: 40 17 | .. 18 | 19 | - :download:`with friction ` (no-slip boundary condition) 20 | 21 | .. figure:: DW_0.png 22 | :scale: 40 23 | .. 24 | 25 | 26 | 27 | - :download:`TaylorInstability ` **How light and dense fluids mix** 28 | 29 | .. figure:: TI_0.png 30 | :scale: 40 31 | .. 32 | 33 | - Von Karman vortex street 34 | 35 | - TwoDimTurb 36 | 37 | - ShearInstab 38 | 39 | - GeophysicalTurb 40 | 41 | - GyreCirculation 42 | 43 | - KelvinHelmholtz 44 | 45 | - Gravity current 46 | 47 | Call for tutorial flows 48 | ----------------------- 49 | 50 | If you come up with an experiment that clearly explains a fluid 51 | process, I would be happy to add your credit to this gallery. 52 | 53 | References 54 | ---------- 55 | 56 | - The absolute reference for an `Album of fluid motion `_ is of course the Van Dyke's book 57 | 58 | - Less known and with schematics instead of photos the Lugt's 59 | book. Everything is interpreted in terms of vorticity, including why 60 | aircrafts fly. A nice compliment to fluid2d. 61 | 62 | Lugt, H. J. (1983). Vortex flow in nature and technology. New York, 63 | Wiley-Interscience, 1983, 305 p. Translation., 1. 64 | 65 | - To see trully amazing artist-rendered computer-generated flows (and much more than flows) 66 | http://physbam.stanford.edu/~fedkiw/ 67 | -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | .. fluid2d documentation master file, created by 2 | sphinx-quickstart on Thu Mar 2 22:46:22 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | 10 | Welcome to Fluid2d documentation 11 | ================================ 12 | 13 | Fluid2d is a versatile Python-Fortran CFD code that solves a large 14 | class of 2D flows. It is designed to be used easily by Students 15 | learning Fluid Mechanics or Geophysical Fluid Dynamics and by their 16 | professors willing to illustrate their course and to organize 17 | numerical practicals. The idea is to visualize flows on the fly, as 18 | they are computed. The effect of parameter changes can be seen 19 | immediately. The key quantity of fluid2D is the vorticity. If you feel 20 | weak on vorticity dynamics, this code is for you. You should rapidly 21 | become as expert as the experts. 22 | 23 | You can learn how basic processes work because of the power of 24 | animations. It is quite easy to go beyond textbooks and to reach 25 | research questions. 26 | 27 | Several features are particulary cool 28 | 29 | - the code handles many different sets of equations: transport, Euler, 30 | quasi-geostrophic, Boussinesq and even the thermal wind equations. 31 | 32 | - the code handles a mask system that allows to have complicated 33 | geometries (closed domain with arbitratry shape, reentrant channel, with 34 | multiple island etc) 35 | 36 | - the code tends to have a very low level of dissipation because the 37 | dissipation is handled implicitely by the numerics. 38 | 39 | - a no-slip condition that allows to study boundary layers questions 40 | 41 | - the code is parallelized and can be run on cluster if high 42 | resolution is desired. 43 | 44 | - a hopefully not too hard user's interface. You tell me. An experiment 45 | boils down to one script where you set everything up. For 46 | forced-dissipated flows or fancy add-ons (like introducing a dye 47 | tracer), you may need a second script. 48 | 49 | Credits: 50 | -------- 51 | 52 | This code have been developed over many years of teaching numerical 53 | methods for CFD. The first version of this code was in Matlab under 54 | the impulse of the FDSE Summer School. A special credit to Caroline 55 | Muller who makes this code happen in the first place. A full writing 56 | in python has been done with the great help of Clement Vic and Nicolas 57 | Grima (CNRS engineer). Others Students have contributed to either the 58 | coding or a better understanding of how the numerics handles 59 | dissipation. They should be thanked here: Ljuba Novi for the work on 60 | RK3 and up5, Alexander Siteur for his work on the pressure (not yet 61 | implemented in this version), Milan Kloewer for his work on a level 62 | set implementation (not present in this version), Mathieu Morvan for 63 | the dipole wall interaction and Markus Reinert for his many valuable 64 | feedbacks. Finally, the code has benefited from my enthusiastic 65 | collaboration with Professor J. C. McWilliams. 66 | 67 | 68 | Where to start? 69 | --------------- 70 | 71 | You may have a quick look at the :ref:`gallery` to see the possibilities. Then you need to :ref:`install` it and start your first experiment. 72 | 73 | 74 | 75 | Cite the code 76 | ------------- 77 | 78 | The code has never been published and will likely never be. If you use the 79 | code for your classes or your research I would appreciate your 80 | feedbacks (mailto: roullet AT univ-brest.fr) 81 | 82 | Disclaimer 83 | ---------- 84 | 85 | As most codes, this code almost certainly has bugs. Please report 86 | them and possibly their fix if you find it. 87 | 88 | A word of caution 89 | ----------------- 90 | 91 | The flows simulated by fluid2d are two-dimensional. This has many 92 | impacts. The most important to be aware of is the systematic tendency for the 93 | inverse cascade of kinetic energy, i.e. the tendency for the vorticity 94 | to form bigger and bigger structures. This holds for all flows. It is 95 | a realistic feature in many cases but not always. Because our world is 96 | three-dimensional, vorticity is actually a vector, not a scalar. In 97 | 3D, vorticity form tubes than can get twisted like spaghettis. Flows 98 | look then very differently than in 2D. 99 | 100 | 101 | Contents: 102 | --------- 103 | 104 | .. toctree:: 105 | :maxdepth: 2 106 | 107 | gallery/gallery 108 | docs/model_equations 109 | docs/model_numerics 110 | docs/howto 111 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pydir=$HOME/.fluid2d 4 | default=core/defaults.json 5 | srcdir=`pwd` 6 | myexpdir=$srcdir/myexp 7 | 8 | echo "--------------------------------------------------------------------------------" 9 | echo "" 10 | echo " Installing Fluid2d" 11 | echo "" 12 | echo "It is recommended to create a virtual environnement. " 13 | echo "If you are using anaconda then you may create the 'pyrsw' environment with" 14 | echo "" 15 | echo "> conda env create -f environment.yml" 16 | echo "" 17 | echo "then whenever you want to use Fluid2d, switch to this environement with" 18 | echo "" 19 | echo "> conda activate fluid2d" 20 | echo "" 21 | echo "Are you ok with you environment? (y/n)" 22 | if [ $ok = "n" ]; then 23 | exit 42 24 | fi 25 | 26 | if [ ! -d "$pydir" ]; then 27 | echo " Create $pydir" 28 | mkdir $pydir 29 | fi 30 | if [ ! -f "$pydir/$default" ]; then 31 | echo " Copy $default in $pydir" 32 | cp $default $pydir/ 33 | fi 34 | if [ ! -d "$myexpdir" ]; then 35 | echo " Create $myexpdir" 36 | mkdir $myexpdir 37 | fi 38 | echo " Copy reference experiments in $myexpdir" 39 | cp -pR $srcdir/experiments/* $myexpdir 40 | 41 | # for bash users 42 | cat > $pydir/activate.sh << EOF 43 | export PYTHONPATH=`pwd`/core:`pwd`/core/gmg 44 | echo Python now knows that Fluid2d is in `pwd` 45 | EOF 46 | 47 | # for csh, tcsh users 48 | cat > $pydir/activate.csh << EOF 49 | setenv PYTHONPATH `pwd`/core:`pwd`/core/gmg 50 | echo Python now knows that Fluid2d is in `pwd` 51 | EOF 52 | 53 | # for fish users 54 | cat > $pydir/activate.fish << EOF 55 | set -gx PYTHONPATH (pwd)/core:(pwd)/core/gmg 56 | echo Python now knows that Fluid2d is in (pwd) 57 | EOF 58 | 59 | # compile the modules with module 60 | echo "--------------------------------------------------------------------------------" 61 | echo "" 62 | echo " Compile modules with f2py" 63 | echo "" 64 | { 65 | # try 66 | #make 67 | python3 build.py 68 | 69 | } || { 70 | #catch 71 | echo "Unable to compile" 72 | echo "Are you sure f2py is installed?" 73 | exit 74 | } 75 | 76 | # copy the experiment into 77 | 78 | echo "" 79 | echo " Before starting, please read this note carefully" 80 | echo "" 81 | echo " As it configured, Fluid2d will store the results in" 82 | echo "" 83 | echo " *** $HOME/data/fluid2d ***" 84 | echo "" 85 | echo " If you don't run the code from your laptop then it is likely that" 86 | echo " you are not allowed to store large binary files on $HOME/data" 87 | echo " because this is your home." 88 | echo "" 89 | echo " In this case, edit $HOME/.fluid2d/defaults.json" 90 | echo " and set 'datadir' default value (in the output section) to a" 91 | echo " place where you are authorized to store large binary files." 92 | echo " In the jargon, this place is usually a 'work' directory." 93 | echo "" 94 | echo " If you are unsure where to store your results check that" 95 | echo " with your system administrator." 96 | echo "" 97 | echo " Once you know where to store the results, then you're good to go" 98 | echo "" 99 | echo " Each time you open a new terminal you need to" 100 | echo " source ~/.fluid2d/activate.sh if you're under bash" 101 | echo " source ~/.fluid2d/activate.csh if you're under csh/tcsh" 102 | echo " source ~/.fluid2d/activate.fish if you're under fish" 103 | echo "" 104 | echo " To run your first experiment" 105 | echo " cd $myexpdir/Vortex" 106 | echo " python3 vortex.py" 107 | echo "" 108 | echo " Write your new experiments in $myexpdir" 109 | echo " or wherever you want, but not in $srcdir/experiments !" 110 | echo "" 111 | 112 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python =>3.10,<3.12 2 | mpi4py >=2.0.0 3 | netCDF4 4 | matplotlib 5 | numpy 6 | scipy 7 | 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #python setup.py build_ext --inplace 2 | from numpy.distutils.core import setup 3 | from numpy.distutils.extension import Extension 4 | 5 | 6 | def gen_ext(module,fortran_options): 7 | src = module.replace('.','/')+'.f90' 8 | ext = Extension(name=module, 9 | sources=[src], # <- should be a list type 10 | extra_f90_compile_args=fortran_options) 11 | return ext 12 | 13 | fortran_options = ['-O3', '-fPIC'] 14 | fortran_modules = ['core.fortran_advection', 15 | 'core.fortran_diag', 16 | 'core.fortran_operators', 17 | 'core.gmg.fortran_multigrid'] 18 | 19 | extensions = [] 20 | for m in fortran_modules: 21 | extensions.append( gen_ext(m,fortran_options) ) 22 | 23 | setup( 24 | name='Fluid2d', 25 | version='1.42', 26 | author='Guillaume Roullet', 27 | author_email='roullet@univ-brest.fr', 28 | license='GPL3', 29 | packages = ['core', 30 | 'core.gmg', 31 | 'exp'], 32 | ext_modules = extensions ) 33 | --------------------------------------------------------------------------------