Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0393575
allow for infiltration of overlandflow
JoostBuitink Aug 13, 2025
4cc452c
fix gwf model
JoostBuitink Sep 2, 2025
c2108d9
overlandflow correction improvement
JoostBuitink Sep 16, 2025
c9465d3
Merge branch 'master' into surfacewater_infiltration
JoostBuitink Sep 16, 2025
3e3d234
Merge branch 'master' into surfacewater_infiltration
JoostBuitink Feb 13, 2026
e141d87
Update config_structure.jl
JoostBuitink Feb 13, 2026
6058567
fix flags
JoostBuitink Feb 13, 2026
37b0c22
Merge branch 'master' into surfacewater_infiltration
JoostBuitink Feb 13, 2026
78ff48d
precommit fix
JoostBuitink Feb 13, 2026
9dcf097
remove todo statement
JoostBuitink Feb 13, 2026
ded6ea6
Merge branch 'master' into surfacewater_infiltration
JoostBuitink Mar 3, 2026
dde75b1
Merge branch 'master' into surfacewater_infiltration
JoostBuitink Mar 6, 2026
077f726
Merge branch 'master' into surfacewater_infiltration
JoostBuitink Mar 6, 2026
2aeb128
fix merge error
JoostBuitink Mar 6, 2026
dfd4afb
add unit test
JoostBuitink Mar 6, 2026
d7d566c
Merge branch 'master' into surfacewater_infiltration
JoostBuitink Mar 26, 2026
1b34b23
Merge branch 'master' into surfacewater_infiltration
JoostBuitink Mar 26, 2026
84cdf22
fix merge confict error
JoostBuitink Mar 26, 2026
49b9f63
another merge fix
JoostBuitink Mar 26, 2026
23567ef
Update surface_local_inertial.jl
JoostBuitink Mar 26, 2026
92cfb00
fix test
JoostBuitink Mar 27, 2026
abecbff
add docs
JoostBuitink Apr 2, 2026
b876737
fix typo
JoostBuitink Apr 2, 2026
8828dc0
update names; updated and added tests
JoostBuitink Apr 10, 2026
b0b9d7b
Merge branch 'master' into surfacewater_infiltration
JoostBuitink Apr 10, 2026
71964b5
process comments part1
JoostBuitink Jul 23, 2026
4a081fe
Update soil.jl
JoostBuitink Jul 23, 2026
c3243d1
move infilt to boundary
JoostBuitink Aug 26, 2026
02542da
move to boundary conditions
JoostBuitink Aug 26, 2026
cc9250e
max_reinfiltration_factor
JoostBuitink Aug 26, 2026
df1770f
update docs
JoostBuitink Aug 26, 2026
81c9088
clarify docs
JoostBuitink Aug 26, 2026
3ad8b1b
infiltration_volume to boundary conditions
JoostBuitink Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Wflow/src/config_structure.jl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ end
snow_gravitational_transport__flag::Bool = false
glacier__flag::Bool = false
soil_infiltration_reduction__flag::Bool = false
land_surface_water_reinfiltration__flag::Bool = false
soil_layer__thickness::Vector{Int} = [100, 300, 800]
saturated_hydraulic_conductivity_profile::VerticalConductivityProfile.T =
VerticalConductivityProfile.exponential
Expand Down
60 changes: 60 additions & 0 deletions Wflow/src/routing/surface/surface_kinwave.jl
Original file line number Diff line number Diff line change
Expand Up @@ -649,3 +649,63 @@ get_inflow_reservoir(
# Exclude subsurface flow from `GroundwaterFlowModel`.
get_inflow_reservoir(::AbstractRiverFlowModel, ::GroundwaterFlowModel, inds::Vector{Int}) =
zeros(length(inds))

"""
Update overland flow water level and discharge for KinWaveOverlandFlow model based on
surface water infiltration.
"""
function update_overland_flow_and_depth!(
overland_flow_model::KinWaveOverlandFlowModel,
soil_model::SbmSoilModel,
domain::Domain,
)
(; infilt_surfacewater) = soil_model.variables
n = length(infilt_surfacewater)
threaded_foreach(1:n; basesize = 1000) do i
update_overland_flow_and_depth!(
overland_flow_model,
infilt_surfacewater[i],
domain.land.parameters,
i,
)
end
end

"""
Update overland flow water level and discharge in-place for a single cell based on
surface water infiltration.
"""
function update_overland_flow_and_depth!(
overland_flow_model::KinWaveOverlandFlowModel,
infilt_surfacewater,
land_parameters,
i,
)
if infilt_surfacewater > 0.0
# Get original h_land in mm
original_h_land = overland_flow_model.variables.h[i] * 1000.0

# Correct values for river fraction to ensure correct water accounting
infiltrated_surfacewater =
(infilt_surfacewater / (1.0 - land_parameters.river_fraction[i]))
# Calculate new h_land in m
h = (original_h_land - infiltrated_surfacewater) / 1000.0

q = ifelse(
land_parameters.surface_flow_width[i] > 0.0 && h > 0.0,
# Compute cross-sectional area from h
pow(
(h * land_parameters.surface_flow_width[i]) /
overland_flow_model.parameters.alpha[i],
1.0 / overland_flow_model.parameters.beta,
),
0.0,
)
# set q to 0.0 if it is below the minimum flow threshold
q = ifelse(q < KIN_WAVE_MIN_FLOW, 0.0, q)

overland_flow_model.variables.flow.q[i] = q
overland_flow_model.variables.h[i] = h
end
return nothing
end
20 changes: 18 additions & 2 deletions Wflow/src/routing/surface/surface_local_inertial.jl
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,7 @@ end
@with_kw struct LocalInertialOverlandFlowBC
n::Int
runoff::Vector{Float64} = zeros(n) # runoff from hydrological model [m³ s⁻¹]
infiltration_volume::Vector{Float64} = zeros(n) # amount of infiltration from surface water [m³]
end

"Local inertial overland flow model using the local inertial method"
Expand Down Expand Up @@ -752,6 +753,10 @@ function update_bc_overland_flow_model!(
net_runoff / 1000.0 * area / dt + net_runoff_river * area * 0.001 / dt
overland_flow_model.boundary_conditions.runoff[river_indices] .+=
get_flux_to_river(subsurface_flow, river_indices)

# infiltration volume [m³] from surface water, applied in `local_inertial_update_water_depth!`
@. overland_flow_model.boundary_conditions.infiltration_volume =
soil.variables.infilt_surfacewater * area * 0.001
return nothing
end

Expand Down Expand Up @@ -1019,7 +1024,7 @@ function local_inertial_update_water_depth!(
sum_at(river_v.q, edges_at_node.dst[inds_river[i]]) + land_v.qx[xd] -
land_v.qx[i] + land_v.qy[yd] - land_v.qy[i] + land_bc.runoff[i] -
river_bc.abstraction[inds_river[i]]
) * dt
) * dt - land_bc.infiltration_volume[i]
if land_v.storage[i] < 0.0
land_v.error[i] = land_v.error[i] + abs(land_v.storage[i])
land_v.storage[i] = 0.0 # set storage to zero
Expand Down Expand Up @@ -1065,7 +1070,7 @@ function local_inertial_update_water_depth!(
(
land_v.qx[xd] - land_v.qx[i] + land_v.qy[yd] - land_v.qy[i] +
land_bc.runoff[i]
) * dt
) * dt - land_bc.infiltration_volume[i]
if land_v.storage[i] < 0.0
land_v.error[i] = land_v.error[i] + abs(land_v.storage[i])
land_v.storage[i] = 0.0 # set storage to zero
Expand Down Expand Up @@ -1353,3 +1358,14 @@ function FloodPlainModel(
floodplain = FloodPlainModel(; parameters, variables)
return floodplain
end

"""
No-op for the `LocalInertialOverlandFlowModel`. Surface water infiltration is applied via
the `infiltration_volume` boundary condition set in `update_bc_overland_flow_model!` and
subtracted from land storage in `local_inertial_update_water_depth!`.
"""
update_overland_flow_and_depth!(
::LocalInertialOverlandFlowModel,
::SbmSoilModel,
::Domain,
) = nothing
5 changes: 5 additions & 0 deletions Wflow/src/routing/surface/surface_routing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ function surface_routing!(model)
(; overland_flow, river_flow, subsurface_flow) = routing
(; reservoir) = river_flow.boundary_conditions

# correct overland flow water levels in case of reinfiltration
if config.model.land_surface_water_reinfiltration__flag
update_overland_flow_and_depth!(overland_flow, soil, domain)
end

dt = tosecond(clock.dt)
# update lateral inflow for kinematic wave overland flow
update_lateral_inflow!(
Expand Down
12 changes: 11 additions & 1 deletion Wflow/src/sbm.jl
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,19 @@ function update_land_hydrology_model!(
soil,
atmospheric_forcing,
(; interception, runoff, demand, allocation),
domain,
config,
)

update_soil_water_flow!(
soil,
domain,
atmospheric_forcing,
(; snow, runoff, demand),
config,
dt,
)

update_soil_water_flow!(soil, atmospheric_forcing, (; snow, runoff, demand), config, dt)
@. soil.variables.actevap += interception.variables.interception_rate
return nothing
end
Expand Down
9 changes: 6 additions & 3 deletions Wflow/src/sbm_gwf_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ end
function update_model!(model::AbstractModel{<:SbmGwfModel})
(; routing, land, domain, clock, config) = model
(; soil, runoff, demand) = land
(; boundary_conditions) = routing.subsurface_flow
(; subsurface_flow, overland_flow) = routing
(; boundary_conditions) = subsurface_flow

dt = tosecond(clock.dt)

Expand All @@ -85,7 +86,7 @@ function update_model!(model::AbstractModel{<:SbmGwfModel})
end
# update groundwater domain
update_subsurface_flow_model!(
routing.subsurface_flow,
subsurface_flow,
soil,
domain,
dt_gwf,
Expand All @@ -94,7 +95,9 @@ function update_model!(model::AbstractModel{<:SbmGwfModel})
# update SBM soil model (runoff, ustorelayerdepth and satwaterdepth)
update_soil_water_storage!(
soil,
(; runoff, demand, subsurface_flow = routing.subsurface_flow),
(; runoff, demand, subsurface_flow, overland_flow),
domain,
config,
)

surface_routing!(model)
Expand Down
9 changes: 8 additions & 1 deletion Wflow/src/sbm_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ function update_model!(model::AbstractModel{<:SbmModel})
(; routing, land, domain, clock, config) = model
(; soil, runoff, demand) = land
(; kv_profile) = land.soil.parameters
(; subsurface_flow, overland_flow) = routing
(; boundary_conditions) = routing.subsurface_flow
dt = tosecond(clock.dt)

Expand All @@ -85,8 +86,14 @@ function update_model!(model::AbstractModel{<:SbmModel})
domain,
clock.dt / BASETIMESTEP,
)

# update SBM soil model (runoff, ustorelayerdepth and satwaterdepth)
update_soil_water_storage!(soil, (; runoff, demand, routing.subsurface_flow))
update_soil_water_storage!(
soil,
(; runoff, demand, subsurface_flow, overland_flow),
domain,
config,
)

surface_routing!(model)

Expand Down
88 changes: 85 additions & 3 deletions Wflow/src/soil/soil.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ abstract type AbstractSoilModel end
infiltsoilpath::Vector{Float64} = fill(MISSING_VALUE, n)
# Infiltration excess water [mm Δt⁻¹]
infiltexcess::Vector{Float64} = fill(MISSING_VALUE, n)
# Infiltration from surface water [mm Δt⁻¹]
infilt_surfacewater::Vector{Float64} = fill(0.0, n)
# Water that cannot infiltrate due to saturated soil (saturation excess) [mm Δt⁻¹]
excesswater::Vector{Float64} = fill(MISSING_VALUE, n)
# Water exfiltrating during saturation excess conditions [mm Δt⁻¹]
Expand Down Expand Up @@ -198,6 +200,10 @@ end
potential_transpiration::Vector{Float64} = fill(MISSING_VALUE, n)
# Potential soil evaporation rate [mm Δt⁻¹]
potential_soilevaporation::Vector{Float64} = fill(MISSING_VALUE, n)
# Potential infiltration originating from surface water [mm Δt⁻¹]
potential_infiltration_surfacewater::Vector{Float64} = fill(0.0, n)
# Total water available for infiltration [mm Δt⁻¹]
potential_infiltration::Vector{Float64} = fill(0.0, n)
end

"Exponential depth profile of vertical hydraulic conductivity at the soil surface"
Expand Down Expand Up @@ -675,6 +681,8 @@ function update_bc_soil_model!(
soil_model::SbmSoilModel,
atmospheric_forcing::AtmosphericForcing,
external_models::NamedTuple,
domain::Domain,
config::Config,
)
(; interception, runoff, demand, allocation) = external_models
(; potential_transpiration, water_flux_surface, potential_soilevaporation) =
Expand All @@ -693,6 +701,13 @@ function update_bc_soil_model!(
runoff.variables.runoff_land .+ get_water_depth(demand.paddy),
0.0,
)
# update available for infiltration in case surface water infiltration is enabled
update_available_for_infiltration!(
soil_model,
domain,
runoff,
config.model.land_surface_water_reinfiltration__flag,
)
return nothing
end

Expand Down Expand Up @@ -741,6 +756,56 @@ function infiltration_reduction_factor!(
return nothing
end

function update_available_for_infiltration!(
Comment thread
JoostBuitink marked this conversation as resolved.
model::SbmSoilModel,
domain::Domain,
runoff::AbstractRunoffModel,
do_surface_water_infiltration::Bool,
)
v = model.variables
(; water_flux_surface, potential_infiltration_surfacewater, potential_infiltration) =
model.boundary_conditions
(; waterdepth_land) = runoff.boundary_conditions
(; river_fraction) = domain.land.parameters

# Fraction of overland flow that is made available for reinfiltration [-]
max_reinfiltration_fraction = 0.95

n = length(potential_infiltration)
threaded_foreach(1:n; basesize = 1000) do i
potential_infiltration_surfacewater[i] = 0.0
if do_surface_water_infiltration
potential_infiltration_surfacewater[i] =
waterdepth_land[i] * (1.0 - river_fraction[i]) * max_reinfiltration_fraction
water_flux_surface[i] += potential_infiltration_surfacewater[i]
end
potential_infiltration[i] = water_flux_surface[i]
end

return nothing
end

function update_infiltration_fluxes!(soil_model::SbmSoilModel)
(; infilt_surfacewater, actinfilt, infiltexcess, excesswater) = soil_model.variables
(; water_flux_surface, potential_infiltration, potential_infiltration_surfacewater) =
soil_model.boundary_conditions

n = length(actinfilt)
threaded_foreach(1:n; basesize = 1000) do i
infilt_surfacewater[i],
actinfilt[i],
infiltexcess[i],
excesswater[i],
water_flux_surface[i] = update_infiltration_fluxes(
potential_infiltration[i],
potential_infiltration_surfacewater[i],
water_flux_surface[i],
actinfilt[i],
infiltexcess[i],
)
end
end

"""
infiltration!(soil_model::SbmSoilMsoil

Expand Down Expand Up @@ -1118,6 +1183,7 @@ transpiration, capillary flux and leakage) for a single timestep.
"""
function update_soil_water_flow!(
soil_model::SbmSoilModel,
domain::Domain,
atmospheric_forcing::AtmosphericForcing,
external_models::NamedTuple,
config::Config,
Expand Down Expand Up @@ -1146,8 +1212,17 @@ function update_soil_water_flow!(
transpiration!(soil_model, dt)
# actual infiltration and excess water
actual_infiltration!(soil_model)
@. v.excesswater = water_flux_surface - v.actinfilt - v.infiltexcess

# Correct fluxes in case of reinfiltration, and only compute excess water if reinfiltration
# is not enabled
if config.model.land_surface_water_reinfiltration__flag
update_infiltration_fluxes!(soil_model)
else
@. v.excesswater = water_flux_surface - v.actinfilt - v.infiltexcess
end

actual_infiltration_soil_path!(soil_model)

@. v.excesswatersoil =
max(water_flux_surface * (1.0 - p.pathfrac) - v.actinfiltsoil, 0.0)
@. v.excesswaterpath = max(water_flux_surface * p.pathfrac - v.actinfiltpath, 0.0)
Expand Down Expand Up @@ -1238,8 +1313,14 @@ store `satwaterdepth` and the water exfiltrating during saturation excess condit
`exfiltsatwater` are updated. Additionally, volumetric water content per soil layer and for
the root zone are updated.
"""
function update_soil_water_storage!(soil_model::SbmSoilModel, external_models::NamedTuple)
(; runoff, demand, subsurface_flow) = external_models
function update_soil_water_storage!(
soil_model::SbmSoilModel,
external_models::NamedTuple,
domain::Domain,
config::Config,
)
(; runoff, demand, subsurface_flow, overland_flow) = external_models

(; runoff_land, ae_openw_l) = runoff.variables
p = soil_model.parameters
v = soil_model.variables
Expand Down Expand Up @@ -1319,6 +1400,7 @@ function update_soil_water_storage!(soil_model::SbmSoilModel, external_models::N
# and the h_max parameter of a paddy field)
update_runoff!(demand.paddy, v.runoff)
@. v.net_runoff = v.runoff - ae_openw_l

return nothing
end

Expand Down
Loading