Boundary layer model with coupled surface energy balance and soil module#

The aim of this exercise is to include the SEB and soil module in the boundary layer model.

Step 1: Load the SEB model.

import math
import numpy as np
from scipy.optimize import minimize, minimize_scalar
import matplotlib.pyplot as plt

%matplotlib inline


def EB_fluxes(T_0,T_a,f,albedo,G,p,rho,U_L,z,z_0):
    """ This function calculates the energy fluxes from the following quantities:
    
    Input: 
    T_0       : Surface temperature, which is optimized [K]
    f         : Relative humdity as fraction, e.g. 0.7 [-]
    albedo    : Snow albedo [-]
    G         : Shortwave radiation [W m^-2]
    p         : Air pressure [hPa]
    rho       : Air denisty [kg m^-3]
    z         : Measurement height [m]
    z_0       : Roughness length [m]
    
    """
    
    # Some constants
    c_p = 1004.0      # specific heat [J kg^-1 K^-1]
    kappa = 0.40      # Von Karman constant [-]
    sigma = 5.67e-8   # Stefan-Bolzmann constant
    
    # Bulk coefficients 
    Cs_t = np.power(kappa,2.0) / ( np.log(z/z_0) * np.log(z/z_0) )
    Cs_q = np.power(kappa,2.0) / ( np.log(z/z_0) * np.log(z/z_0) )  
    
    # Correction factor for incoming longwave radiation
    eps_cs = 0.23 + 0.433 * np.power(100*(f*E_sat(T_a))/T_a,1.0/8.0)
    
    # Select the appropriate latent heat constant
    L = 2.83e6 # latent heat for sublimation

    # Calculate turbulent fluxes
    H_0 = rho * c_p  * Cs_t * U_L * (T_0-T_a)
    E_0 = rho * ((L*0.622)/p) * Cs_q * U_L * (E_sat(T_0)-f*E_sat(T_a))
    
    # Calculate radiation budget
    L_d = eps_cs * sigma * (T_a)**4
    L_u = sigma * (T_0)**4 
    Q_0 = (1-albedo)*G #+ L_d - L_u

    return (Q_0, L_d, L_u, H_0, E_0)

def E_sat(T):
    """ Saturation water vapor equation """
    Ew = 6.112 * np.exp((17.67*(T-273.16)) / ((T-29.66)))
    return Ew


def optim_T0(x,T_a,f,albedo,G,p,rho,U_L,z,z0):
    """ Optimization function for surface temperature:
    
    Input: 
    T_0       : Surface temperature, which is optimized [K]
    f         : Relative humdity as fraction, e.g. 0.7 [-]
    albedo    : Snow albedo [-]
    G         : Shortwave radiation [W m^-2]
    p         : Air pressure [hPa]
    rho       : Air denisty [kg m^-3]
    z         : Measurement height [m]
    z_0       : Roughness length [m]
    
    """
    
    Q_0, L_d, L_u, H_0, E_0 = EB_fluxes(x,T_a,f,albedo,G,p,rho,U_L,z,z0)
    
    # Get residual for optimization
    res = np.abs(Q_0+L_d-L_u-H_0-E_0)

    # return the residuals
    return res

Step 2: Extend the heat equation to 2D.

import numpy as np
import matplotlib.pyplot as plt
import math


def heat_equation_time_2D(T, dz, dt):
    """ This is a 2D version of the heat equation. It takes an initial 2D-temperature
    field, dz the spacing in the vertical and the timestep dt. The function returns the
    updated temperature field.
    
    T    :: Initial temperature field [K]
    dz   :: Grid spacing in z-direction [m]
    dt   :: Timestep [s]
    """

    # Definitions and assignments
    K   = 1.2e-6               # Thermal conductivity
 
    # Define index arrays (z-direction) 
    Nz = T.shape[0]
    k  = np.arange(1,Nz-1)  # all indices at location i
    kr  = np.arange(2,Nz)   # all indices at location i+1
    kl  = np.arange(0,Nz-2) # all indices at location i-1

    # Create array for new temperature values
    Tnew = T

    # Loop over x-direction
    for x in range(T.shape[1]):

        # Set lower BC - Neumann condition
        T[Nz-1,:] = T[Nz-2,:]
        
        # Update temperature using indices arrays
        Tnew[k,x] = T[k,x] + ((T[kr,x] + T[kl,x] - 2*T[k,x])/dz**2) * dt * K
        
        # Copy the new temperature as old timestep values (used for the 
        # next time loop step)
        T = Tnew


    # return temperature array, grid spacing, and number of integration steps
    return T

Step 3: Test the soil module

# Define necessary variables and parameters
# for the soil module
Nx = 20
Nz = 40
dx = 500
dz = 0.5
x = np.arange(0, Nx*dx, dx)
z = np.arange(0, -Nz*dz, -dz)

# Initial temperature field
T = np.zeros((Nz, Nx))

# Do some test runs
for t in range(4):
    T = heat_equation_time_2D(T, 1, 86400)

plt.figure(figsize=(15,6))
plt.contourf(x, z, T);
../_images/solution_nb_coupled_model_6_0.png

Step 4: Add the boundary layer model.

import numpy as np
import matplotlib.pyplot as plt
import math
    
# --------------------------
# Auxiliary functions
# --------------------------
def saturation_water_vapor(T):
    """ Calculates the saturation water vapor pressure [Pa]"""
    return ( 6.122*np.exp( (17.67*(T-273.16))/(T-29.66) ) )

def hypsometric_eqn(p0, Tv, z):
    """Hypsometric equation to calculate the pressure [hPa] at a certain height[m]
       when the surface pressure is given
       p0 :: surface pressure [hPa]
       Tv :: mean virtual temperature of atmosphere [K]
       z  :: height above ground [m]
    """
    return(p0/(np.exp((9.81*z)/(287.4*Tv) )))

def mixing_ratio(theta, p0, Tv, z):
    """ Calculates the mixing ratio from
        theta :: temperature [K]
        p0    :: surface pressure [hPa]
        Tv    :: mean virtual temperature of atmosphere [K]
        z     :: height [m]
    """
    return(622.97 * (saturation_water_vapor(theta)/(hypsometric_eqn(p0,Tv,z)-saturation_water_vapor(theta))))

def make_plot(data, x, z, levels, title, unit, xlab, zlab, cmap='RdBu_r', size=(18,5)):
    """ Useful function for plotting 2D-fields as contour plot"""
    
    # Create figure
    fig, ax = plt.subplots(1,1,figsize=size);
    cn0 = ax.contourf(x,z,data,10,origin='lower',levels=levels,cmap=cmap);
    
    # Add the colorbar and set ticks and labels
    cbar= fig.colorbar(cn0, ax=ax, orientation='vertical')
    cbar.set_label(label=unit, size=16)
    cbar.ax.tick_params(labelsize=14)
    
    # Add labels and modify ticks
    ax.set_xlabel(xlab, fontsize=14)
    ax.set_ylabel(zlab, fontsize=14)
    ax.set_title(title)
    
    # return the handler to the figure axes
    return ax
    
    
    
def boundary_layer_evolution_moisture(theta, q, u, K, dx, dz, Nx, Nz, dt):
    """ Simple advection-diffusion equation for temperature and moisture. 
    
    theta       :: Initial temperature field [K]
    q           :: Initial moisture field [g/kg]
    u           :: Fluid velocity [m/s]
    K           :: turbulent diffusivity [m^2/s]
    dx          :: Grid spacing in x-direction [-]
    dz          :: Grid spacing in z-direction [-]
    Nx          :: Number of grid cells in x-direction [m]
    Nz          :: Number of grid cells in z-direction [m]
    dt          :: time step in seconds [s]
    """
    
    # Define index arrays 
    # Since this a 2D problem we need to define two index arrays.
    # The first set of index arrays is used for indexing in x-direction. This
    # is needed to calculate the derivatives in x-direction (advection)
    k   = np.arange(1,Nx-1) # center cell
    kr  = np.arange(2,Nx)   # cells to the right
    kl  = np.arange(0,Nx-2) # cells to the left
    
    # The second set of index arrays is used for indexing in z-direction. This
    # is needed to calculate the derivates in z-direction (turbulent diffusion)
    m   = np.arange(1,Nz-1) # center cell
    mu  = np.arange(2,Nz)   # cells above 
    md  = np.arange(0,Nz-2) # cells below

    # --------------------------
    # Init other arrays
    # --------------------------
    cov = np.zeros((Nz, Nx))        # Empty array for the covariances
    adv = np.zeros((Nz, Nx))        # Empty array for the advection term 
    
    # --------------------------
    # Dimensionless parameters
    # --------------------------
    c = (u*dt)/dx
    d = (K*dt)/(dz**2)

    # --------------------------
    # Integrate the model
    # --------------------------
    # Note, not time iteration since this model only updates the temperature field
    # for one timestep using dt
    
    # Set BC top (Neumann condition)
    # The last term accounts for the fixed gradient of 0.01
    theta[Nz-1, :] = theta[Nz-2, :]
    
    # Set top BC for moisture
    q[Nz-1, :] = q[Nz-2, :] 

    # Set BC right (Dirichlet condition)
    theta[:, Nx-1] = theta[:, Nx-2]

    # Set right BC for moisture
    q[:, Nx-1] = q[:, Nx-2]

    # We need to keep track of the old values for calculating the new derivatives.
    # That means, the temperature value a grid cell is calculated from its values 
    # plus the correction term calculated from the old values. This guarantees that
    # the gradients for the x an z direction are based on the same old values.
    old = theta
    old_q = q

    # First update grid cells in z-direction. Here, we loop over all x grid cells and
    # use the index arrays m, mu, md to calculate the gradients for the
    # turbulent diffusion (which only depends on z)
    for x in range(1,Nx-1):
        # Update temperature including lapse rate 
        theta[m,x] = theta[m,x] + ((K*dt)/(dz**2))*(old[mu,x]+old[md,x]-2*old[m,x]) + lapse_rate
        # Moisture transport (turbulent diffusion)
        q[m,x] = q[m,x] + ((K*dt)/(dz**2))*(old_q[mu,x]+old_q[md,x]-2*old_q[m,x])
        # Calculate the warming rate [K/s] by covariance
        cov[m,x] = ((K)/(dz**2))*(old[mu,x]+old[md,x]-2*old[m,x])

    # Then update grid cells in x-direction. Here, we loop over all z grid cells and
    # use the index arrays k, kl, kr to calculate the gradients for the
    # advection (which only depends on x)
    for z in range(1,Nz-1):
        # temperature advection
        theta[z,k] = theta[z,k] - ((u*dt)/(dx))*(old[z,k]-old[z,kl])
        # moisture advection
        q[z,k] = q[z,k] - ((u*dt)/(dx))*(old_q[z,k]-old_q[z,kl])
        # Calculate the warming rate [K/s] by the horizontal advection 
        # Note: Here, we use a so-called upwind-scheme (backward discretization)
        adv[z,k] = - (u/dx)*(old[z,k]-old[z,kl])

    # Calculate new saturation mixing ratio
    qsat = mixing_ratio(theta, 1013, 270, height)

    # Then the relative humidity using qsat
    # Limit the relative humidity to 100 %
    rH = np.minimum(q/qsat, 1)

    # Return results    
    return theta, q, qsat, rH, cov, adv, c, d, np.arange(0, Nx*dx, dx), np.arange(0, Nz*dz, dz)

Step 5: Combine the boundary layer model with the SEB and soil modules.

#--------------------------------------------
# General parameters
#--------------------------------------------
lake_from = 10 # Where does the lake start (grid cell)
lake_to = 20   # where does the lake end (grid cell)
Nx = 50        # how many grid cells in x-direction
dx = 500       # grid spacing in x

# Integration parameters
dt = 60             # timestep in seconds
integration = 4*60 # number of integration steps

#--------------------------------------------
# Define necessary variables and parameters
# for the SEB
#--------------------------------------------
albedo = 0.3     # albedo [-]
G      = 200.0   # Incoming shortwave radiation
rho    = 1.1     # Air density [kg m^-3]
U      = 5.0     # Wind velocity [m/s]
z      = 2.0     # Measurement height [m]
z0     = 1e-3    # Roughness length [m]
p      = 1013    # Pressure [hPa]

#--------------------------------------------
# Define necessary variables and parameters
# for the soil module
#--------------------------------------------
Nz = 10      # Number of grid cells in the soil
dz = 0.1    # grid spacing [m]
Tsoil = 268  # Initial soil temperatures
Tlake = 278  # Lake temperatures

# Initial soil temperature field
T = Tsoil * np.ones((Nz, Nx))
T[:, lake_from:lake_to] = Tlake


# --------------------------------------
# Initial atmospheric temperature field
# --------------------------------------
Nza = 20 # Number of vertical grid cells in the atmosphere
dza = 10  # Grid spacing [m]

# Neutral stratification with lapse rate of 0.01 K/m
# Create a 1D-array with the vertical temperature distribution
# Surface = 268 K, decreasing according to the dry-adiabative lapse rate 0.01 K/m
Tatm = 268           # initial temperature of the atmosphere [K]
lapse_rate = -0.01   # lapse rate [K/m]
theta_vec = np.array([Tatm + lapse_rate * (dza * z) for z in range(Nza)])
theta = np.array([theta_vec,] * Nx).transpose() 

# The lower temperature boundary needs to be updated where there is the lake
theta[0, lake_from:lake_to] = Tlake

# Make height grid
height = np.array([np.arange(0,Nza*dza,dza),] * Nx).transpose()

# --------------------------
# Initialize moisture fields 
# --------------------------
# Init saturation mixing ratio array
qsat = mixing_ratio(theta, 1013, Tatm, height)

# Use qsat to derive mixing ratio at each grid cell assuming a 
# decreasing relative humidity from 70 % at the surfacee to 20 % at the top
q = (qsat.T * np.linspace(0.8, 0.2, Nza)).T

# The lower moisture boundary needs to be updated where there is the lake
# Here, were set the moisture at the lower boundary from the grid cell 50
# to 150 to a mixing ratio of 0.9 times the saturation mixing ratio
q[0, lake_from:lake_to] = 0.95 * qsat[0, lake_from:lake_to] 

# Iterate over time
for t in range(integration):
        
    # Loop over all x-cells and calculate the surface temperature
    for x in range(T.shape[1]):
        
        # Keep the same temperature for the lake (no heating)
        if (x>=lake_from) & (x<=lake_to):
            T[0,x] = Tlake
        else:
            # Set the initial guess for the temperature optimization
            T_0 = T[0,x]

            #-----------------------------------
            # Solve for the surface temperature
            #-----------------------------------
            # The 2m temperature and mixing ratio is taken from the 
            # boundary layer model (theta)
            res = minimize(optim_T0,x0=T_0,args=(theta[1,x],q[1,x]/qsat[1,x],
                    albedo,G,p,rho,U,z,z0),bounds=((None,400),), \
                    method='L-BFGS-B',options={'eps':1e-8})

            # Assign optimized surface temperature to the temperature array
            T[0,x] = res.x

    #-------------------------
    # Solve the heat equation
    #-------------------------
    T = heat_equation_time_2D(T, dz, dt)

    #-------------------------
    # Solve the BL model
    #-------------------------
    # First, set the surface temperature from the soil module
    theta[0,:] = T[0,:]
    theta, q, qsat, rH, cov, adv, c, d, xa, za = boundary_layer_evolution_moisture(theta=theta, 
        q=q, u=U, K=0.5, dx=dx, dz=dza, Nx=Nx, Nz=Nza, dt=dt)

Task 6: Make some nice plots

# Create 2D plot for the covariance
ax = make_plot(theta, x=xa/1000, z=za, levels=11, title='Temperature', unit='K', 
               xlab='Distance [km]', zlab='Height [m]', cmap='RdBu_r')

ax = make_plot(T, x=xa/1000, z=np.arange(0, -Nz*dz, -dz), levels=11, title='Soil temperature', unit='K', 
               xlab='Distance [km]', zlab='Height [m]', cmap='RdBu_r')

# Create 2D plot for the covariance
ax = make_plot(q, x=xa/1000, z=za, levels=11, title='Mixing ratio', unit='g kg$^{-1}$', 
               xlab='Distance [km]', zlab='Height [m]', cmap='YlGnBu')

# Create 2D plot for the covariance
ax = make_plot(qsat, x=xa/1000, z=za, levels=11, title='Saturation mixing ratio', unit='g kg$^{-1}$', 
               xlab='Distance [km]', zlab='Height [m]', cmap='YlGnBu')

# Create 2D plot for the covariance
ax = make_plot(rH*100, x=xa/1000, z=za, levels=11, title='Relative humidity', unit='%', 
               xlab='Distance [km]', zlab='Height [m]', cmap='YlGnBu')

ax.contour(xa/1000, za, rH, levels=[0.95,1.0],colors='red');
../_images/solution_nb_coupled_model_12_0.png ../_images/solution_nb_coupled_model_12_1.png ../_images/solution_nb_coupled_model_12_2.png ../_images/solution_nb_coupled_model_12_3.png ../_images/solution_nb_coupled_model_12_4.png