####################################################################################
##########################  Supplementary_Code.py  #################################
####################################################################################

# Author: Douglas Brinkerhoff, 2016
# License: GNU GPLv3`
# Description: Complete python code for running temperate experiment described in 
# manuscript `Sediment transport drives tidewater glacier periodicity'.  
# Requires python libraries: fenics, numpy, scipy, pickle

####################################################################################
####################################################################################
####################################################################################

from dolfin import *
from pylab import deg2rad,plot,show,linspace,zeros,argmin,array,ion,draw,xlim,ylim,subplots,ones,zeros_like,pause
from numpy.polynomial.legendre import leggauss
from scipy.integrate import trapz,cumtrapz
from scipy.ndimage.filters import gaussian_filter1d
from pylab import argmax,where
import pickle
from scipy.special import gamma

parameters['form_compiler']['cpp_optimize'] = True
parameters['form_compiler']['representation'] = 'quadrature'
parameters['allow_extrapolation'] = True

ffc_options = {"optimize": True, \
               "eliminate_zeros": True, \
               "precompute_basis_const": True, \
               "precompute_ip_const": True}

##########################################################
###############        CONSTANTS       ###################
##########################################################

L = 45000.                  # Characteristic domain length
spy = 60**2*24*365          
thklim = 5.0

zmin = -300.0               # Minimum elevation
zmax = 2200.0               # Maximum elevation

amin = Constant(-8.0)       # Minimum smb
amax = Constant(2.5)        # Maximum smb

c = 2.0                     # Coefficient of exponential decay

amp = 100.0                   # Amplitude of sinusoidal topography 
shore = 0

rho = 917.                  # Ice density
rho_w = 1029.0              # Seawater density
rho_s = 1600.0              # Sediment density
rho_r = 2750.0              # Bedrock density

g = 9.81                    # Gravitational acceleration
n = 3.0                     # Glen's exponent
m = 1.0                     # Sliding law exponent
b = 1e-16**(-1./n)          # Ice hardness
eps_reg = 1e-5              # Regularization parameter

La = 3.35e5                 # Latent heat

l_s = 2.0                   # Sediment thickness at which bedrock erosion becomes negligible 
be = Constant(1e-8)         # Bedrock erosion coefficient
cc = Constant(2e-12)        # Fluvial erosion coefficient
d = Constant(500.0)         # Fallout fraction
h_0 = 0.1                   # Avg. depth of subglacial water layer

k_s_0 = Constant(5000.0)    # Sediment diffusivity

dt_float = 0.1            # Time step
dt = Constant(dt_float)

theta = Constant(0.5)       # Crank-Nicholson parameter

#########################################################
#################      GEOMETRY     #####################
#########################################################

from scipy.interpolate import interp1d
import numpy.random


# Topography
x = linspace(-L,L,101)
N = len(x)
corr_len = 2000.0
corr = zeros((N,N))
for i in range(N):
    for j in range(i+1):
        corr[i,j] = exp(-abs(x[i]-x[j])**2/corr_len**2)
        corr[j,i] = exp(-abs(x[i]-x[j])**2/corr_len**2)

rand_amp = 0.0
cov = rand_amp**2 * corr
z_noise = numpy.random.multivariate_normal(zeros(N),cov)
iii = interp1d(x,z_noise)

# Surface elevation
class Surface(Expression):
  def eval(self,values,x):
    values[0] = (zmax - zmin)*exp(-(x[0]+L)/(L*0.3)) + zmin - amp*(sin(4*pi*x[0]/L)) + thklim

# Bed elevation
class Bed(Expression):
  def eval(self,values,x):
    values[0] = (zmax - zmin)*exp(-(x[0]+L)/(L*0.3)) + zmin - amp*(sin(4*pi*x[0]/L))

# Basal traction
class Beta2(Expression):
  def eval(self,values,x):
    values[0] = 6e3

# Flowline width
class Width(Expression):
  k = 1.0
  theta = 5000.0
  w_min = 3000.0
  xmin = -45000.0
  A = 200e6
  def eval(self,values,x):
    values[0] = (self.A/(gamma(self.k)*self.theta**self.k)*(x[0]-self.xmin)**(self.k-1)*exp(-(x[0]-self.xmin)/self.theta) + self.w_min)/self.w_min + 0.0*cos(2*pi*x[0]*8/L)

##########################################################
################           MESH          #################
##########################################################  

# Define a rectangular mesh
nx = 1000
mesh = IntervalMesh(nx,-L,L)

# Define boundaries 
ocean = FacetFunctionSizet(mesh,0)
ds = ds[ocean]

for f in facets(mesh):
    if near(f.midpoint().x(),3*L/4.):
       ocean[f] = 1
    if near(f.midpoint().x(),-L):
       ocean[f] = 2

#########################################################
#################  FUNCTION SPACES  #####################
#########################################################

Q = FunctionSpace(mesh,"CG",1)
V = MixedFunctionSpace([Q]*3)

ze = Function(Q)
B = interpolate(Bed(),Q)
B_0 = interpolate(Bed(),Q)
beta2 = interpolate(Beta2(),Q)

#########################################################
#################  FUNCTIONS  ###########################
#########################################################

# VELOCITY AND THICKNESS
U = Function(V)
dU = TrialFunction(V)
Phi = TestFunction(V)

u,u2,H = split(U)
phi,phi1,xsi = split(Phi)

un = Function(Q)
u2n = Function(Q)

H0 = Function(Q)
H0.vector()[:] = 10
Hmid = theta*H + (1-theta)*H0

# BEDROCK ELEVATION, SED FLUX, SED THICKNESS

Sedvars = Function(V)
dSedvars = TrialFunction(V)
TestSedvars = TestFunction(V)

h_s,Q_s,B = split(Sedvars)
psi_h,psi_Q,psi_B = split(TestSedvars)

h_s_0 = Function(Q)
Q_s_0 = Function(Q)
rhh = Function(Q)

# Define surface elevation
S = (B + h_s) + Hmid

psi = TestFunction(Q)
dQ = TrialFunction(Q)

# Grounded indicator
grounded=Function(Q)
grounded.vector()[:] = 1
dg = TrialFunction(Q)

# Water flux
Q_w = Function(Q)

# water layer thickness 
h_eff = Function(Q)

ghat = Function(Q)
gl = Constant(0)

# Surface mass balance
climate_factor = Constant(1.0)
adot = climate_factor*(amin + (amax-amin)/(1-exp(-c))*(1.-exp(-c*((S-0)/(2000.-0)))))*grounded + (-0.5*H)*(1-grounded)

width = interpolate(Width(),Q)

########################################################
#################   MOMENTUM BALANCE   #################
########################################################

class VerticalBasis(object):
    def __init__(self,u,coef,dcoef):
        self.u = u
        self.coef = coef
        self.dcoef = dcoef

    def __call__(self,s):
        return sum([u*c(s) for u,c in zip(self.u,self.coef)])

    def ds(self,s):
        return sum([u*c(s) for u,c in zip(self.u,self.dcoef)])

    def dx(self,s,x):
        return sum([u.dx(x)*c(s) for u,c in zip(self.u,self.coef)])

class VerticalIntegrator(object):
    def __init__(self,points,weights):
        self.points = points
        self.weights = weights
    def integral_term(self,f,s,w):
        return w*f(s)
    def intz(self,f):
        return sum([self.integral_term(f,s,w) for s,w in zip(self.points,self.weights)])

def dsdx(s):
    return 1./Hmid*(S.dx(0) - s*H.dx(0))

def dsdz(s):
    return -1./Hmid

# ANSATZ    
coef = [lambda s:1.0, lambda s:1./4.*(5*s**4-1.)]
dcoef = [lambda s:0.0, lambda s:5*s**3]

u_ = [U[0],U[1]]
phi_ = [Phi[0],Phi[1]]

u = VerticalBasis(u_,coef,dcoef)
phi = VerticalBasis(phi_,coef,dcoef)

def eta_v(s):
    return b/2.*((u.dx(s,0) + u.ds(s)*dsdx(s))**2 \
                +0.25*((u.ds(s)*dsdz(s))**2) \
                + eps_reg)**((1.-n)/(2*n))

def membrane_xx(s):
    return (phi.dx(s,0) + phi.ds(s)*dsdx(s))*Hmid*eta_v(s)*(4*(u.dx(s,0) + u.ds(s)*dsdx(s))) + (phi.dx(s,0) + phi.ds(s)*dsdx(s))*H0*eta_v(s)*(2*u(s)/width*width.dx(0))

def shear_xz(s):
    return dsdz(s)**2*phi.ds(s)*Hmid*eta_v(s)*u.ds(s)

def tau_dx(s):
    return rho*g*Hmid*S.dx(0)*phi(s)

def tau_dx_f(s):
    return rho*g*(1-rho/rho_w)*Hmid*Hmid.dx(0)*phi(s)


points = array([0.0,0.4688,0.8302,1.0])
weights = array([0.4876/2.,0.4317,0.2768,0.0476])

vi = VerticalIntegrator(points,weights)

normalx = (B.dx(0) + h_s.dx(0))/sqrt((B.dx(0) + h_s.dx(0))**2 + 1.0)
normalz = sqrt(1 - normalx**2)

# Pressure and sliding law
P_0 = rho*g*H
P_w = Max(-rho_w*g*(B + h_s),0.7*rho*g*H)
N = Max((P_0 - P_w)/(rho*g),50)
tau_b = beta2*(abs(u(1))+1.0)**(1./n-1)*u(1)*(abs(N)+1.0)**(1./n)*(1-normalx**2)*grounded

R = (- vi.intz(membrane_xx) - vi.intz(shear_xz) - phi(1)*tau_b - vi.intz(tau_dx)*grounded - vi.intz(tau_dx_f)*(1-grounded))*dx
F_ocean_x = 1./2.*rho*g*(1-(rho/rho_w))*H**2*Phi[0]*ds(1)
R += F_ocean_x

#############################################################################
##########################  MASS BALANCE  ###################################
#############################################################################

h = CellSize(mesh)

D = h*abs(U[0])/2.

area = Hmid*width
R += ((H-H0)/dt*xsi  - xsi.dx(0)*U[0]*Hmid + D*xsi.dx(0)*Hmid.dx(0) - (adot - un*H0/width*width.dx(0))*(xsi + h/2.*xsi.dx(0)))*dx  + U[0]*area*xsi*ds(1)

J = derivative(R,U,dU)

# Meltrate
me = (tau_b*u(1)/(rho*La) - Min(adot,-1e-16))*grounded

#############################################################################
###########################  Floatation evolution  ##########################
#############################################################################

theta_g = 1.0
ptc_multiplier = Constant(10.0)
dtau = dt*ptc_multiplier
ghat = conditional(Or(And(ge(rho*g*H,Max(P_w,1e-16)),ge(H,1.5*rho_w/rho*thklim)),ge(B_0,1e-16)),1,0)

R_g = psi*(dg - grounded + dtau*(dg*theta_g + grounded*(1-theta_g) - ghat))*dx
A_g = lhs(R_g)
b_g = rhs(R_g)

g0 = gl.compute_vertex_values(mesh)[0]
gl.assign(20000.0)

#############################################################################
###########################  Water Flux  ####################################
#############################################################################

R_Qw = (-dQ*psi.dx(0) + h/2.*dQ.dx(0)*psi.dx(0) - (me - dQ/width*width.dx(0))*(psi + h/2.*psi.dx(0)))*dx + psi*dQ*ds(1)
A_Qw = lhs(R_Qw)
b_Qw = rhs(R_Qw)

#############################################################################
#############################  Sediment evolution  ##########################
#############################################################################


delta = exp(-h_s/l_s)

k_s = k_s_0*(1-exp(-h_s/10.0))

Bdot = -be*tau_b*u(1)*delta  # Rate of bedrock erosion
R_B = psi_B*((B - B_0)/dt - Bdot)*dx    
#R_B = psi_B*(B - B_0)/dt*dx  # Turn off topography evolution

ubar = Q_w/h_eff  # Water velocity from mass conservation

edot = cc/h_eff*ubar**2*(1-delta)  # Erosion rate
ddot = d*Q_s/Q_w                   # Deposition rate

R_Qs = (-psi_Q.dx(0)*Q_s + h/2*psi_Q.dx(0)*Q_s.dx(0) + (psi_Q + h/2.*psi_Q.dx(0))*(ddot - edot - Q_s/width*width.dx(0)))*dx + psi_Q*Q_s*ds(1)

R_h_s = (psi_h*(h_s - h_s_0)/dt + psi_h*(rho_r/rho_s)*Bdot - psi_h*(ddot - edot))*dx 

R_h_s += (psi_h.dx(0)*k_s*(h_s.dx(0) + B.dx(0)))*dx - (k_s*psi_h*(h_s.dx(0) + B.dx(0)))*ds(1)

R_sed = R_B + R_Qs + R_h_s
J_sed = derivative(R_sed,Sedvars,dSedvars)

#####################################################################
#########################  I/O Functions  ###########################
#####################################################################

# For moving data between vector functions and scalar functions 
assigner_inv = FunctionAssigner([Q,Q,Q],V)
assigner     = FunctionAssigner(V,[Q,Q,Q])

#####################################################################
######################  Variational Solvers  ########################
#####################################################################

#Define variational solver for the momentum problem
mass_problem = NonlinearVariationalProblem(R,U,bcs=[],J=J,form_compiler_parameters=ffc_options)
mass_solver = NonlinearVariationalSolver(mass_problem)
mass_solver.parameters['nonlinear_solver'] = 'snes'

mass_solver.parameters['snes_solver']['method'] = 'vinewtonrsls'
mass_solver.parameters['snes_solver']['relative_tolerance'] = 1e-3
mass_solver.parameters['snes_solver']['absolute_tolerance'] = 1e-3
mass_solver.parameters['snes_solver']['error_on_nonconvergence'] = True
mass_solver.parameters['snes_solver']['linear_solver'] = 'mumps'
mass_solver.parameters['snes_solver']['maximum_iterations'] = 10
mass_solver.parameters['snes_solver']['report'] = False

#Define variational solver for the sediment problem
sed_problem = NonlinearVariationalProblem(R_sed,Sedvars,J=J_sed,form_compiler_parameters=ffc_options)
sed_solver = NonlinearVariationalSolver(sed_problem)
sed_solver.parameters['nonlinear_solver'] = 'snes'

sed_solver.parameters['snes_solver']['method'] = 'vinewtonrsls'
sed_solver.parameters['snes_solver']['relative_tolerance'] = 1e-3
sed_solver.parameters['snes_solver']['absolute_tolerance'] = 1e-3
sed_solver.parameters['snes_solver']['error_on_nonconvergence'] = True
sed_solver.parameters['snes_solver']['linear_solver'] = 'mumps'
sed_solver.parameters['snes_solver']['maximum_iterations'] = 10
sed_solver.parameters['snes_solver']['report'] = False

# Bounds
l_thick_bound = project(Constant(thklim),Q)
u_thick_bound = project(Constant(1e4),Q)

l_v_bound = project(-10000.0,Q)
u_v_bound = project(10000.0,Q)


l_bound = Function(V)
u_bound = Function(V)

assigner.assign(l_bound,[l_v_bound]*2+[l_thick_bound])
assigner.assign(u_bound,[u_v_bound]*2+[u_thick_bound])

l_sed_bound = Function(V)
u_sed_bound = Function(V)

assigner.assign(l_sed_bound,[ze]*2+[l_v_bound])
assigner.assign(u_sed_bound,[u_v_bound]*2+[u_v_bound])

######################################################################
#######################   SOLUTION   #################################
######################################################################

### PLOTTING ###
ion()
fig,ax = subplots(nrows=5,sharex=True,figsize=(10,12))
x = mesh.coordinates()
SS = project(S)
BB = B_0.compute_vertex_values()
ph0, = ax[0].plot(x,BB,'b-')
ph8, = ax[0].plot(x,zeros_like(x),'ko',alpha=0.5,markersize=2.0)
ph9, = ax[0].plot(x,zeros_like(x),'yo',alpha=0.5,markersize=2.0)
HH = H0.compute_vertex_values()
ph1, = ax[0].plot(x,BB+HH,'g-')
ph5, = ax[0].plot(x,BB+HH,'r-')
ax[0].set_xlim(-L,L/2.)
ax[0].set_ylim(-1000,2500)
ph6, = ax[0].plot([50000,50000],[-1000,2000],'k-')
ph10, = ax[0].plot([50000,50000],[-1000,2000],'y-')
ph7, = ax[0].plot(x,BB,'k-')
ax[0].set_ylabel('Elevation (m)')

xx = mesh.coordinates()
ph2, = ax[1].plot(xx,H0.compute_vertex_values())
ax[1].set_xlim(-L,L/2.)
ax[1].set_ylim(-4,4)
ax[1].set_ylabel('e_dot - d_dot (m/yr)')

us = project(u(0))
ub = project(u(1))
ph3, = ax[2].plot(xx,us.compute_vertex_values())
ph4, = ax[2].plot(xx,ub.compute_vertex_values())

ax[2].set_xlim(-L,L/2.)
ax[2].set_ylim(0,400)
ax[2].set_ylabel('Velocity (m/yr)')

ph11, = ax[3].semilogy(xx,h_s_0.compute_vertex_values()+1e-2)
ax[3].set_ylim(1e-2,1e2)
ax[3].set_ylabel('h_s (m)')

bb = project(Bdot)
ph12, = ax[4].plot(xx,bb.compute_vertex_values())
ax[4].set_ylim(-0.1,0.005)
ax[4].set_xlabel('x (m)')
ax[4].set_ylabel('B_dot (m/yr)')

#draw()

pause(0.00001)

### DATA STORAGE ###
mass = []
time = []

tdata = []
Hdata = []
hdata = []
Bdata = []
Hgldata = []
ugldata = []
aardata = []
usdata = []
ubdata = []
gldata = []
grdata = []
massdata = []
Qgldata= []
adotplusdata = []
adotminusdata = []
edotdata = []
ddotdata = []
bdotdata = []


# Time interval
t = 0.0
t_end = 10000.

# Restart by uncommenting these files
#File('U.xml') >> U
#File('grounded.xml') >> grounded
#File('Sed.xml') >> Sedvars
#assigner_inv.assign([un,u2n,H0],U)

def time_modifier(t,t_0=1900,t_1=2000,scale=100.,amin=-1,amax=1.5):
    if t<t_0:
        return amin,amax
    if t>=t_0 and t<t_1:
        return amin - (t-t_0)/scale,amax
    else:
        return amin - (t_1-t_0)/scale,amax


while t<t_end:
    # Uncomment for perturbed climate
    #new_amin,new_amax = time_modifier(t,t_0=1900,t_1=2000,scale=100.,amin=-8.0,amax=2.5)
    #amin.assign(new_amin)
    #amax.assign(new_amax)
    time.append(t)

    # Update water thickness
    h_eff.vector()[:] = -(rho/rho_w*H0.vector().array() + B_0.vector().array() + h_s_0.vector().array())
    h_eff.vector()[h_eff.vector()[:]<h_0] = h_0

    # Update grounded status
    solve(A_g == b_g,grounded)

    # Find water flux
    solve(A_Qw == b_Qw,Q_w)

    # Find grounding line position
    gbar = mesh.coordinates()[argmin(grounded.compute_vertex_values().round().astype(bool))-1][0]
    g0 = gl.compute_vertex_values(mesh)[0]
    gl.assign((1-0.5)*g0 + 0.5*gbar)

    # Solve governing equations, with reinitialized initial guess if convergence fails
    if t>10:
        try:
            sed_solver.solve(l_sed_bound,u_sed_bound)
        except:
            sed_solver.parameters['snes_solver']['error_on_nonconvergence'] = False
            assigner.assign(Sedvars,[ze,ze,B_0])
            sed_solver.solve(l_sed_bound,u_sed_bound)
            sed_solver.parameters['snes_solver']['error_on_nonconvergence'] = True
        assigner_inv.assign([h_s_0,Q_s_0,B_0],Sedvars)

    try:
        mass_solver.solve(l_bound,u_bound)
    except:
        mass_solver.parameters['snes_solver']['error_on_nonconvergence'] = False
        assigner.assign(U,[ze,ze,H0])
        mass_solver.solve(l_bound,u_bound)
        mass_solver.parameters['snes_solver']['error_on_nonconvergence'] = True

    assigner_inv.assign([un,u2n,H0],U)

    # Data logging
    tb = project(tau_b)
    tb = tb.compute_vertex_values()

    BB = B_0.compute_vertex_values()
    HH = H0.compute_vertex_values()
    hh = h_s_0.compute_vertex_values()
    H0.vector().array()

    us = project(u(0))
    ub = project(u(1))

    rhh = project(edot-ddot)
    
    ph6.set_xdata([gl(0),gl(0)])
    pp = grounded.compute_vertex_values().round().astype(bool)
    ph8.set_data(x[pp],zeros_like(x[pp]))
    ph9.set_data(x[~pp],zeros_like(x[~pp]))
    ph0.set_ydata(BB)
    ph1.set_ydata((BB + hh + HH)*grounded.compute_vertex_values() + (1-rho/rho_w)*HH*(1-grounded.compute_vertex_values()))
    ph5.set_ydata((BB + hh)*grounded.compute_vertex_values() + (-rho/rho_w*HH)*(1-grounded.compute_vertex_values()))
    ph7.set_ydata(BB+hh)
    ph2.set_ydata(-rhh.compute_vertex_values())

    ph3.set_ydata(us.compute_vertex_values())
    ph4.set_ydata(ub.compute_vertex_values())
    
    ph11.set_ydata(h_s_0.compute_vertex_values()+1e-2)

    bb = project(Bdot)
    ph12.set_ydata(bb.compute_vertex_values())

    pause(0.00001)

    aar = assemble(conditional(gt(adot,0),1,0)*dx)/(gl(0)+L)

    # Update thickness field    
    tdata.append(t)
    Hdata.append(H0.vector().array())
    hdata.append(h_s_0.vector().array())
    Bdata.append(B_0.vector().array())
    gldata.append(gl(0))
    Hgldata.append(H0(gl(0)+1))
    ugldata.append(un(gl(0)+1))

    Qgldata.append(H0(gl(0)+1)*un(gl(0)+1)*width(gl(0)+1))
    adotplusdata.append(assemble(adot*width*conditional(ge(adot,0),1,0)*grounded*dx))
    adotminusdata.append(assemble(adot*width*conditional(lt(adot,0),1,0)*grounded*dx))

    print Qgldata[-1],adotplusdata[-1],adotminusdata[-1]

    aardata.append(aar)
    
    usdata.append(us.vector().array())
    ubdata.append(ub.vector().array())
    grdata.append(grounded.vector().array())
    
    sed_mass = assemble(B*width*dx + rho_s/rho_r*h_s*width*dx)
    massdata.append(sed_mass)

    ee = project(edot)
    dd = project(ddot)
    bb = project(Bdot)

    edotdata.append(ee.vector().array())
    ddotdata.append(dd.vector().array())
    bdotdata.append(bb.vector().array())

    print t,H0.vector().max(),sed_mass
    t+=dt_float

pickle.dump((tdata,Hdata,hdata,Bdata,Hgldata,ugldata,aardata,usdata,ubdata,grdata,gldata,massdata,Qgldata,adotplusdata,adotminusdata,edotdata,ddotdata,bdotdata),open('results/temperate_experiment','w'))

