Celia et al. (1990) infiltration problem

This tutorial runs the one-dimensional Richards benchmark from the following paper:

Celia, M. A., Bouloutas, E. T., Zarba, R. L. (1990). A general mass-conservative numerical solution for the unsaturated flow equation. Water Resources Research, 26(7), 1483-1496. DOI: 10.1029/WR026i007p01483

First, we load the required packages.

using HydroTrixi
using SciMLBase
using Trixi

Solve the Richards problem

The setup below follows the elixir_richards_celia_1990.jl example, but we keep a saved time history so the same solution object can drive the animation step later in the tutorial.

asset_dir = docs_generated_dir("celia_1990")
"/home/runner/work/HydroTrixi.jl/HydroTrixi.jl/docs/src/assets/generated/celia_1990"

1. Load the benchmark definition

HydrologicProblemCelia1990 packages the Richards equation, water retention maps, boundary data, spatial domain, and time interval for the standard infiltration problem.

problem = HydrologicProblemCelia1990()
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ HydrologicProblem                                                                                │
│ ═════════════════                                                                                │
│ #spatial dimensions: ……………………………………… 1                                                           │
│ equations: ………………………………………………………………… RichardsEquation1D                                          │
│ initial condition: …………………………………………… initial_condition                                           │
│ boundary conditions: ……………………………………… 2                                                           │
│ │ negative x: ………………………………………………………… BoundaryConditionDirichlet                                  │
│ │ positive x: ………………………………………………………… BoundaryConditionDirichlet                                  │
│ domain: ………………………………………………………………………… ((0.0,), (0.4,))                                            │
│ time interval: ……………………………………………………… (0.0, 360.0)                                                │
│ source terms: ………………………………………………………… nothing                                                     │
│ state to evolved: ……………………………………………… water_content                                               │
│ evolved to state: ……………………………………………… pressure_head_from_water_content                            │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

2. Build the mesh

The benchmark is one-dimensional, so a TreeMesh with five levels of initial refinement gives 32 cells before time integration begins.

mesh = TreeMesh(problem.domain...,
                initial_refinement_level = 5,
                n_cells_max = 30_000)
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ TreeMesh{1, Trixi.SerialTree{1, Float64}}                                                        │
│ ═════════════════════════════════════════                                                        │
│ center: ………………………………………………………………………… [0.2]                                                       │
│ length: ………………………………………………………………………… 0.4                                                         │
│ periodicity: …………………………………………………………… (false,)                                                    │
│ current #cells: …………………………………………………… 63                                                          │
│ #leaf-cells: …………………………………………………………… 32                                                          │
│ current capacity: ……………………………………………… 30000                                                       │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

3. Set up the spatial discretization

We use a polynomial degree of $N = 3$ together with the mixed implicit Richards semidiscretization SemidiscretizationImplicit, using a local DG (LDG) formulation for the parabolic solver.

solver = DGSEM(polydeg = 3)

semi = SemidiscretizationImplicit(mesh, problem, solver;
                                  solver_parabolic = ParabolicFormulationLocalDG())
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ SemidiscretizationImplicit                                                                       │
│ ══════════════════════════                                                                       │
│ #spatial dimensions: ……………………………………… 1                                                           │
│ mesh: ……………………………………………………………………………… TreeMesh{1, Trixi.SerialTree{1, Float64}} with length 63    │
│ equations: ………………………………………………………………… RichardsEquation1D                                          │
│ initial condition: …………………………………………… initial_condition                                           │
│ boundary conditions: ……………………………………… 2                                                           │
│ │ negative x: ………………………………………………………… BoundaryConditionDirichlet                                  │
│ │ positive x: ………………………………………………………… BoundaryConditionDirichlet                                  │
│ source terms: ………………………………………………………… nothing                                                     │
│ solver: ………………………………………………………………………… DG                                                          │
│ parabolic solver: ……………………………………………… ParabolicFormulationLocalDG                                 │
│ temporal operator: …………………………………………… TemporalOperatorConstitutive                                │
│ state to evolved: ……………………………………………… water_content                                               │
│ evolved to state: ……………………………………………… pressure_head_from_water_content                            │
│ total #DOFs per field: ………………………………… 128                                                         │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

4. Solve and keep a time history

The Richards problem is stiff, so default_algorithm selects the Rodas5P Rosenbrock method from the SciML ecosystem. The time step is adaptive, with an initial step size of $\Delta t = 1.0 \times 10^{-2}$ seconds. We specify saveat = 0.0:6.0:360.0 to save a solution every six seconds, which will be used to create an animation in the next tutorial section. The adaptive = true keyword below controls time adaptivity only; see examples/elixir_richards_celia_1990_amr.jl for mesh adaptivity based on water content.

ode = semidiscretize(semi, problem.tspan)

sol = solve(ode,
            default_algorithm(semi);
            dt = 1.0e-2,
            adaptive = true,
            saveat = 0.0:6.0:360.0,
            save_everystep = false,
            maxiters = typemax(Int))

println("Solved Richards problem to t = $(sol.t[end]) with $(length(sol.t)) saved states.")
Solved Richards problem to t = 360.0 with 61 saved states.

Plot the final pressure head profile

Now that the solve is complete, we load the optional plotting packages. HydroTrixi declares these packages as weak dependencies, so a fresh package environment may not have them installed yet. If using CairoMakie fails, install the plotting packages in the active environment before continuing:

using Pkg
Pkg.add(["CairoMakie", "LaTeXStrings"])
using CairoMakie
using LaTeXStrings

The mixed Richards formulation stores both evolved and state variables. The pressure head lives in the state block, so we plot component = 2 with plot_solution_1d. The output file is written into the docs asset directory prepared by the build.

plot_path = joinpath(asset_dir, "richards_celia_1990_pressure_head.png")

_ = plot_solution_1d(sol;
                     component = 2,
                     xlabel = L"$z$ (m)",
                     ylabel = L"$\psi$ (m)",
                     ylims = (-0.65, -0.15),
                     output_path = plot_path)

println("Saved final-time plot to $(plot_path)")
Saved final-time plot to /home/runner/work/HydroTrixi.jl/HydroTrixi.jl/docs/src/assets/generated/celia_1990/richards_celia_1990_pressure_head.png

Final-time pressure head profile

Render the GIF animation

Because the solve already stored a time history, animate_solution_1d only needs to read the saved states and render them.

animation_path = joinpath(asset_dir, "richards_celia_1990_pressure_head.gif")

_ = animate_solution_1d(sol;
                        component = 2,
                        xlabel = L"$z$ (m)",
                        ylabel = L"$\psi$ (m)",
                        ylims = (-0.65, -0.15),
                        output_path = animation_path,
                        framerate = 20)

println("Saved animation to $(animation_path)")
Saved animation to /home/runner/work/HydroTrixi.jl/HydroTrixi.jl/docs/src/assets/generated/celia_1990/richards_celia_1990_pressure_head.gif

Pressure head animation