Hydrostatic Equilibrium test for Richards Equation

This tutorial shows how to use ClimateMachine code to solve Richards equation in a column of soil. We choose boundary conditions of zero flux at the top and bottom of the column, and then run the simulation long enough to see that the system is approaching hydrostatic equilibrium, where the gradient of the pressure head is equal and opposite the gradient of the gravitational head. Note that the SoilWaterModel includes a prognostic equation for the volumetric ice fraction, as ice is a form of water that must be accounted for to1 ensure water mass conservation. If freezing and thawing are not turned on (the default), the amount of ice in the model is zero for all space and time (again by default).

The equations are:

$\frac{ ∂ ϑ_l}{∂ t} = ∇ ⋅ K (T, ϑ_l, θ_i; ν, ...) ∇h( ϑ_l, z; ν, ...).$

$\frac{ ∂ θ_i}{∂ t} = 0$


$t$ is the time (s),

$z$ is the location in the vertical (m),

$T$ is the temperature of the soil (K),

$K$ is the hydraulic conductivity (m/s),

$h$ is the hydraulic head (m),

$ϑ_l$ is the augmented volumetric liquid water fraction,

$θ_i$ is the volumetric ice fraction, and

$ν, ...$ denotes parameters relating to soil type, such as porosity.

We will solve this equation in an effectively 1-d domain with $z ∈ [-10,0]$, and with the following boundary and initial conditions:

$- K ∇h(t, z = 0) = 0 ẑ$

$-K ∇h(t, z = -10) = 0 ẑ$

$ϑ(t = 0, z) = ν-0.001$

$θ_i(t = 0, z) = 0.0.$

where $\nu$ is the porosity.

A word about the hydraulic conductivity: please see the hydraulic functions tutorial for options regarding this function. The user can choose to make it depend on the temperature and the amount of ice in the soil; the default, which we use here, is that K only depends on the liquid moisture content.

Lastly, our formulation of this equation allows for a continuous solution in both saturated and unsaturated areas, following C. S. Woodward , C. N. Dawson (2000).

Preliminary setup

  • Load external packages
using MPI
using OrderedCollections
using StaticArrays
using Statistics
  • Load CLIMAParameters and ClimateMachine modules
using CLIMAParameters
struct EarthParameterSet <: AbstractEarthParameterSet end
const param_set = EarthParameterSet()

using ClimateMachine
using ClimateMachine.Land
using ClimateMachine.Land.SoilWaterParameterizations
using ClimateMachine.Mesh.Topologies
using ClimateMachine.Mesh.Grids
using ClimateMachine.DGMethods
using ClimateMachine.DGMethods.NumericalFluxes
using ClimateMachine.DGMethods: BalanceLaw, LocalGeometry
using ClimateMachine.MPIStateArrays
using ClimateMachine.GenericCallbacks
using ClimateMachine.ODESolvers
using ClimateMachine.VariableTemplates
using ClimateMachine.SingleStackUtils
using ClimateMachine.BalanceLaws:
    BalanceLaw, Prognostic, Auxiliary, Gradient, GradientFlux, vars_state
  • Define the float type desired (Float64 or Float32)
const FT = Float64;
  • Initialize ClimateMachine for CPU
ClimateMachine.init(; disable_gpu = true);

Load plot helpers:

const clima_dir = dirname(dirname(pathof(ClimateMachine)));
include(joinpath(clima_dir, "docs", "plothelpers.jl"));

Set up the soil model

We want to solve Richards equation alone, without simultaneously solving the heat equation. Because of that, we choose a PrescribedTemperatureModel. The user can supply a function for temperature, depending on time and space; if this option is desired, one could also choose to model the temperature dependence of viscosity, or to drive a freeze/thaw cycle, for example. If the user simply wants to model Richards equation for liquid water, the defaults will allow for that. Here we ignore the effects of temperature and freezing and thawing, using the defaults.

soil_heat_model = PrescribedTemperatureModel();

Define the porosity, Ksat, and specific storage values for the soil. Note that all values must be given in mks units. The soil parameters chosen roughly correspond to Yolo light clay, and are stored in SoilParamFunctions. Hydrology specific parameters are further organized and stored in WaterParamFunctions, with the exception of the hydraulic model and hydraulic conductivity model - see the hydraulics tutorial.

wpf = WaterParamFunctions(FT; Ksat = 0.0443 / (3600 * 100), S_s = 1e-3);
soil_param_functions = SoilParamFunctions(FT; porosity = 0.495, water = wpf);

Define the boundary conditions. The user can specify two conditions, either at the top or at the bottom, and they can either be Dirichlet (on ϑ_l) or Neumann (on -K∇h). Note that fluxes are supplied as scalars, inside the code they are multiplied by ẑ.

surface_flux = (aux, t) -> eltype(aux)(0.0)
bottom_flux = (aux, t) -> eltype(aux)(0.0);

Our problem is effectively 1D, so we do not need to specify lateral boundary conditions.

bc = LandDomainBC(
    bottom_bc = LandComponentBC(soil_water = Neumann(bottom_flux)),
    surface_bc = LandComponentBC(soil_water = Neumann(surface_flux)),

Define the initial state function. The default for θ_i is zero.

ϑ_l0 = (aux) -> eltype(aux)(0.494);

Create the SoilWaterModel. The defaults are a temperature independent viscosity, and no impedance factor due to ice. We choose to make the hydraulic conductivity a function of the moisture content ϑ_l, and employ the vanGenuchten hydraulic model with n = 2.0. The van Genuchten parameter m is calculated from n, and we use the default value for α.

soil_water_model = SoilWaterModel(
    moisture_factor = MoistureDependent{FT}(),
    hydraulics = vanGenuchten(FT; n = 2.0),
    initialϑ_l = ϑ_l0,

Create the soil model - the coupled soil water and soil heat models.

m_soil = SoilModel(soil_param_functions, soil_water_model, soil_heat_model);

We are ignoring sources and sinks here, like runoff or freezing and thawing.

sources = ();

Define the function that initializes the prognostic variables. This in turn calls the functions supplied to soil_water_model.

function init_soil_water!(land, state, aux, localgeo, time)
    state.soil.water.ϑ_l = eltype(state)(land.soil.water.initialϑ_l(aux))
    state.soil.water.θ_i = eltype(state)(land.soil.water.initialθ_i(aux))
init_soil_water! (generic function with 1 method)

Create the land model - in this tutorial, it only includes the soil.

m = LandModel(
    boundary_conditions = bc,
    source = sources,
    init_state_prognostic = init_soil_water!,

Specify the numerical configuration and output data.

Specify the polynomial order and vertical resolution.

N_poly = 2;
nelem_vert = 20;

Specify the domain boundaries.

zmax = FT(0);
zmin = FT(-10);

Create the driver configuration.

driver_config = ClimateMachine.SingleStackConfiguration(
    zmin = zmin,
    numerical_flux_first_order = CentralNumericalFluxFirstOrder(),
ClimateMachine.array_type() = Array
┌ Info: Model composition
│     param_set = Main.##387.EarthParameterSet()
│     soil = ClimateMachine.Land.SoilModel{ClimateMachine.Land.SoilParamFunctions{Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,ClimateMachine.Land.WaterParamFunctions{Float64,ClimateMachine.Land.var"#3#15"{Float64,Float64},ClimateMachine.Land.var"#5#17"{Float64,Float64},ClimateMachine.Land.var"#8#20"{Float64,ClimateMachine.Land.var"#14#26"}}},ClimateMachine.Land.SoilWaterModel{Float64,ClimateMachine.Land.SoilWaterParameterizations.NoImpedance{Float64},ClimateMachine.Land.SoilWaterParameterizations.ConstantViscosity{Float64},ClimateMachine.Land.SoilWaterParameterizations.MoistureDependent{Float64},ClimateMachine.Land.SoilWaterParameterizations.vanGenuchten{Float64,Float64,Float64,Float64},Main.##387.var"#15#16",ClimateMachine.Land.var"#38#42"},ClimateMachine.Land.PrescribedTemperatureModel{ClimateMachine.Land.var"#43#44"}}(ClimateMachine.Land.SoilParamFunctions{Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,ClimateMachine.Land.WaterParamFunctions{Float64,ClimateMachine.Land.var"#3#15"{Float64,Float64},ClimateMachine.Land.var"#5#17"{Float64,Float64},ClimateMachine.Land.var"#8#20"{Float64,ClimateMachine.Land.var"#14#26"}}}(0.495, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 0.24, 18.1, 0.053, ClimateMachine.Land.WaterParamFunctions{Float64,ClimateMachine.Land.var"#3#15"{Float64,Float64},ClimateMachine.Land.var"#5#17"{Float64,Float64},ClimateMachine.Land.var"#8#20"{Float64,ClimateMachine.Land.var"#14#26"}}(ClimateMachine.Land.var"#3#15"{Float64,Float64}(1.2305555555555556e-7), ClimateMachine.Land.var"#5#17"{Float64,Float64}(0.001), ClimateMachine.Land.var"#8#20"{Float64,ClimateMachine.Land.var"#14#26"}(ClimateMachine.Land.var"#14#26"()))), ClimateMachine.Land.SoilWaterModel{Float64,ClimateMachine.Land.SoilWaterParameterizations.NoImpedance{Float64},ClimateMachine.Land.SoilWaterParameterizations.ConstantViscosity{Float64},ClimateMachine.Land.SoilWaterParameterizations.MoistureDependent{Float64},ClimateMachine.Land.SoilWaterParameterizations.vanGenuchten{Float64,Float64,Float64,Float64},Main.##387.var"#15#16",ClimateMachine.Land.var"#38#42"}(ClimateMachine.Land.SoilWaterParameterizations.NoImpedance{Float64}(), ClimateMachine.Land.SoilWaterParameterizations.ConstantViscosity{Float64}(), ClimateMachine.Land.SoilWaterParameterizations.MoistureDependent{Float64}(), ClimateMachine.Land.SoilWaterParameterizations.vanGenuchten{Float64,Float64,Float64,Float64}(2.0, 2.6, 0.5), Main.##387.var"#15#16"(), ClimateMachine.Land.var"#38#42"()), ClimateMachine.Land.PrescribedTemperatureModel{ClimateMachine.Land.var"#43#44"}(ClimateMachine.Land.var"#43#44"()))
│     surface = ClimateMachine.Land.SurfaceFlow.NoSurfaceFlowModel()
│     boundary_conditions = ClimateMachine.Land.LandDomainBC{ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.Neumann{Main.##387.var"#11#12"},ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC},ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.Neumann{Main.##387.var"#13#14"},ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC},ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC},ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC},ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC},ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC}}(ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.Neumann{Main.##387.var"#11#12"},ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC}(ClimateMachine.Land.Neumann{Main.##387.var"#11#12"}(Main.##387.var"#11#12"()), ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC()), ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.Neumann{Main.##387.var"#13#14"},ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC}(ClimateMachine.Land.Neumann{Main.##387.var"#13#14"}(Main.##387.var"#13#14"()), ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC()), ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC}(ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC()), ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC}(ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC()), ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC}(ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC()), ClimateMachine.Land.LandComponentBC{ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC,ClimateMachine.Land.NoBC}(ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC(), ClimateMachine.Land.NoBC()))
│     source = ()
│     source_dt = DispatchedTuples.DispatchedTuple{Tuple{},DispatchedTuples.NoDefaults} with 0 entries:
│   default => ()
│     init_state_prognostic = init_soil_water!
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/driver_configs.jl:188
┌ Warning: This table is temporarily incomplete
└ @ ClimateMachine.BalanceLaws /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/BalanceLaws/show_tendencies.jl:51

PDE: ∂_t Y_i + (∇•F_1(Y))_i + (∇•F_2(Y,G)))_i = (S(Y,G))_i
│                 Equation │ Flux{FirstOrder} │ Flux{SecondOrder} │ Source │
│                    (Y_i) │            (F_1) │             (F_2) │    (S) │
│ VolumetricLiquidFraction │               () │       (DarcyFlux) │     () │
│    VolumetricIceFraction │               () │                () │     () │

┌ Info: Establishing single stack configuration for LandModel
│     precision               = Float64
│     horiz polynomial order  = 2
│     vert polynomial order   = 2
│     domain_min              = 0.00 m, 0.00 m, -10.00 m
│     domain_max              = 1.00 m, 1.00 m, 0.00 m
│     # vert elems            = 20
│     MPI ranks               = 1
│     min(Δ_horz)             = 0.50 m
│     min(Δ_vert)             = 0.25 m
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/driver_configs.jl:612

Choose the initial and final times, as well as a timestep.

t0 = FT(0)
timeend = FT(60 * 60 * 24 * 36)
dt = FT(100);

Create the solver configuration.

solver_config =
    ClimateMachine.SolverConfiguration(t0, timeend, driver_config, ode_dt = dt);
┌ Info: Initializing LandModel
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/solver_configs.jl:185

Determine how often you want output.

const n_outputs = 6;

const every_x_simulation_time = ceil(Int, timeend / n_outputs);

Create a place to store this output.

state_types = (Prognostic(), Auxiliary(), GradientFlux())
dons_arr = Dict[dict_of_nodal_states(solver_config, state_types; interp = true)]
time_data = FT[0] # store time data

callback = GenericCallbacks.EveryXSimulationTime(every_x_simulation_time) do
    dons = dict_of_nodal_states(solver_config, state_types; interp = true)
    push!(dons_arr, dons)
    push!(time_data, gettime(solver_config.solver))

Run the integration

ClimateMachine.invoke!(solver_config; user_callbacks = (callback,));
┌ Info: Starting LandModel
│     dt              = 1.00000e+02
│     timeend         = 3110400.00
│     number of steps = 31104
│     norm(Q)         = 1.5621651641231795e+00
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/Driver.jl:802
┌ Info: Update
│     simtime = 1996300.00 / 3110400.00
│     wallclock = 00:01:00
│     efficiency (simtime / wallclock) = 33017.9785
│     wallclock end (estimated) = 00:01:34
│     norm(Q) = 1.5645132716254200e+00
└ @ ClimateMachine.Callbacks /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/Callbacks/Callbacks.jl:75
┌ Info: Finished
│     norm(Q)            = 1.5646872647860275e+00
│     norm(Q) / norm(Q₀) = 1.0016144904013806e+00
│     norm(Q) - norm(Q₀) = 2.5221006628479703e-03
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/Driver.jl:853

Get z-coordinate

z = get_z(solver_config.dg.grid; rm_dupes = true);

Create some plots

We'll plot the moisture content vs depth in the soil, as well as the expected profile of ϑ_l in hydrostatic equilibrium. For ϑ_l values above porosity, the soil is saturated, and the pressure head changes from being equal to the matric potential to the pressure generated by compression of water and the soil matrix. The profile can be solved for analytically by (1) solving for the form that ϑ_l(z) must take in both the saturated and unsaturated zones to satisfy the steady-state requirement with zero flux boundary conditions, (2) requiring that at the interface between saturated and unsaturated zones, the water content equals porosity, and (3) solving for the location of the interface by requiring that the integrated water content at the end matches that at the beginning (yielding an interface location of z≈-0.56m).

output_dir = @__DIR__;

t = time_data ./ (60 * 60 * 24);

    label = string("t = ", string(t[1]), "days"),
    xlim = [0.47, 0.501],
    ylabel = "z",
    xlabel = "ϑ_l",
    legend = :bottomleft,
    title = "Equilibrium test",
    label = string("t = ", string(t[2]), "days"),
    label = string("t = ", string(t[7]), "days"),
function expected(z, z_interface)
    ν = 0.495
    S_s = 1e-3
    α = 2.6
    n = 2.0
    m = 0.5
    if z < z_interface
        return -S_s * (z - z_interface) + ν
        return ν * (1 + (α * (z - z_interface))^n)^(-m)
plot!(expected.(dons_arr[1]["z"], -0.56), dons_arr[1]["z"], label = "expected");

    1e-3 .+ dons_arr[1]["soil.water.ϑ_l"],
    label = "porosity",

save the output.

savefig(joinpath(output_dir, "equilibrium_test_ϑ_l_vG.png"))


This page was generated using Literate.jl.