Groundwater Model
1. Overview
The groundwater component of HydroPol2D represents the coupled dynamics of:
- vadose-zone storage,
- recharge from infiltration,
- lateral saturated groundwater flow,
- groundwater exfiltration to the surface,
- and groundwater interaction with river cells.
The current implementation combines three complementary components:
-
a layered vadose/recharge module, which moves water through near-surface, root-zone, and transmission-zone storage using a reduced Richards-consistent Darcy closure based on the van Genuchten–Mualem constitutive relations,
-
a capillary-rise exchange module, which can move water upward from shallow groundwater into available vadose storage, and
-
a two-dimensional Boussinesq groundwater flow solver, which routes saturated flow laterally across the domain and returns exfiltration to the land surface when the groundwater table rises above the local surface elevation.
This structure allows HydroPol2D to represent dynamic coupling between:
- surface water,
- vadose storage,
- groundwater recharge,
- saturated groundwater flow,
- and saturation-excess return flow.
Figure 1. The default groundwater scheme on three model cells. Recharge and capillary rise move the water table h every surface step. Lateral Boussinesq flow runs on the groundwater clock. Exfiltration and saturation excess return water where h reaches the ground.
1.1 Two subsurface schemes
HydroPol2D has two subsurface schemes. This page describes the default one.
| Scheme | Aquifer | Switch | Page |
|---|---|---|---|
| Default | Inside the soil column; the aquifer base is the ground minus the soil depth | default | this page |
| Layered | Soil slots over regolith slots, down to the depth to bedrock (capped at 50 m) | Python vertical.subsurface.scheme: layered; MATLAB flag_layered_subsurface = 1 | Layered Subsurface |
Both schemes can feed the optional deep aquifer described in Section 13.
On this page, "layered vadose" means the three soil stores of the default scheme (near surface, root zone, transmission). It is not the layered subsurface scheme.
2. General Conceptual Structure
The groundwater model is coupled with two clocks. Vadose-zone exchange is evaluated every surface-water time step because infiltration and evapotranspiration need fast soil-moisture feedback. This net exchange changes the water-table heads in every surface-water step. The groundwater scheduler is always asynchronous, and only the lateral groundwater flow, when it is on, waits for it. The lateral update usually runs on a daily target interval unless stability or head-change limits require a shorter one.
The logic is:
- compute the water table position from the current groundwater head,
- determine the maximum unsaturated storage permitted by the current water table depth,
- compute drainage/recharge from vadose layers to groundwater during the current surface step,
- compute capillary rise from groundwater to the vadose layers when enabled,
- change the water-table heads by the net exchange in every surface-water step,
- compute groundwater exfiltration to the surface where the head rises above the land surface,
- solve lateral groundwater flow using a 2D Boussinesq equation when lateral flow is on (
flag_baseflow = 1in MATLAB,vertical.groundwater_lateral_flow: truein HydroPol2D-Python) and the scheduler is due, - add the exfiltration of the lateral update to the surface,
- correct vadose storage if the rising water table reduces the available unsaturated storage,
- update surface water depth accordingly.
The principal variables are:
- = groundwater hydraulic head
- = aquifer base elevation or bedrock elevation
- = saturated thickness
- = recharge rate to groundwater
- = exfiltration flux from groundwater to the surface
- = specific yield
- = saturated hydraulic conductivity
2.1 Asynchronous scheduler
The asynchronous scheduler is always active when groundwater modeling is enabled. It decides only when the lateral Boussinesq update runs. The principal controls are:
| Control | Default | Meaning |
|---|---|---|
flag_groundwater_async | 1 | Not read: the lateral update always follows the scheduler (Section 7.1). |
groundwater_target_dt_s | 86400 | Longest interval between lateral updates, in seconds (one day). HydroPol2D-Python also reads it from a legacy workbook in minutes, as groundwater_target_dt_min (1440). |
groundwater_min_dt_s | 60 | Shortest interval between lateral updates and smallest Boussinesq substep, in seconds. The legacy workbook key is groundwater_min_dt_min (1). |
groundwater_max_head_change_m | 0.25 | HydroPol2D-Python: forces a lateral update when the water table has moved by this amount since the last one. The MATLAB default scheme does not use it (Section 7.1). |
groundwater_head_tolerance_m | 0.001 | Absolute head tolerance of the adaptive Boussinesq substeps, in metres. MATLAB also uses it in the head-change trigger. |
groundwater_relative_tolerance | 0.001 | Relative tolerance on the saturated thickness, used together with groundwater_head_tolerance_m. |
groundwater_courant | 0.25 | Stability factor for the Boussinesq groundwater timestep estimate. |
flag_capillary_rise | 1 | Enables capillary rise from the water table into the root zone when layered soil storage is present. |
In HydroPol2D-Python these settings sit in the vertical block, and capillary rise is vertical.capillary_rise (default true).
The scheduler keeps the time since the last lateral update, the substep suggested by the last adaptive Boussinesq run, the number of lateral updates, and three depths per cell. The pending net exchange holds the exchange of the current step only. The local update adds it to the heads and sets it to zero in the same step. The pending recharge and the pending capillary rise are running totals since the last lateral update and are cleared when that update runs. For the head-change trigger, HydroPol2D-Python also keeps the head change applied since the last lateral update, and the MATLAB code keeps the head of that update. The Boussinesq solver receives no recharge term, because the heads already include the net exchange. Without lateral flow, the scheduler only clears these totals, in every step in MATLAB and when the trigger is due in HydroPol2D-Python.
3. Water Table Position and Unsaturated-Zone Capacity
HydroPol2D determines the position of the water table relative to the land surface using the current groundwater head.
Let:
- = land-surface elevation
- = soil depth
- = groundwater hydraulic head
The saturated thickness above the bedrock is computed as
The depth from the land surface to the water table is then
with the constraint
where:
- = saturated thickness measured upward from the aquifer base
- = depth to water table below the ground surface
This variable is central to the coupling between vadose storage and groundwater.
3.1 Maximum unsaturated storage
The maximum water that can be stored in the unsaturated zone is defined using the current active storage above residual water content convention:
where:
- = maximum unsaturated storage
- = saturated volumetric water content
- = residual volumetric water content
Thus, as the water table rises, the vadose zone shrinks and the storage capacity available for infiltration decreases. When layered soil storage is active, the code evaluates this limit through the explicit capacities of the near-surface, root-zone, and transmission-zone stores after truncation by the current water-table position.
4. Default Layered Vadose-to-Groundwater Exchange
Recharge is computed from the current vadose storage after the infiltration module has already moved water from the surface into the soil. This avoids double-counting infiltration. The current default pathway is layered:
- near-surface storage,
- root-zone storage,
- transmission-zone storage,
- groundwater recharge.
4.1 Surface-step coupling
Vadose exchange is evaluated every surface-water step because infiltration and evapotranspiration need immediate soil-moisture feedback. In the current implementation, infiltration is first accepted by the infiltration module, stored in the vadose layers, and only then allowed to drain downward toward groundwater.
4.2 Layerwise constitutive state
For a generic vadose layer with active storage and thickness , HydroPol2D evaluates
with
where:
- = layer water content
- = pervious fraction of the cell ; the layer water sits in the pervious part, and is a depth over the whole cell
- = layer effective saturation
- = layer unsaturated conductivity
- = Mualem pore-connectivity parameter , fixed at 0.5 in this drainage step in both codes
This is the same van Genuchten-Mualem constitutive family used by the infiltration module.
4.3 Downward drainage cascade
The layered recharge pathway is computed as a downward cascade:
- near-surface drainage goes to the root zone when root storage exists,
- root-zone drainage goes to the transmission zone when transmission storage exists,
- transmission-zone drainage becomes groundwater recharge.
Conceptually, the drainage depth from a layer over a time step can be summarized as
subject to:
- water available in the donor layer,
- remaining capacity in the receiving layer,
- and the current layer thickness and soil parameters.
The net downward recharge reaching groundwater is then
where is the total transmission of water into groundwater during the current surface step. Direct drainage from the near-surface or root zone to groundwater occurs only when the lower vadose layers are absent.
4.4 Legacy fallback formulation
The MATLAB code still contains the older representative-bucket recharge closure for cases where the explicit layered storage data are unavailable. In that fallback mode, a single vadose storage state is converted to a representative water content and drained with a Darcy-like closure. HydroPol2D-Python has no such fallback and always uses the three layers. That formulation should be treated as legacy/fallback, not the primary theory path for the current model.
5. Optional Capillary Rise
When flag_capillary_rise = 1, HydroPol2D allows shallow groundwater to move upward into available vadose storage. The current implementation is a storage-limited van Genuchten-Mualem transfer into the root zone, not a full Richards-equation solution.
The upward exchange increases with:
- shallower water-table depth,
- larger storage deficit in the receiving layer,
- and larger saturated conductivity of the root zone.
Over a surface step , the upward capillary transfer is
with , and , where:
- is the deficit of the receiving layer below field capacity, , with and the storage and capacity of that layer
- is the field-capacity suction head,
capillary_field_capacity_head_m(default 3.3 m) - is the gap between the water table and the bottom of the root zone
- is the groundwater stored in the cell
- , , and are the van Genuchten-Mualem parameters of the soil
In the current code, capillary rise fills only the root zone, or the near-surface layer where the roots end inside it and there is no separate root zone. The transmission zone receives no capillary rise.
The net vadose-groundwater exchange of each surface-water step is therefore
where is the total upward capillary-rise flux.
6. Two-Dimensional Boussinesq Groundwater Flow Model
After the local head update, and only when lateral flow is on and the scheduler is due, HydroPol2D routes groundwater laterally using the function Boussinesq_2D_explicit. HydroPol2D-Python uses the same discretization.
This solver represents the saturated groundwater system through a depth-integrated unconfined flow approximation.
6.1 Governing equation
The groundwater routing follows the 2D Boussinesq equation in conservative form:
where:
- = specific yield
- = groundwater hydraulic head
- = groundwater flux vector
- = recharge rate
HydroPol2D evaluates this equation numerically by computing fluxes at cell interfaces and then updating the groundwater head explicitly. In the code the recharge term is applied separately. The net exchange changes the heads in every surface-water step (Section 7.1), and the lateral solver is called with over the time elapsed since its last run.
6.2 Saturated thickness
The active saturated thickness is defined as
where:
- = saturated thickness
- = aquifer base elevation or bedrock elevation
This enforces the physical condition that no saturated storage exists below the aquifer base.
6.3 Interface fluxes
HydroPol2D computes groundwater fluxes at cell interfaces using Darcy’s law with saturated thickness weighting.
For the direction:
and for the direction:
where:
- = fluxes per unit width
- = hydraulic conductivity at cell interfaces
- = interface saturated thicknesses
In the implementation, the interface hydraulic conductivity is taken as the arithmetic average of the conductivities in adjacent cells:
and the interface saturated thickness is likewise averaged:
6.4 Conservative flux limiting
To avoid numerical instability and nonphysical depletion of groundwater storage, HydroPol2D applies a flux limiter so that no interface can carry more water than its donor cell holds during a substep.
For example, in the direction, the maximum allowed flux is
and the final interface flux is limited as
A similar expression is used in the direction.
Because the limit applies to each interface separately, a cell that drains through several interfaces can still fall below the aquifer base in one substep. The solver then sets the head to , and the added water shows up as a residual in the mass-balance check of Section 14.
6.5 Divergence of groundwater fluxes
The divergence of the flux field is then computed from the interface fluxes as
using conservative finite differences on the raster grid.
6.6 Explicit predictor–corrector update
The Boussinesq solver uses an explicit predictor–corrector strategy.
Predictor step
A first estimate of head is computed using the current fluxes:
Corrector step
Fluxes are recomputed from the predictor state, averaged, and used in the final update:
where:
- = predictor hydraulic head
- = final hydraulic head after the time step
- = average of predictor and corrector interface fluxes
This predictor–corrector procedure improves stability and reduces numerical diffusion relative to a simple explicit Euler update.
7. Groundwater Update Timing and Adaptive Time Stepping
HydroPol2D uses a two-level groundwater time-step logic.
7.1 Outer asynchronous scheduler
Vadose exchange changes the water-table heads in every surface-water step. The groundwater scheduler is always asynchronous, and only the lateral groundwater flow, when it is on, waits for it. The lateral update runs at the end of the first surface step in which one of these conditions holds:
- the time since the last lateral update reaches , where is
groundwater_target_dt_s, is the stability cap of Section 7.2, is the substep suggested by the last adaptive Boussinesq run, and isgroundwater_min_dt_s, - the water table has moved far enough since the last lateral update,
- the surface step is the last one of the run.
The head-change test differs between the codes. HydroPol2D-Python compares the largest head change applied since the last lateral update with groundwater_max_head_change_m (0.25 m). The MATLAB default scheme does not use that setting. It runs the update when, in any cell, , where is the head after the last lateral update, is groundwater_head_tolerance_m and is groundwater_relative_tolerance (both 0.001 by default). The lateral solver then advances over the whole time elapsed since its last run.
The scheduler cannot be switched off. Neither HydroPol2D-Python nor the MATLAB code reads flag_groundwater_async, so a value of 0 does not force a lateral update every surface step. A shorter groundwater_target_dt_s makes the lateral updates more frequent.
7.2 Explicit groundwater stability cap
The outer scheduler cap is based on an explicit diffusivity estimate for the unconfined Boussinesq equation:
where:
- = groundwater diffusivity
- = groundwater Courant/safety factor
- = maximum domain diffusivity over the active groundwater cells
This is the cap used by the current estimate_groundwater_stable_timestep logic.
7.3 Inner Boussinesq substepping
When a lateral update is triggered, the solver receives no recharge term, because the net exchange has already changed the heads. It advances the heads over the elapsed interval with adaptive predictor-corrector substeps. Each substep is at most the stability cap of Section 7.2 and at least groundwater_min_dt_s. Both codes control the error by step doubling. A substep is accepted when one full step and two half steps differ by no more than in every cell; otherwise it is shortened and repeated. The next substep the solver would have tried is kept as the suggested step for the next lateral update.
In the MATLAB code, each of these substeps is further split by a velocity-based condition of the form
where:
- are Darcy groundwater velocities
- is the internal velocity-based safety factor, the same
groundwater_courant
HydroPol2D-Python takes one predictor-corrector update per substep and has no velocity condition.
The result is a two-level strategy:
- an outer scheduler cap based on explicit groundwater stability and allowed head change,
- an inner adaptive predictor-corrector substepping loop inside the Boussinesq solver.
8. Boundary Conditions
The groundwater solver supports:
- Dirichlet boundary conditions,
- no-flow boundary conditions,
- domain masking.
8.1 Dirichlet boundaries
If a Dirichlet mask is provided, the head is prescribed directly:
on the selected boundary cells, where:
- = prescribed boundary head
8.2 No-flow boundaries
No-flow boundaries are enforced by zero-gradient conditions at the domain perimeter. In practice, HydroPol2D sets the flux to zero across every interface with a cell outside the domain and across the outer edge of the grid, unless the edge cell has a prescribed head. This is a discrete Neumann condition; the code does not copy heads into the perimeter cells.
8.3 Catchment mask
All groundwater calculations are restricted to the valid computational domain. Cells outside the catchment mask are set to NaN.
9. Groundwater Exfiltration to the Surface
After updating the groundwater head, HydroPol2D computes exfiltration where the groundwater table rises above the effective land surface.
The surface elevation used for exfiltration is
where:
- = effective land-surface elevation for groundwater emergence
Exfiltration is then computed as
where:
- = exfiltration flux
This expression means that if groundwater head exceeds the land surface, the excess saturated storage is expelled upward and transferred to the surface water system.
The groundwater head is then clipped so that
which prevents groundwater from remaining above the land surface after exfiltration has been accounted for.
10. River–Aquifer Exchange
Both codes can exchange water between the aquifer and the river cells. The exchange is optional and off by default. The switch is flag_river_groundwater_exchange in MATLAB and flags.river_groundwater_exchange in HydroPol2D-Python. In each river cell, the exchange follows a linear riverbed law,
where:
- = exchange rate per unit cell area, positive from the aquifer to the river
- = riverbed hydraulic conductivity
- = riverbed area, the river width times the cell size and at most the cell area
- = riverbed thickness,
riverbed_thickness_m(default 0.5 m) - = water-surface elevation in the river cell
Each surface-water step solves this law exactly for two linked stores, the aquifer of the cell and its surface water, and limits the volume to the water the donor store holds. The exchange is added to the net exchange, so it changes the water-table head in the same step as recharge and capillary rise.
11. Saturation-Excess Correction After Water Table Rise
After the heads change in a surface-water step, HydroPol2D recomputes the water-table depth and therefore the new maximum unsaturated storage:
If the updated vadose storage exceeds this new capacity, the excess is returned to the surface as saturation-excess water:
The correction is then:
and surface water depth is increased by
In the layered default implementation, the correction is applied layer by layer. The three layer capacities are recomputed from the new water table, each layer keeps at most its own capacity, and the sum of the excesses goes to the surface. HydroPol2D-Python applies the correction after the local update and again after a lateral update; the MATLAB code applies it once per step, after both. This is a very important part of HydroPol2D because it guarantees mass consistency between the vadose and saturated zones when the water table rises during a time step.
12. Coupling Back to Surface Water
The groundwater model feeds back to the surface water system in two ways:
- exfiltration, through ,
- saturation excess, through .
The surface water depth is updated as
with the appropriate unit conversion in the code from to .
This means that HydroPol2D allows groundwater to actively influence flood generation and near-surface wetness, rather than treating it as a disconnected lower boundary process.
13. Deep Aquifer
The optional deep aquifer represents slow groundwater below the modeled aquifer. It is semi-distributed: each subbasin has one linear reservoir.
13.1 Structure
A fixed fraction of the downward recharge goes to the deep aquifer before the rest reaches the water table. The recharge of all cells in subbasin feeds reservoir . The reservoir releases
where:
- = storage of reservoir
- = response time , the same for all subbasins
- = deep return flow
Two rasters define the layout. The subbasin raster says which cells feed which reservoir. The channel-weight raster says where each reservoir discharges: the return flow is spread over the river cells of its own subbasin, in proportion to the weight.
13.2 Update
For an input rate over a step , the reservoir uses the exact solution of the linear-reservoir equation:
and the discharged volume is
The update is conservative and does not depend on the step length.
13.3 Starting state
Because the outflow is storage over , the starting storage follows from a starting outflow per unit area :
where is the active area of subbasin . Give at most one of the following:
| Setting | Meaning | |
|---|---|---|
deep_aquifer_initial_baseflow_mm_day | Deep baseflow on the start date, for example from a gauge | as given |
deep_aquifer_mean_recharge_mm_year | Long-term mean recharge ; the reservoirs start at steady state | |
| Deep-aquifer initial-state file | Storage per subbasin, written by the groundwater calibration | storage given directly |
With none of them, the reservoirs start empty.
From a gauge. HydroPol2D-Python estimates from a daily discharge record with hydropol2d estimate-deep-baseflow:
- Lyne–Hollick baseflow of the observed discharge;
- recession segments and a master recession curve (MRC);
- separation of the slow, linear deep tail of the MRC, which also gives ;
- the start-date baseflow placed on the MRC, and its deep part divided by the drainage area.
Pick a start date in a recession. The baseflow filter lags a rising hydrograph, so the tool warns when the start-date discharge is more than twice the baseflow.
13.4 Settings
| Item | HydroPol2D-Python | HydroPol2D (MATLAB) |
|---|---|---|
| Switch | flags.deep_aquifer | flag_deep_aquifer |
| Subbasins | static.deep_aquifer_subbasin_path | DeepAquiferSubbasins_path |
| Channel weights | static.deep_aquifer_channel_weight_path | DeepAquiferChannelWeight_path |
| Recharge fraction | vertical.deep_recharge_fraction | deep_recharge_fraction |
| Response time | vertical.deep_aquifer_response_time_days (default 180) | deep_aquifer_tau_days |
| Starting baseflow | vertical.deep_aquifer_initial_baseflow_mm_day | deep_aquifer_initial_baseflow_mm_day |
| Mean recharge | vertical.deep_aquifer_mean_recharge_mm_year | deep_aquifer_mean_recharge_mm_year |
| Initial-state file | static.deep_aquifer_initial_state_path | DeepAquiferInitialState_path |
In MATLAB the starting-state values are in the "Deep aquifer: starting storage" block of the Soil and groundwater tab (see Case Workbook Settings). The Case Builder writes the subbasin and channel-weight rasters from the conditioned D8 terrain.
14. Mass Balance
The groundwater solver performs an explicit mass-balance check during each update.
The recharge input volume is
The exfiltration loss volume is
The groundwater storage change is
The mass-balance residual is then
where:
- = groundwater mass-balance error
The local update of every surface-water step closes the same balance, with the net exchange as the recharge term. The lateral solver is called with , and its check also counts the water exchanged with fixed-head cells. The MATLAB code stores the lateral residual as a groundwater diagnostic error. HydroPol2D-Python does not store it and tracks the water balance of the whole model instead.
Recharge and capillary rise do not wait in the scheduler. The net exchange changes the heads in the same surface-water step, so no exchange is in transit between lateral updates. The pending recharge and capillary-rise totals only count what has passed since the last lateral update and are not extra storage. This distinction is important for interpreting whole-model water-balance diagnostics.
15. Summary
The HydroPol2D groundwater model combines:
- a layered vadose-zone recharge module based on a Darcy–van Genuchten–Mualem closure,
- capillary rise from shallow groundwater into the root zone,
- an asynchronous groundwater scheduler for the slower lateral groundwater flow, while the net exchange changes the heads in every surface-water step,
- a 2D Boussinesq groundwater flow model for lateral saturated flow,
- adaptive explicit time stepping,
- surface exfiltration when groundwater rises above the ground surface,
- a saturation-excess correction that preserves mass when the water table reduces vadose storage capacity,
- and an optional deep aquifer of linear reservoirs per subbasin, with a starting state set from a baseflow or a mean recharge.
This structure allows HydroPol2D to represent a dynamically coupled subsurface system in which:
- infiltration does not simply disappear into a static bucket,
- recharge affects groundwater head,
- groundwater flow redistributes water laterally,
- and groundwater can return to the land surface and influence overland flow generation.
The result is a groundwater model that is physically interpretable, mass conservative, and fully integrated with the hydrologic and hydrodynamic architecture of HydroPol2D.