Soil evaporation

This sets up the simulation that mimicks the coarse sand lab experiment presented in Figures 7 and 8a of Lehmann et al. [14]

using CairoMakie
import SciMLBase
import ClimaTimeSteppers as CTS
using Thermodynamics

using ClimaCore
import ClimaParams as CP
using SurfaceFluxes
using StaticArrays
using Statistics
using Dates
using DelimitedFiles: readdlm
using ClimaUtilities.Utils: linear_interpolation

using ClimaLand
using ClimaLand.Domains: Column
import ClimaLand.Simulations: LandSimulation, solve!
using ClimaLand.Soil
import ClimaLand
import ClimaLand.Parameters as LP
import SurfaceFluxes.Parameters as SFP

FT = Float64;
toml_dict = LP.create_toml_dict(FT);
earth_param_set = LP.LandParameters(toml_dict);
thermo_params = LP.thermodynamic_parameters(earth_param_set);

We model evaporation using Monin-Obukhov surface theory. In our soil model, it is not possible to set the initial condition corresponding to MOST fluxes, but not include radiative fluxes. This is because for land surface models does not make sense to include atmospheric forcing but not radiative forcing.

Because of this, we need to supply downward welling short and long wave radiation. We chose SW = 0 and LW = σT^4, in order to approximately balance out the blackbody emission of the soil which is accounted for by our model. Our assumption is that in the lab experiment there was no radiative heating or cooling of the soil.

Timestepping:

start_date = DateTime(2005)
stop_date = start_date + Day(13)
dt = Float64(900.0)

SW_d = (t) -> 0
LW_d = (t) -> 301.15^4 * 5.67e-8
radiation = PrescribedRadiativeFluxes(
    FT,
    TimeVaryingInput(SW_d),
    TimeVaryingInput(LW_d),
    start_date,
);

Set up atmospheric conditions that result in the potential evaporation rate obsereved in the experiment. Some of these conditions are reported in the paper.

T_air = FT(301.15)
rh = FT(0.38)
esat = Thermodynamics.saturation_vapor_pressure(
    thermo_params,
    T_air,
    Thermodynamics.Liquid(),
)
e = rh * esat
q = FT(0.622 * e / (101325 - 0.378 * e))
precip = (t) -> 0.0
T_atmos = (t) -> T_air
u_atmos = (t) -> 0.44
q_atmos = (t) -> q
h_atmos = FT(0.1)
P_atmos = (t) -> 101325
gustiness = FT(1e-2)
atmos = PrescribedAtmosphere(
    TimeVaryingInput(precip),
    TimeVaryingInput(precip),
    TimeVaryingInput(T_atmos),
    TimeVaryingInput(u_atmos),
    TimeVaryingInput(q_atmos),
    TimeVaryingInput(P_atmos),
    start_date,
    h_atmos,
    toml_dict;
    gustiness = gustiness,
);

Define the boundary conditions

top_bc = ClimaLand.Soil.AtmosDrivenFluxBC(atmos, radiation);
zero_water_flux = WaterFluxBC((p, t) -> 0)
zero_heat_flux = HeatFluxBC((p, t) -> 0)
boundary_fluxes = (;
    top = top_bc,
    bottom = WaterHeatBC(; water = zero_water_flux, heat = zero_heat_flux),
);
[ Info: Warning: No runoff model was provided; zero runoff generated.

Define the parameters n and alpha estimated by matching air entry and Δh_cap values in Lehmann paper

K_sat = FT(225.1 / 3600 / 24 / 1000)
vg_n = FT(8.91)
vg_α = FT(3)
hcm = vanGenuchten{FT}(; α = vg_α, n = vg_n)
ν = FT(0.43)
θ_r = FT(0.043)
S_s = FT(1e-3)
ν_ss_om = FT(0.0)
ν_ss_quartz = FT(1.0)
ν_ss_gravel = FT(0.0)
emissivity = FT(1.0)
z_0m = FT(1e-2)
z_0b = FT(1e-2)
d_ds = FT(0.01)
params = ClimaLand.Soil.EnergyHydrologyParameters(
    toml_dict;
    ν,
    ν_ss_om,
    ν_ss_quartz,
    ν_ss_gravel,
    hydrology_cm = hcm,
    K_sat,
    S_s,
    θ_r,
    emissivity,
    z_0m,
    z_0b,
    d_ds,
);

IC functions

function hydrostatic_equilibrium(z, z_interface, params)
    (; ν, S_s, hydrology_cm) = params
    (; α, n, m) = hydrology_cm
    if z < z_interface
        return -S_s * (z - z_interface) + ν
    else
        return ν * (1 + (α * (z - z_interface))^n)^(-m)
    end
end
function set_ic!(Y, p, t0, model)
    params = model.parameters
    z = model.domain.fields.z
    FT = eltype(Y.soil.ϑ_l)
    Y.soil.ϑ_l .= hydrostatic_equilibrium.(z, FT(-0.001), params)
    Y.soil.θ_i .= 0
    T = FT(296.15)
    ρc_s = @. Soil.volumetric_heat_capacity(
        Y.soil.ϑ_l,
        FT(0),
        params.ρc_ds,
        params.earth_param_set,
    )
    Y.soil.ρe_int =
        Soil.volumetric_internal_energy.(FT(0), ρc_s, T, params.earth_param_set)
end
set_ic! (generic function with 1 method)

Domain - single column

zmax = FT(0)
zmin = FT(-0.35)
nelems = 28
soil_domain = Column(; zlim = (zmin, zmax), nelements = nelems);
z = ClimaCore.Fields.coordinate_field(soil_domain.space.subsurface).z;

Soil model, and create the prognostic vector Y and cache p:

soil = Soil.EnergyHydrology{FT}(;
    parameters = params,
    domain = soil_domain,
    boundary_conditions = boundary_fluxes,
    sources = (),
);

Timestepping:

timestepper = CTS.ARS111();
ode_algo = CTS.IMEXAlgorithm(
    timestepper,
    CTS.NewtonsMethod(
        max_iters = 6,
        update_j = CTS.UpdateEvery(CTS.NewNewtonIteration),
    ),
);
saveat = Second(3600.0)
saving_cb = ClimaLand.NonInterpSavingCallback(start_date, stop_date, saveat)
sv_hr = saving_cb.affect!.saved_values

simulation = LandSimulation(
    start_date,
    stop_date,
    dt,
    soil;
    set_ic! = set_ic!,
    solver_kwargs = (; saveat),
    timestepper = ode_algo,
    user_callbacks = (saving_cb,),
    updateat = Second(3600.0),
    diagnostics = (),
);

Solve

sol_hr = solve!(simulation);

Repeat at lower resolution

zmax = FT(0)
zmin = FT(-0.35)
nelems = 7
soil_domain = Column(; zlim = (zmin, zmax), nelements = nelems);
z = ClimaCore.Fields.coordinate_field(soil_domain.space.subsurface).z;

Soil model, and create the prognostic vector Y and cache p:

soil = Soil.EnergyHydrology{FT}(;
    parameters = params,
    domain = soil_domain,
    boundary_conditions = boundary_fluxes,
    sources = (),
);

Timestepping:

timestepper = CTS.ARS111();
ode_algo = CTS.IMEXAlgorithm(
    timestepper,
    CTS.NewtonsMethod(
        max_iters = 6,
        update_j = CTS.UpdateEvery(CTS.NewNewtonIteration),
    ),
);

saveat = Second(3600.0)
saving_cb = ClimaLand.NonInterpSavingCallback(start_date, stop_date, saveat);
sv_lr = saving_cb.affect!.saved_values;
simulation = LandSimulation(
    start_date,
    stop_date,
    dt,
    soil;
    set_ic! = set_ic!,
    updateat = Second(3600.0),
    solver_kwargs = (; saveat),
    timestepper = ode_algo,
    user_callbacks = (saving_cb,),
    diagnostics = (),
);

Solve

sol_lr = solve!(simulation);

Extract the evaporation at each saved step, and convert to mm/day

evap_hr =
    [
        parent(sv_hr.saveval[k].soil.turbulent_fluxes.vapor_flux_liq)[1] for
        k in 1:length(sol_hr.t)
    ] .* (1000 * 3600 * 24)
evap_lr =
    [
        parent(sv_lr.saveval[k].soil.turbulent_fluxes.vapor_flux_liq)[1] for
        k in 1:length(sol_lr.t)
    ] .* (1000 * 3600 * 24)
evaporation_data = ClimaLand.Artifacts.lehmann2008_evaporation_data();
ref_soln_E = readdlm(evaporation_data, ',')
ref_soln_E_350mm = ref_soln_E[2:end, 1:2]
data_dates = ref_soln_E_350mm[:, 1]
data_e = ref_soln_E_350mm[:, 2];

Goodness of fit metrics: Mean Absolute Error (MAE) and Kling-Gupta Efficiency (KGE)

mae(x, obs) = mean(abs.(x .- obs));
function kge(x, obs)
    σx = std(x)
    σo = std(obs)
    μx = mean(x)
    μo = mean(obs)
    α = σx / σo
    β = μx / μo
    r = mean((x .- μx) .* (obs .- μo)) / (σx * σo)
    return 1 - sqrt((β - 1)^2 + (r - 1)^2 + (α - 1)^2)
end;

We need to interpolate the simulation output to the timestamps of the data

interpolated_lr = [
    linear_interpolation(FT.(sol_lr.t) ./ 3600 ./ 24, evap_lr, data_date)
    for data_date in data_dates
]
interpolated_hr = [
    linear_interpolation(FT.(sol_hr.t) ./ 3600 ./ 24, evap_hr, data_date)
    for data_date in data_dates
];
@show mae(interpolated_lr, data_e)
0.5103765195086495
@show mae(interpolated_hr, data_e)
0.4479072924435174
@show kge(interpolated_lr, data_e)
0.7278547007450462
@show kge(interpolated_hr, data_e)
0.7643283494251139

Figures

fig = Figure(size = (800, 400), fontsize = 22)
ax = Axis(
    fig[1, 1],
    xlabel = "Day",
    ylabel = "Evaporation rate (mm/d)",
    xgridvisible = false,
    ygridvisible = false,
)
CairoMakie.xlims!(minimum(data_dates), maximum(float.(sol_lr.t) ./ 3600 ./ 24))
CairoMakie.lines!(
    ax,
    FT.(data_dates),
    FT.(data_e),
    label = "Data",
    color = :orange,
    linewidth = 3,
)
CairoMakie.lines!(
    ax,
    FT.(sol_lr.t) ./ 3600 ./ 24,
    evap_lr,
    label = "Model, 7 elements",
    color = :blue,
    linewidth = 3,
)
CairoMakie.lines!(
    ax,
    FT.(sol_hr.t) ./ 3600 ./ 24,
    evap_hr,
    label = "Model, 28 elements",
    color = :blue,
    linestyle = :dash,
    linewidth = 3,
)
CairoMakie.axislegend(ax, framevisible = false)

ax = Axis(
    fig[1, 2],
    xlabel = "Mass (g)",
    yticksvisible = false,
    yticklabelsvisible = false,
    xgridvisible = false,
    ygridvisible = false,
)
A_col = π * (0.027)^2
mass_0_hr = sum(sol_hr.u[1].soil.ϑ_l) * 1e6 * A_col
mass_loss_hr = [
    mass_0_hr - sum(sol_hr.u[k].soil.ϑ_l) * 1e6 * A_col for
    k in 1:length(sol_hr.t)
]

mass_0_lr = sum(sol_lr.u[1].soil.ϑ_l) * 1e6 * A_col
mass_loss_lr = [
    mass_0_lr - sum(sol_lr.u[k].soil.ϑ_l) * 1e6 * A_col for
    k in 1:length(sol_lr.t)
]
CairoMakie.lines!(
    ax,
    cumsum(FT.(data_e)) ./ (1000 * 24) .* A_col .* 1e6,
    FT.(data_e),
    color = :orange,
    linewidth = 3,
)
CairoMakie.lines!(ax, mass_loss_lr, evap_lr, color = :blue, linewidth = 3)
CairoMakie.lines!(
    ax,
    mass_loss_hr,
    evap_hr,
    color = :blue,
    linewidth = 3,
    linestyle = :dash,
)

save("evaporation_lehmann2008_fig8.png", fig);


This page was generated using Literate.jl.