├── Makefile ├── PrepareInputFiles.py ├── RunRichards.py ├── lib ├── DrainageFunFreeDrainage.py ├── DrainageFunNoFlow.py ├── DrainageFunWaterTable.py ├── GetSteadyInfiltration.py ├── InfiltrationFunNoRunoff.py ├── InfiltrationFunRunoff.py ├── MyNumba.py ├── PlantFunctions.py ├── Richards.py ├── plots.py ├── saveload.py └── vanGenuchten_numba.py ├── readme.md └── run.sh /Makefile: -------------------------------------------------------------------------------- 1 | all: run001/ts.pkl run002/ts.pkl run003/ts.pkl 2 | 3 | run001/ts.pkl: run001/pars.json run001/BC.json run001/IC.json run001/grid.json lib/* 4 | bash run.sh run001 5 | 6 | run002/ts.pkl: run002/pars.json run002/BC.json run002/IC.json run002/grid.json lib/* 7 | bash run.sh run002 8 | 9 | run003/ts.pkl: run003/pars.json run003/BC.json run003/IC.json run003/grid.json lib/* 10 | bash run.sh run003 11 | 12 | run001/pars.json: 13 | python PrepareInputFiles.py 14 | 15 | run002/pars.json: 16 | python PrepareInputFiles.py 17 | 18 | run003/pars.json: 19 | python PrepareInputFiles.py 20 | 21 | clean: 22 | rm Run001/* 23 | rmdir Run001 24 | rm Run002/* 25 | rmdir Run002 26 | rm Run003/* 27 | rmdir Run003 28 | -------------------------------------------------------------------------------- /PrepareInputFiles.py: -------------------------------------------------------------------------------- 1 | # Script to prepare input data for three simple model runs 2 | 3 | import numpy as np 4 | from lib.MyNumba import MakeDictFloat 5 | from lib import vanGenuchten_numba as vg 6 | from lib import saveload as sl 7 | 8 | import os 9 | os.system('mkdir run001') 10 | os.system('mkdir run002') 11 | os.system('mkdir run003') 12 | 13 | # CASE 1: ### Simple model run with constant boundary conditions 14 | 15 | # Choose run configuration - steady or dynamic boundaries, with/without plant uptake: 16 | # Set soil properties: 17 | pars=vg.SiltLoamGE3() 18 | pars['rootdepth']=0.2 19 | 20 | # Set time and space grid: 21 | t=np.arange(0,11) 22 | grid={} 23 | grid['t']=t 24 | grid['dz']=0.05 25 | grid['ProfileDepth']=5. 26 | 27 | # Initial conditions: Choose from options 28 | IC={} 29 | # IC['type']='Hydrostatic' 30 | IC['type']='SteadyInfiltration' 31 | IC['value']=0.001 32 | 33 | # Boundary conditions: Choose from options 34 | BC={} 35 | BC['qIt']=np.array([0,1e6]) 36 | BC['qI']=np.array([0.05,0.05]) 37 | BC['PEt']=np.array([0.0,1e6]) 38 | BC['PE']=np.array([0.,0.]) 39 | 40 | BC['upper']='NoRunoff' 41 | BC['lower']='NoFlow' 42 | 43 | fn='run001' 44 | sl.dict2json(grid,fn+'/grid.json') 45 | sl.dict2json(IC,fn+'/IC.json') 46 | sl.dict2json(BC,fn+'/BC.json') 47 | parsimport={} 48 | for k in pars: 49 | parsimport[k]=pars[k] 50 | sl.dict2json(parsimport,fn+'/pars.json') 51 | 52 | 53 | # CASE 2: ### Same as above, but in dynamic model configuration (slower, but can handle time series input) 54 | 55 | # Choose run configuration - steady or dynamic boundaries, with/without plant uptake: 56 | # Set soil properties: 57 | pars=vg.SiltLoamGE3() 58 | pars['rootdepth']=0.2 59 | 60 | # Set time and space grid: 61 | t=np.arange(0,11) 62 | grid={} 63 | grid['t']=t 64 | grid['dz']=0.05 65 | grid['ProfileDepth']=5. 66 | 67 | # Initial conditions: Choose from options 68 | IC={} 69 | # IC['type']='Hydrostatic' 70 | IC['type']='SteadyInfiltration' 71 | IC['value']=0.01 72 | 73 | # Boundary conditions: Choose from options 74 | BC={} 75 | BC['qIt']=np.array([0.,2.,2.01,1e6]) 76 | BC['qI']=np.array([0.05,0.05,0.,0.]) 77 | BC['PEt']=np.array([0.0,1e6]) 78 | BC['PE']=np.array([0.,0.]) 79 | 80 | BC['upper']='NoRunoff' 81 | BC['lower']='Free' 82 | 83 | fn='run002' 84 | sl.dict2json(grid,fn+'/grid.json') 85 | sl.dict2json(IC,fn+'/IC.json') 86 | sl.dict2json(BC,fn+'/BC.json') 87 | parsimport={} 88 | for k in pars: 89 | parsimport[k]=pars[k] 90 | sl.dict2json(parsimport,fn+'/pars.json') 91 | 92 | 93 | # CASE 3: ### One calendar year, plant and runoff generation 94 | 95 | # Choose run configuration - steady or dynamic boundaries, with/without plant uptake: 96 | 97 | # Set soil properties: 98 | pars=vg.SiltLoamGE3() 99 | pars['rootdepth']=0.2 100 | 101 | # Set time and space grid:http://localhost:8888/notebooks/RunRichards.ipynb# 102 | t=np.arange(365) #pd.date_range('1-Jan-2018',periods=365,freq='D') 103 | grid={} 104 | grid['t']=t 105 | grid['dz']=0.05 106 | grid['ProfileDepth']=5. 107 | 108 | # Initial conditions: Choose from options 109 | IC={} 110 | IC['type']='Hydrostatic' 111 | # IC['type']='SteadyInfiltration' 112 | IC['value']=0.001 113 | 114 | # Boundary conditions: Choose from options 115 | PE=(1-np.cos(np.arange(365)/365*2*np.pi))/1000. 116 | P=np.zeros(365)+0.001 117 | 118 | BC={} 119 | BC['qIt']=np.arange(365) 120 | BC['qI']=P 121 | BC['PEt']=np.arange(365) 122 | BC['PE']=PE 123 | 124 | BC['upper']='Runoff' 125 | BC['lower']='WaterTable' 126 | 127 | fn='run003' 128 | sl.dict2json(grid,fn+'/grid.json') 129 | sl.dict2json(IC,fn+'/IC.json') 130 | sl.dict2json(BC,fn+'/BC.json') 131 | parsimport={} 132 | for k in pars: 133 | parsimport[k]=pars[k] 134 | sl.dict2json(parsimport,fn+'/pars.json') 135 | 136 | -------------------------------------------------------------------------------- /RunRichards.py: -------------------------------------------------------------------------------- 1 | # Import all of the basic libraries (you will always need these) 2 | from matplotlib import pyplot as pl 3 | import numpy as np 4 | import pandas as pd 5 | import time 6 | 7 | from numba import jit 8 | from lib.MyNumba import MakeDictFloat 9 | 10 | # Import a library that contains soil moisture properties and functions 11 | from lib import vanGenuchten_numba as vg 12 | from lib import Richards as re 13 | from lib.plots import InfiltrationPlot 14 | from lib.plots import SimpleBalancePlot 15 | 16 | from lib import saveload as sl 17 | 18 | IC=sl.json2dict('run/IC.json') 19 | BC=sl.json2dict('run/BC.json') 20 | grid=sl.json2dict('run/grid.json') 21 | parsimport=sl.json2dict('run/pars.json') 22 | 23 | pars=MakeDictFloat() 24 | for k in parsimport: 25 | pars[k]=parsimport[k] 26 | 27 | tic=time.time() 28 | ts,state=re.runmodel(IC,BC,pars,grid) 29 | print('runtime = %.1f seconds'%(time.time()-tic)) 30 | 31 | sl.save(ts,'run/ts') 32 | sl.save(state,'run/state') 33 | -------------------------------------------------------------------------------- /lib/DrainageFunFreeDrainage.py: -------------------------------------------------------------------------------- 1 | from numba import jit 2 | import numpy as np 3 | from lib.vanGenuchten_numba import KFun 4 | 5 | @jit(nopython=True) 6 | def DrainageFun(pars,psiB,dz): 7 | # Free drainage 8 | # Note, KFun always needs an array input, even if only a single item, and 9 | # always writes output as an array. numba doesn't like zero-D arrays, so 10 | # when calculating a single value of K, convert to a float afterwards. 11 | KBot=KFun(np.array([psiB]),pars)[0] 12 | qD=KBot 13 | return qD 14 | -------------------------------------------------------------------------------- /lib/DrainageFunNoFlow.py: -------------------------------------------------------------------------------- 1 | from numba import jit 2 | import numpy as np 3 | from lib.vanGenuchten_numba import KFun 4 | 5 | @jit(nopython=True) 6 | def DrainageFun(pars,psiB,dz): 7 | 8 | qD=0. 9 | return qD 10 | -------------------------------------------------------------------------------- /lib/DrainageFunWaterTable.py: -------------------------------------------------------------------------------- 1 | from numba import jit 2 | import numpy as np 3 | from lib.vanGenuchten_numba import KFun 4 | 5 | @jit(nopython=True) 6 | def DrainageFun(pars,psiB,dz): 7 | # Type 1 boundary 8 | # Note, KFun always needs an array input, even if only a single item, and 9 | # always writes output as an array. numba doesn't like zero-D arrays, so 10 | # when calculating a single value of K, convert to a float afterwards. 11 | KBot=KFun(np.zeros(1),pars)[0] 12 | qD=-KBot*((-psiB)/dz*2-1.0) 13 | return qD 14 | -------------------------------------------------------------------------------- /lib/GetSteadyInfiltration.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from lib.vanGenuchten_numba import KFun 3 | 4 | def GetSteadyInfiltration(pars,q): 5 | # Use mid-point search to find psi, where K(psi)=q. Search in logspace 6 | psiL=-3 7 | psiU=3 8 | errL=errfun(psiL,pars,q) 9 | errU=errfun(psiU,pars,q) 10 | errM=10 11 | c=0 12 | # print(c,errM,psiL,psiU) 13 | 14 | while np.abs(errM)>1e-10 and c < 100: 15 | psiM=(psiL+psiU)/2. 16 | errM=errfun(psiM,pars,q) 17 | if errM < 0.: 18 | psiL=psiM 19 | errL=errM 20 | else: 21 | psiU=psiM 22 | errU=errM 23 | c+=1 24 | # print(c,psiM,errM) 25 | if c == 100: print('Warning: reached %d iterations'%c) 26 | return -10**psiM 27 | 28 | def errfun(psi,pars,q): 29 | return q-KFun(np.array([-10**psi]),pars) 30 | -------------------------------------------------------------------------------- /lib/InfiltrationFunNoRunoff.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numba import jit 3 | 4 | @jit(nopython=True) 5 | def InfiltrationFun(t,qIt,qI,psiT=0,dz=0,pars=0): 6 | # Type 2 boundary 7 | qI=np.interp(t,qIt,qI) 8 | return qI 9 | 10 | -------------------------------------------------------------------------------- /lib/InfiltrationFunRunoff.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numba import jit 3 | from lib.vanGenuchten_numba import KFun 4 | 5 | @jit(nopython=True) 6 | def InfiltrationFun(t,qIt,qI,psiT,dz,pars): 7 | # Upper boundary infiltration flux and runoff 8 | # Infiltration capacity such that psiT<=0 9 | 10 | # Get Potential infiltration at the current time step 11 | PotentialInfiltration=np.interp(t,qIt,qI) 12 | 13 | # Surface saturation limited (max flux from Darcy's law): 14 | # Note, KFun always needs an array input, even if only a single item, and 15 | # always writes output as an array. numba doesn't like zero-D arrays, so 16 | # when calculating a single value of K, convert to a float afterwards. 17 | KT=KFun(np.array([psiT]),pars)[0] 18 | InfiltrationCapacity=-KT*(psiT/dz*2.-1.) 19 | # Infiltration=InfiltrationCapacity 20 | 21 | # Actual infiltration flux: 22 | Infiltration=min(PotentialInfiltration,InfiltrationCapacity) 23 | 24 | return Infiltration 25 | -------------------------------------------------------------------------------- /lib/MyNumba.py: -------------------------------------------------------------------------------- 1 | from numba import jit 2 | from numba import types 3 | from numba.typed import Dict 4 | 5 | def MakeDictArray(): 6 | d=Dict.empty( 7 | key_type=types.unicode_type, 8 | value_type=types.float64[:],) 9 | return d 10 | 11 | def MakeDictFloat(): 12 | d=Dict.empty( 13 | key_type=types.unicode_type, 14 | value_type=types.float64,) 15 | return d -------------------------------------------------------------------------------- /lib/PlantFunctions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numba import jit 3 | 4 | @jit(nopython=True) 5 | def PlantFun(psi,t,PEt,PE,z,dz,pars): 6 | # Root water uptake function, based on Feddes model 7 | 8 | PE=np.interp(t,PEt,PE) 9 | # Get root distribution: 10 | rd=np.exp(z/-pars['rootdepth']) 11 | 12 | # Get water stress: 13 | a=np.interp(psi,np.array([-1e6,-150,-4,-0.02,-0.01,1e6]),np.array([0,0,1,1,0,0])) 14 | 15 | # Uptake: 16 | Sp=PE*a*rd/np.sum(rd*dz+1e-30) 17 | 18 | return Sp 19 | 20 | def AEfun(t,psi,PEt,PE,pars,dz,z): 21 | # Use root water uptake model to calculate Actual Evaporation 22 | 23 | AE=np.zeros(len(t)) 24 | for i in range(len(t)): 25 | Sp=PlantFun(psi[i,:],t[i],PEt,PE,z,dz,pars) 26 | AE[i]=np.sum(Sp*dz) 27 | return AE 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/Richards.py: -------------------------------------------------------------------------------- 1 | # Import all of the basic libraries (you will always need these 2 | 3 | # numpy/pandas 4 | import numpy as np 5 | import pandas as pd 6 | 7 | # Import ODE solvers 8 | from scipy.interpolate import interp1d 9 | from scipy.integrate import odeint 10 | 11 | # numba to speed up runtimes 12 | from numba import jit 13 | from lib.MyNumba import MakeDictFloat, MakeDictArray 14 | 15 | # Import a library that contains soil moisture properties and functions 16 | from lib.vanGenuchten_numba import thetaFun 17 | from lib.vanGenuchten_numba import CFun 18 | from lib.vanGenuchten_numba import KFun 19 | 20 | # Import flux functions 21 | # Read run options: 22 | from lib import saveload as sl 23 | BC=sl.json2dict('run/BC.json') 24 | if BC['upper'].lower()=='runoff': 25 | from lib.InfiltrationFunRunoff import InfiltrationFun 26 | print(" Upper boundary condition: Runoff generated to prevent ponding") 27 | elif BC['upper'].lower()=='norunoff': 28 | from lib.InfiltrationFunNoRunoff import InfiltrationFun 29 | print(" Upper boundary condition: zero runoff, 100% infiltration") 30 | else: 31 | print('Warning: no upper boundary condition selected. Abort and try again') 32 | 33 | if BC['lower'].lower()=='watertable': 34 | from lib.DrainageFunWaterTable import DrainageFun 35 | print(" Lower boundary condition: Fixed water table") 36 | elif BC['lower'].lower()=='free': 37 | from lib.DrainageFunFreeDrainage import DrainageFun 38 | print(" Lower boundary condition: Free drainage") 39 | elif BC['lower'].lower()=='noflow': 40 | from lib.DrainageFunNoFlow import DrainageFun 41 | print(" Lower boundary condition: No flow") 42 | else: 43 | print('Warning: no lower boundary condition selected. Abort and try again') 44 | 45 | from lib.PlantFunctions import PlantFun 46 | from lib.PlantFunctions import AEfun 47 | 48 | # Import other functions 49 | from lib.GetSteadyInfiltration import GetSteadyInfiltration 50 | 51 | 52 | # 53 | #################################################################################### 54 | # 55 | 56 | # Function to run model: 57 | def runmodel(IC,BC,pars,grid): 58 | # This block of code sets up and runs the model 59 | 60 | # Grid in space 61 | dz=grid['dz'] 62 | ProfileDepth=grid['ProfileDepth'] 63 | 64 | z=np.arange(dz/2.0,ProfileDepth,dz) 65 | n=z.size 66 | 67 | # Grid in time: 68 | t=grid['t'] 69 | 70 | # Initial conditions 71 | if IC['type']=='SteadyInfiltration': 72 | psiInfiltration=GetSteadyInfiltration(pars,IC['value']) 73 | psi0=np.zeros(len(z))+psiInfiltration 74 | print(" Initial condition: steady-state infiltration profile") 75 | elif IC['type']=='Hydrostatic': 76 | psi0=z-grid['ProfileDepth'] 77 | print(" Initial condition: hydrostatic profile") 78 | elif IC['type']=='Saturation': 79 | Se=IC['value'] 80 | psi0=np.zeros(len(z))-(Se**(-1/pars['m'])-1)**(1/pars['n'])/pars['alpha'] 81 | print(" Initial condition: specified saturation") 82 | elif IC['type']=='psi': 83 | psi0=IC['value'] 84 | print(" Initial condition: specified matric potential") 85 | 86 | # Boundary conditions: 87 | qI=BC['qI'] 88 | qIt=BC['qIt'] 89 | PEt=BC['PEt'] 90 | PE=BC['PE'] 91 | 92 | 93 | # Solve and post process various model configurations: 94 | print("\nSolving Richards' Equation") 95 | psi=odeint(RichardsModelSimple,psi0,t,args=(dz,z,n,qIt,qI,PEt,PE,pars),ml=1,mu=1,mxstep=10000); 96 | ts,state=PostProcessSimple(t,z,psi,grid,IC,BC,pars) 97 | print("Model run successfully") 98 | 99 | return ts,state 100 | 101 | # Richards equation solver: 102 | @jit(nopython=True) 103 | def RichardsModelSimple(psi,t,dz,z,n,qIt,qI,PEt,PE,pars): #,BC): 104 | 105 | # Basic properties: 106 | C=CFun(psi,pars) 107 | 108 | # initialize vectors: 109 | q=np.zeros(n+1) 110 | 111 | # Upper boundary: infiltration rate 112 | q[0]=InfiltrationFun(t,qIt,qI,psi[0],dz,pars) 113 | 114 | # Lower boundary 115 | q[n]=DrainageFun(pars,psi[-1],dz) 116 | 117 | # Internal nodes 118 | i=np.arange(0,n-1) 119 | Knodes=KFun(psi,pars) 120 | Kmid=(Knodes[i+1]+Knodes[i])/2.0 121 | 122 | j=np.arange(1,n) 123 | q[j]=-Kmid*((psi[i+1]-psi[i])/dz-1.0) 124 | 125 | # 126 | Sp=PlantFun(psi,t,PEt,PE,z,dz,pars) 127 | 128 | # Continuity 129 | i=np.arange(0,n) 130 | dpsidt=(-(q[i+1]-q[i])/dz-Sp)/C 131 | 132 | return dpsidt 133 | 134 | # Postprocess and save model outputs 135 | def PostProcessSimple(tmod,z,psi,grid,IC,BC,pars): 136 | # Post process model output to get useful information 137 | 138 | # Get water content 139 | i,j=psi.shape 140 | theta=np.reshape(thetaFun(psi.flatten(),pars),(i,j)) 141 | 142 | # Get total profile storage 143 | S=theta.sum(axis=1)*grid['dz'] 144 | 145 | # Get change in storage [dVol] 146 | dS=np.zeros(S.size) 147 | dS[1:]=np.diff(S)/(tmod[1]-tmod[0]) 148 | 149 | # Get infiltration flux 150 | qI=np.array([InfiltrationFun(tmod[i],BC['qIt'],BC['qI'],psi[i,0],grid['dz'],pars) for i in range(len(tmod))]).squeeze() 151 | 152 | # Get runoff flux 153 | PotI=np.interp(tmod,BC['qIt'],BC['qI']) 154 | runoff=PotI-qI 155 | 156 | # Get discharge flux 157 | qD=np.array([DrainageFun(pars,psiB,grid['dz']) for psiB in psi[:,-1]]) 158 | 159 | # Get evaporation flux 160 | PE=np.interp(tmod,BC['PEt'],BC['PE']) 161 | AE=AEfun(tmod,psi,BC['PEt'],BC['PE'],pars,grid['dz'],z) 162 | 163 | state={} 164 | state['t']=tmod 165 | state['z']=z 166 | state['psi']=psi 167 | state['theta']=theta 168 | 169 | ts=pd.DataFrame(index=tmod) 170 | ts['qI']=qI 171 | ts['runoff']=runoff 172 | ts['qD']=qD 173 | ts['PE']=PE 174 | ts['AE']=AE 175 | ts['S']=S 176 | ts['dS']=dS 177 | 178 | # Print water balance to the screen: 179 | dt=tmod[1]-tmod[0] 180 | print(' Water balance information:') 181 | print('%-30s%.4f mm'%('Infiltration',np.sum(qI[1:])*dt)) 182 | print('%-30s%.4f mm'%('Drainage',np.sum(qD[1:])*dt)) 183 | print('%-30s%.4f mm'%('Actual evaporation',np.sum(AE[1:])*dt)) 184 | print('%-30s%.4f mm'%('Potential evaporation',np.sum(PE[1:])*dt)) 185 | print('%-30s%.4f mm'%('Change in storage',S[-1]-S[0])) 186 | print('%-30s%.4f mm'%('Balance',(np.sum(qI[1:]-qD[1:]-AE[1:])*dt-(S[-1]-S[0])))) 187 | 188 | return ts,state 189 | 190 | -------------------------------------------------------------------------------- /lib/plots.py: -------------------------------------------------------------------------------- 1 | # Import all of the basic libraries (you will always need these) 2 | from matplotlib import pyplot as pl 3 | import numpy as np 4 | import pandas as pd 5 | 6 | def InfiltrationPlot(state,grid): 7 | pl.figure(figsize=(10,5)) 8 | pl.subplot(1,2,1) 9 | pl.plot(state['psi'].T,state['z']) 10 | pl.ylim(grid['ProfileDepth'],0) 11 | pl.ylabel('Depth below ground (m)',fontsize=13) 12 | pl.xlabel('Matric potential (m)',fontsize=13) 13 | pl.grid() 14 | 15 | pl.subplot(1,2,2) 16 | pl.plot(state['theta'].T,state['z']) 17 | pl.ylim(grid['ProfileDepth'],0) 18 | pl.xlabel('Volumetric water content (-)',fontsize=13) 19 | pl.grid() 20 | 21 | def SimpleBalancePlot(ts): 22 | # Shift all fluxes half a step forward 23 | # Only plot the change in storage from the second step 24 | 25 | t=ts.index 26 | dt=t[1]-t[0] 27 | tm=t+dt/2. 28 | pl.plot(t[1:],ts['dS'].iloc[1:],label='dS') 29 | pl.plot(tm,ts['qI'],label='qI') 30 | pl.plot(tm,ts['qD'],label='qD') 31 | pl.plot(tm,(ts['qI']-ts['qD']),'.',label='Net flux') 32 | pl.legend() 33 | 34 | def NumToDate(t,start): 35 | freq=(t[1]-t[0]) 36 | if freq<1/24: 37 | freq='%.8fmin'%(freq*24*60) 38 | elif freq<1: 39 | freq='%.8fH'%(freq*24) 40 | else: 41 | freq='%.8fD'%(freq) 42 | return pd.date_range(start=start,freq=freq,periods=len(t)) 43 | 44 | def DateToNum(t): 45 | dd=t-t[0] 46 | return (dd.days+dd.seconds/86400.).values 47 | 48 | -------------------------------------------------------------------------------- /lib/saveload.py: -------------------------------------------------------------------------------- 1 | # Two functions to save and load a python object 2 | # A. Ireson 3 | import pickle 4 | import json 5 | import numpy as np 6 | 7 | def save(obj,name): 8 | if not(name.lower()[-4:]=='.pkl'): name=name+'.pkl' 9 | with open(name, 'wb') as f: 10 | pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL) 11 | 12 | def load(name): 13 | if not(name.lower()[-4:]=='.pkl'): name=name+'.pkl' 14 | with open(name, 'rb') as f: 15 | return pickle.load(f) 16 | 17 | def dict2json(d,fn): 18 | # Save a dictionary to a json text file 19 | for key in d: 20 | if isinstance(d[key],np.ndarray): 21 | d[key]=d[key].tolist() 22 | f=open(fn,'w') 23 | f.write(json.dumps(d,indent=2)) 24 | f.close() 25 | 26 | def json2dict(fn): 27 | # Load a json text file into a dictionary variable 28 | f=open(fn,'r') 29 | d=json.load(f) 30 | f.close() 31 | for key in d: 32 | if isinstance(d[key],list): 33 | d[key]=np.array(d[key]) 34 | return d 35 | 36 | -------------------------------------------------------------------------------- /lib/vanGenuchten_numba.py: -------------------------------------------------------------------------------- 1 | # These are the van Genuchten (1980) equations 2 | # The input is matric potential, psi and the hydraulic parameters. 3 | # psi must be sent in as a numpy array. 4 | # The pars variable is like a MATLAB structure. 5 | import numpy as np 6 | 7 | from numba import jit 8 | from lib.MyNumba import MakeDictFloat 9 | 10 | @jit(nopython=True) 11 | def thetaFun(psi,pars): 12 | Se=(1+(psi*-pars['alpha'])**pars['n'])**(-pars['m']) 13 | Se[psi>0.]=1.0 14 | return pars['thetaR']+(pars['thetaS']-pars['thetaR'])*Se 15 | 16 | @jit(nopython=True) 17 | def CFun(psi,pars): 18 | Se=(1+(psi*-pars['alpha'])**pars['n'])**(-pars['m']) 19 | Se[psi>0.]=1.0 20 | dSedh=pars['alpha']*pars['m']/(1-pars['m'])*Se**(1/pars['m'])*(1-Se**(1/pars['m']))**pars['m'] 21 | return Se*pars['Ss']+(pars['thetaS']-pars['thetaR'])*dSedh 22 | 23 | @jit(nopython=True) 24 | def KFun(psi,pars): 25 | Se=(1+(psi*-pars['alpha'])**pars['n'])**(-pars['m']) 26 | Se[psi>0.]=1.0 27 | return pars['Ks']*Se**pars['neta']*(1-(1-Se**(1/pars['m']))**pars['m'])**2 28 | 29 | def setpars(): 30 | pars=MakeDictFloat() 31 | pars['thetaR']=float(raw_input("thetaR = ")) 32 | pars['thetaS']=float(raw_input("thetaS = ")) 33 | pars['alpha']=float(raw_input("alpha = ")) 34 | pars['n']=float(raw_input("n = ")) 35 | pars['m']=1-1/pars['n'] 36 | pars['Ks']=float(raw_input("Ks = ")) 37 | pars['neta']=float(raw_input("neta = ")) 38 | pars['Ss']=float(raw_input("Ss = ")) 39 | return pars 40 | 41 | def PlotProps(pars): 42 | import numpy as np 43 | import pylab as pl 44 | import vanGenuchten as vg 45 | psi=np.linspace(-10,2,200) 46 | pl.figure 47 | pl.subplot(3,1,1) 48 | pl.plot(psi,vg.thetaFun(psi,pars)) 49 | pl.ylabel(r'$\theta(\psi) [-]$') 50 | pl.subplot(3,1,2) 51 | pl.plot(psi,vg.CFun(psi,pars)) 52 | pl.ylabel(r'$C(\psi) [1/m]$') 53 | pl.subplot(3,1,3) 54 | pl.plot(psi,vg.KFun(psi,pars)) 55 | pl.xlabel(r'$\psi [m]$') 56 | pl.ylabel(r'$K(\psi) [m/d]$') 57 | #pl.show() 58 | 59 | def HygieneSandstone(): 60 | pars=MakeDictFloat() 61 | pars['thetaR']=0.153 62 | pars['thetaS']=0.25 63 | pars['alpha']=0.79 64 | pars['n']=10.4 65 | pars['m']=1-1/pars['n'] 66 | pars['Ks']=1.08 67 | pars['neta']=0.5 68 | pars['Ss']=0.000001 69 | return pars 70 | 71 | def TouchetSiltLoam(): 72 | pars=MakeDictFloat() 73 | pars['thetaR']=0.19 74 | pars['thetaS']=0.469 75 | pars['alpha']=0.5 76 | pars['n']=7.09 77 | pars['m']=1-1/pars['n'] 78 | pars['Ks']=3.03 79 | pars['neta']=0.5 80 | pars['Ss']=0.000001 81 | return pars 82 | 83 | def SiltLoamGE3(): 84 | pars=MakeDictFloat() 85 | pars['thetaR']=0.131 86 | pars['thetaS']=0.396 87 | pars['alpha']=0.423 88 | pars['n']=2.06 89 | pars['m']=1-1/pars['n'] 90 | pars['Ks']=0.0496 91 | pars['neta']=0.5 92 | pars['Ss']=0.000001 93 | return pars 94 | 95 | def GuelphLoamDrying(): 96 | pars=MakeDictFloat() 97 | pars['thetaR']=0.218 98 | pars['thetaS']=0.520 99 | pars['alpha']=1.15 100 | pars['n']=2.03 101 | pars['m']=1-1/pars['n'] 102 | pars['Ks']=0.316 103 | pars['neta']=0.5 104 | pars['Ss']=0.000001 105 | return pars 106 | 107 | def GuelphLoamWetting(): 108 | pars=MakeDictFloat() 109 | pars['thetaR']=0.218 110 | pars['thetaS']=0.434 111 | pars['alpha']=2.0 112 | pars['n']=2.76 113 | pars['m']=1-1/pars['n'] 114 | pars['Ks']=0.316 115 | pars['neta']=0.5 116 | pars['Ss']=0.000001 117 | return pars 118 | 119 | def BeitNetofaClay(): 120 | pars=MakeDictFloat() 121 | pars['thetaR']=0. 122 | pars['thetaS']=0.446 123 | pars['alpha']=0.152 124 | pars['n']=1.17 125 | pars['m']=1-1/pars['n'] 126 | pars['Ks']=0.00082 127 | pars['neta']=0.5 128 | pars['Ss']=0.000001 129 | return pars 130 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Simple 1D Richards' Equation Model 2 | 3 | Andrew Ireson 4 | 5 | This is a 1D Richards equation, with different upper and lower boundary options, and a Feddes plant water uptake function, driven by potential evapotranspiration. The model is written in python, using numba for JIT compilation. 6 | 7 | To setup and run three example cases, run the makefile. 8 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #1/bin/bash 2 | 3 | echo "########################################################" 4 | echo "# Running $1 " 5 | echo "########################################################" 6 | 7 | mkdir run 8 | cp $1/* run/ 9 | 10 | python RunRichards.py 11 | 12 | mv run/*.pkl $1/. 13 | 14 | rm run/* 15 | rmdir run 16 | echo "" 17 | --------------------------------------------------------------------------------