# Source code for simulation.waveSed

##~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~##
##                                                                                   ##
##  This file forms part of the Badlands surface processes modelling companion.      ##
##                                                                                   ##
##  For full license and copyright information, please refer to the LICENSE.md file  ##
##  located at the project root, or contact the authors.                             ##
##                                                                                   ##
##~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~##
"""
Regional scale model of wave propogation and associated sediment transport.

Important:
The wave model is based on **Airy wave theory** and takes into account wave refraction based on
**Huygen's principle**.

Airy wave theory:

.. image:: img/airy.png
:scale: 90 %
:alt: airy wave theory
:align: center

Huygen's principle:

.. image:: img/huygens.jpg
:scale: 110 %
:alt: Huygens theory
:align: center

The sediment entrainment is computed from wave shear stress and transport according to both
wave direction and longshore transport. Deposition is dependent of shear stress and diffusion.

Note:
The model is intended to quickly simulate the average impact of wave induced sediment transport
at large scale and over geological time period.

"""

import os
import math
import time
import errno
import numpy as np
import pandas as pd

from scipy import interpolate
from scipy.spatial import cKDTree
from scipy.interpolate import interpn
from scipy.ndimage.filters import gaussian_filter

from matplotlib.path import Path
from collections import OrderedDict

if 'READTHEDOCS' not in os.environ:
from random import *
from badlands import waveseds as ocean

import warnings
warnings.simplefilter(action = "ignore", category = FutureWarning)

[docs]class waveSed:
"""
Class for building wave based on **linear wave theory**.

Initialisation function.

Args:
input: class containing XML input file parameters.
recGrid: class describing the regular grid characteristics.
Ce: sediment entrainment coefficient [Default is 1.].
Cd: sediment diffusion coefficient [Default is 30.].
"""
def __init__(self, input, recGrid, Ce, Cd):

# Gravity [L/T2]
self.grav = 9.81
# Sea water density [M/L3]
self.rhow = 1027
# Sediment density [M/L3]
self.rhos = 2650
# Porosity
self.poro = 0.4
# Bottom friction coefficient
self.fric = 0.032
# Kinematic viscosity water (20C) [m2/s]
self.nu = 1.004*1.e-6

self.dia = input.d50
self.tsteps = input.tsteps
self.dsteps = input.dsteps
self.Ce = Ce
self.Cd = Cd
# Maximum eroded thickness for each time step
self.Ero = input.wEro*input.tWave

# Non-dimensional diameter
self.ds = self.dia*np.power(self.grav*(self.rhos/self.rhow-1)/(self.nu*self.nu),1./3.)

# Van Rijn formula
if self.ds <= 4.:
self.tau_cr = 0.24*np.power(self.ds,-1.)
elif self.ds<= 10.:
self.tau_cr = 0.14*np.power(self.ds,-0.64)
elif self.ds<= 20.:
self.tau_cr = 0.04*np.power(self.ds,-0.1)
elif self.ds<= 150.:
self.tau_cr = 0.013*np.power(self.ds,0.29)
else:
self.tau_cr = 0.055

self.tau_cr = self.tau_cr*self.grav*self.dia*(self.rhos-self.rhow)
self.wavebase = input.waveBase
self.resfac = input.resW

minX = recGrid.rectX.min()-input.resW
maxX = recGrid.rectX.max()+input.resW
minY = recGrid.rectY.min()-input.resW
maxY = recGrid.rectY.max()+input.resW
self.dx = float(input.resW)
self.regX = np.arange(minX,maxX+self.dx,self.dx)
self.regY = np.arange(minY,maxY+self.dx,self.dx)

self.nx = len(self.regX)
self.ny = len(self.regY)
self.regZ = np.zeros((self.nx,self.ny),order='F')

self.xi, self.yi = np.meshgrid(self.regX, self.regY)
self.XY = np.column_stack((self.xi.flatten(),self.yi.flatten()))
self.wtree2 = cKDTree(self.XY)

self.sealvl = 0.
self.inland = None
self.depth = None
self.sear = None
self.seac = None
self.landc = None
self.landr = None
self.transX = None
self.transY = None
self.erodep = None
self.waveS = None
self.waveH = None
self.dists = None
self.inds = None
self.wxyTIN = None
self.wtree = None
self.dists2 = None
self.inds2 = None

self.regularlayer = None

return

[docs]    def build_tree(self, xyTIN):
"""
Update wave mesh.

Args:
xyTIN: numpy float-type array containing the coordinates for each nodes in the TIN (in m)
"""

# Update TIN grid kdtree for interpolation
self.wxyTIN = xyTIN
self.wtree = cKDTree(xyTIN)
tindx = xyTIN[1,0] - xyTIN[0,0]
schpts = max(int(self.dx*self.dx/(tindx*tindx)),4)
self.dists, self.inds = self.wtree.query(self.XY, k=schpts)

self.dists2, self.inds2 = self.wtree2.query(self.wxyTIN, k=4)

return

[docs]    def compute_wavesed(self,tNow,input,force,elev,actlay):
"""
This function computes **wave evolution** and wave-induced **sedimentary changes**.

Args:
tNow: current simulation time.
input: class containing XML input file parameters.
force: class containing wave forcing parameters.
elev: elevation of the TIN.
actlay: active layer from TIN.

Returns
-------
tED
numpy array containing wave induced erosion/deposition changes.
nactlay
numpy array containing the updated active layer rock-type content.
"""

self.sealvl = force.sealevel

self._findland(elev, actlay, self.sealvl)

inside = 0
if actlay is not None:
nactlay = np.copy(actlay)
else:
nactlay = None

for w in range(input.waveNb):
if tNow >= input.waveTime[w,0] and tNow < input.waveTime[w,1]:
for clim in range(input.climNb[w]):
perc = input.wavePerc[w][clim]
direction = input.waveWd[w][clim]
height = input.waveWh[w][clim]

# Define wave source direction
source = self._wavesource(direction)

# Compute wave parameters for given condition
self._cmptwaves(source, h0=height, sigma=1.)
t1 = time.process_time()

# Compute sediment transport
self._cmptsed(perc,sigma=1.)

if clim > 0:
avedz += self.erodep
avewH += self.waveH*perc
avewS += self.waveS*perc
else:
inside = 1
avedz = self.erodep
avewH = self.waveH*perc
avewS = self.waveS*perc

# Interpolate to TIN nodes
if inside > 0:
# Interpolate wave mesh information on TIN
h = avewH.flatten('F')
s = avewS.flatten('F')
ed = avedz.flatten('F')
h_vals = h[self.inds2]
s_vals = s[self.inds2]
ed_vals = ed[self.inds2]

with np.errstate(invalid='ignore'):
force.meanH = np.average(h_vals,weights=(1./self.dists2), axis=1)
force.meanS = np.average(s_vals,weights=(1./self.dists2), axis=1)
tED = np.average(ed_vals,weights=(1./self.dists2), axis=1)

onIDs = np.where(self.dists2[:,0] == 0)[0]
if len(onIDs) > 0:
force.meanH[onIDs] = h[self.inds2[onIDs,0]]
force.meanS[onIDs] = s[self.inds2[onIDs,0]]
tED[onIDs] = ed[self.inds2[onIDs,0]]

if actlay is not None:
tal = actlay[:,0] + tED
tal[tal<0.] = 0.
nactlay[:,0] = tal

# al = self.regularlayer.flatten('F')
# al_vals = al[self.inds2]
# with np.errstate(invalid='ignore'):
#     tal = np.average(al_vals,weights=(1./self.dists2), axis=1)
# if len(onIDs) > 0:
#     tal[onIDs] = al[self.inds2[onIDs,0]]
# tal[tal<0.] = 0.
# nactlay[:,0] = tal
# force.meanH = interpn( (self.regX, self.regY), avewH, (self.wxyTIN), method='linear')
# force.meanS = interpn( (self.regX, self.regY), avewS, (self.wxyTIN), method='linear')
# tED = interpn( (self.regX, self.regY), avedz, (self.wxyTIN), method='linear')
else:
force.meanH = np.zeros(len(self.wxyTIN))
force.meanS = np.zeros(len(self.wxyTIN))
tED = np.zeros(len(self.wxyTIN))

return tED, nactlay

def _wavesource(self, dir=0.):
"""
This function defines wave source boundary conditions from input directions.

Args:
dir: wave direction from input condition.

Returns:
- src - numpy array containing the imposed wave boundary conditions.
"""

src = np.zeros(self.regZ.shape)

src.fill(-2)
# East
if dir == 0:
src[-1,:] = 0
# North
elif dir == 90:
src[:,-1] = 0
# West
elif dir == 180:
src[0,:] = 0
# South
elif dir == 270:
src[:,0] = 0
# North-East
elif dir > 0 and dir < 90:
src[-1,-1] = 0
# North-West
elif dir > 90 and dir < 180:
src[0,-1] = 0
# South-West
elif dir > 180 and dir < 270:
src[0,0] = 0
# South-East
elif dir > 270:
src[-1,0] = 0

src[self.landr,self.landc] = -2

return src

def _findland(self, elev, actlay, lvl=0.):
"""
This function computes the land IDs as well as the lake IDs.

Args:
elev: elevation of the TIN.
actlay: active layer from TIN.
lvl: sea-level position.
"""

self.sealvl = lvl

# Interpolate TIN elevation on wave mesh
z_vals = elev[self.inds]
with np.errstate(invalid='ignore'):
nz = np.average(z_vals,weights=(1./self.dists), axis=1)
onIDs = np.where(self.dists[:,0] == 0)[0]
if len(onIDs) > 0:
nz[onIDs] = elev[self.inds[onIDs,0]]
self.regZ = np.reshape(nz,(self.nx,self.ny),order='F')

# Interpolate TIN active layer composition on wave mesh
if actlay is not None:
a_vals = actlay[self.inds,0]
with np.errstate(invalid='ignore'):
na = np.average(a_vals,weights=(1./self.dists), axis=1)
if len(onIDs) > 0:
na[onIDs] = actlay[self.inds[onIDs,0],0]
self.regularlayer = np.reshape(na,(self.nx,self.ny),order='F')

# Specify land/sea areas
tmpxi = self.xi
tmpXY = self.XY

self.inland = np.ones(self.regZ.shape)
self.depth = self.sealvl - self.regZ
self.sear,self.seac = np.where(self.depth>0)
self.inland[self.sear,self.seac]=0
self.landr,self.landc = np.where(self.depth<=0)

return

def _cmptwaves(self, src=None, h0=0., sigma=1., shadow=0, shoalC=0.99):
"""
Waves are transformed from deep to shallow water assuming shore-parallel depth contours. The
orientation of wave fronts is determine by wave celerity and refraction due to depth variations
and travel time in the domain is calculated from Huygen's principle.

Args:
src: position of wave boundary condition.
h0: wave height value along the boundary.
sigma: smoothing coefficient.
shadow: considering shadow effect (1) or no shadow (0) [default is 0].
shoalC: coefficent at attenuation in shoaling region [default is 0.99].
"""

waveC,waveL,travel,self.waveH = ocean.airymodel(self.dx,
shoalC,h0,self.depth,

lkr,lkc = np.where(np.logical_and(travel<0,self.depth>0))
self.waveH[lkr,lkc] *= 0.05
self.waveH[self.waveH>h0*1.25] = h0*1.25

#self.waveU = np.copy(travel)
#waveC = gaussian_filter(waveC, sigma=sigma)
waveL = gaussian_filter(waveL, sigma=sigma)
waveL[self.landr,self.landc] = 0.

# Breaking wave height
Hb = np.zeros(waveC.shape)
# McCowan (1894)
Hb[self.sear,self.seac] = 0.78*self.depth[self.sear,self.seac]

# Wave height [L]
self.waveH = gaussian_filter(self.waveH, sigma=sigma)
self.waveH[self.landr,self.landc] = 0.
breakr,breakc = np.where(self.waveH>Hb)
self.waveH[breakr,breakc] = Hb[breakr,breakc]

# Wave direction [radians]
travel[travel<0.] = travel.max()+10.
waveD = waveD%(np.pi*2)

# Wave maximum orbital velocity [L/T]
waveU = np.zeros(waveC.shape)
tmp3 = np.sqrt(self.grav/self.depth[self.sear,self.seac])
waveU[self.sear,self.seac] = 0.5*self.waveH[self.sear,self.seac]*tmp3
waveU = gaussian_filter(waveU, sigma=sigma)
tmpr1,tmpc1 = np.where(self.depth>self.wavebase)
waveU[tmpr1,tmpc1] = 0.

# Bathymetric contour angle
cDir = cDir%(np.pi*2)

# Wave transport direction
self.transpX = np.cos(waveD)
self.transpY = np.sin(waveD)

# Longshore drift contour
tr,tc = np.where(abs(waveD-cDir)>0.5*np.pi)
cDir[tr,tc] = cDir[tr,tc]+np.pi

# Sediment transport direction
tmpr,tmpc = np.where(np.logical_and(self.depth>0.,self.depth<self.wavebase*0.5))
self.transpX[tmpr,tmpc] = np.cos(cDir[tmpr,tmpc])
self.transpY[tmpr,tmpc] = np.sin(cDir[tmpr,tmpc])
self.transpX[self.landr,self.landc] = 0.
self.transpY[self.landr,self.landc] = 0.

# Wave period [T]
waveT = np.zeros(waveC.shape)
tmpr1,tmpc1 = np.where(self.depth>0.5)
waveT[tmpr1,tmpc1] = waveL[tmpr1,tmpc1]/np.sqrt(self.grav*self.depth[tmpr1,tmpc1])

# Friction factor
fric = np.zeros(waveC.shape)
kbb = 2.*np.pi*self.dia/12.
R = waveU*waveT/(2.*np.pi*kbb)
tmpr2,tmpc2 = np.where(R>0.)
fric[tmpr2,tmpc2] = 1.39*np.power(R[tmpr2,tmpc2],-0.52)

# Shear stress (N/m2)
self.waveS = 0.5*self.rhow*fric*np.power(waveU,2.)
self.waveS = gaussian_filter(self.waveS, sigma=sigma)
self.waveS[self.landr,self.landc] = 0.

return

def _cmptsed(self, perc, sigma=1.):
"""
Compute wave induced sedimentation (erosion/deposition).

Args:
perc: percentage of activity for the considered wave climate
sigma: smoothing coefficient.
"""

# Thickness of entrained sediment [L]
self.waveS[self.waveS<1.e-5] = 0.
r,c=np.where(self.waveS>0.)
Hent = np.zeros(self.waveS.shape)
#Hent[r,c] = self.Ce*np.log(self.waveS[r,c]/self.tau_cr)*perc
Hent[r,c] = -self.Ce*np.log(np.sqrt(np.power(self.tau_cr/self.waveS[r,c],2)))*perc
Hent[Hent<0.] = 0.
r,c=np.where(np.logical_and(Hent>0.,Hent>0.25*self.depth))
Hent[r,c] = 0.25*self.depth[r,c]
if sigma>0:
Hent = gaussian_filter(Hent, sigma=sigma)
Hent[self.landr,self.landc] = 0.
Hent[Hent>self.Ero] = self.Ero

if np.max(Hent)==0.:
self.erodep = np.zeros(self.waveS.shape)
return

# Limit erosion thickness based on active layer composition
if self.regularlayer is not None:
tr1,tc1 = np.where(Hent>self.regularlayer)
if len(tr1)>0:
Hent[tr1,tc1] = self.regularlayer[tr1,tc1]

# Proportion of transport in X,Y direction
tot = np.abs(self.transpX)+np.abs(self.transpY)
tr,tc = np.where(tot>0)
tX = np.zeros(self.waveS.shape)
tY = np.zeros(self.waveS.shape)
tX[tr,tc] = self.transpX[tr,tc]/tot[tr,tc]
tY[tr,tc] = self.transpY[tr,tc]/tot[tr,tc]

# Compute sediment transport
wdz,distw = ocean.wavtransport(self.tsteps,self.depth,Hent,tX,tY)

# Diffuse marine coefficient
area = self.dx*self.dx
CFL = float(area*area/(4.*self.Cd*area))
Cdiff = self.Cd/area

# Perform wave related sediment diffusion
nelev = -self.depth+wdz-Hent

# Compute maximum marine fluxes and maximum timestep to avoid excessive diffusion erosion
ndz = ocean.wavdiffusion(nelev, wdz, Cdiff, self.Ero, CFL, self.dsteps)

# Distribute sediment
if sigma>0.:
val = gaussian_filter(ndz+distw, sigma=sigma)
totval = np.sum(val)
if totval>0.:
frac = np.sum(ndz+distw)/totval
else:
frac = 1.
val = frac*val
else:
val = ndz+distw

dz = val-Hent

r,c = np.where(np.logical_and(dz>0,self.depth<-2.))
dz[r,c] = 0.

if self.regularlayer is not None:
self.regularlayer += dz
r2,c2 = np.where(self.regularlayer<0.)
self.regularlayer[r2,c2] = 0.
dz[r2,c2] = 0.

self.erodep = dz

return