From 858d248950758f3eae1be7d1013fdf45d4133d79 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 11 Mar 2026 09:15:05 +0100 Subject: [PATCH 01/90] Add `floodplain` to kinematic river flow Initialized as `nothing`. --- Wflow/src/Wflow.jl | 1 + Wflow/src/routing/surface_kinwave.jl | 10 +- Wflow/src/routing/surface_local_inertial.jl | 280 -------------------- 3 files changed, 9 insertions(+), 282 deletions(-) diff --git a/Wflow/src/Wflow.jl b/Wflow/src/Wflow.jl index 31ef7901a..277164457 100644 --- a/Wflow/src/Wflow.jl +++ b/Wflow/src/Wflow.jl @@ -202,6 +202,7 @@ include("groundwater/aquifer.jl") include("groundwater/boundary_conditions.jl") include("routing/subsurface.jl") include("routing/reservoir.jl") +include("routing/floodplain.jl") include("routing/surface_kinwave.jl") include("routing/surface_local_inertial.jl") include("routing/surface_routing.jl") diff --git a/Wflow/src/routing/surface_kinwave.jl b/Wflow/src/routing/surface_kinwave.jl index 7e32206df..03cd960b2 100644 --- a/Wflow/src/routing/surface_kinwave.jl +++ b/Wflow/src/routing/surface_kinwave.jl @@ -125,12 +125,16 @@ function RiverFlowBC( end "River flow model using the kinematic wave method and the Manning flow equation" -@with_kw struct KinWaveRiverFlow{R <: RiverFlowBC, A <: AbstractAllocationModel} <: - AbstractRiverFlowModel +@with_kw struct KinWaveRiverFlow{ + R <: RiverFlowBC, + F <: Union{AbstractFloodPlain, Nothing}, + A <: AbstractAllocationModel, +} <: AbstractRiverFlowModel timestepping::TimeStepping boundary_conditions::R parameters::RiverFlowParameters variables::FlowVariables + floodplain::F # Floodplain (1D) schematization allocation::A # Water allocation end @@ -151,12 +155,14 @@ function KinWaveRiverFlow( variables = FlowVariables(; n) parameters = RiverFlowParameters(dataset, config, domain) boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir) + floodplain = nothing river_flow = KinWaveRiverFlow(; timestepping, boundary_conditions, parameters, variables, + floodplain, allocation, ) diff --git a/Wflow/src/routing/surface_local_inertial.jl b/Wflow/src/routing/surface_local_inertial.jl index f92b8a557..b6774e365 100644 --- a/Wflow/src/routing/surface_local_inertial.jl +++ b/Wflow/src/routing/surface_local_inertial.jl @@ -1,5 +1,3 @@ -abstract type AbstractFloodPlain end - "Struct for storing local inertial river flow model parameters" @with_kw struct LocalInertialRiverFlowParameters n::Int # number of cells [-] @@ -1070,281 +1068,3 @@ function local_inertial_update_water_depth!( end return nothing end - -""" - FloodPlainProfile - -Floodplain `storage` is a function of `depth` (flood depth intervals). Based on the -cumulative floodplain `storage` a floodplain profile as a function of `flood_depth` is -derived with floodplain area `a` (cumulative) and wetted perimeter radius `p` (cumulative). -""" -@with_kw struct FloodPlainProfile{N} - depth::Vector{Float64} # Flood depth [m] - storage::Array{Float64, 2} # Flood storage (cumulative) [m³] - width::Array{Float64, 2} # Flood width [m] - a::Array{Float64, 2} # Flow area (cumulative) [m²] - p::Array{Float64, 2} # Wetted perimeter (cumulative) [m] -end - -"Initialize floodplain profile `FloodPlainProfile`" -function FloodPlainProfile( - dataset::NCDataset, - config::Config, - domain::DomainRiver, - index_pit::Vector{Int}, -) - (; indices) = domain.network - (; flow_width, flow_length) = domain.parameters - storage = ncread( - dataset, - config, - "floodplain_water__sum_of_volume_per_depth"; - optional = false, - sel = indices, - type = Float64, - dimname = :flood_depth, - ) - n = length(indices) - - # for convenience (interpolation) flood depth 0.0 m is added, with associated area (a), - # storage, width (river width) and wetted perimeter (p). - storage = vcat(fill(Float64(0), n)', storage) - start_storage = storage - flood_depths = Float64.(dataset["flood_depth"][:]) - pushfirst!(flood_depths, 0.0) - n_depths = length(flood_depths) - - p = zeros(n_depths, n) - a = zeros(n_depths, n) - segment_storage = zeros(n_depths, n) - width = zeros(n_depths, n) - width[1, :] = flow_width[1:n] - - # determine flow area (a), width and wetted perimeter (p) FloodPlain - h = diff(flood_depths) - incorrect_vol = 0 - riv_cells = 0 - error_vol = 0 - for i in 1:n - riv_cell = 0 - diff_storage = diff(storage[:, i]) - - for j in 1:(n_depths - 1) - # assume rectangular shape of flood depth segment - width[j + 1, i] = diff_storage[j] / (h[j] * flow_length[i]) - # check provided flood storage (floodplain width should be constant or increasing - # as a function of flood depth) - if width[j + 1, i] < width[j, i] - # raise warning only if difference is larger than rounding error of 0.01 m³ - if ((width[j, i] - width[j + 1, i]) * h[j] * flow_length[i]) > 0.01 - incorrect_vol += 1 - riv_cell = 1 - error_vol = - error_vol + - ((width[j, i] - width[j + 1, i]) * h[j] * flow_length[i]) - end - width[j + 1, i] = width[j, i] - end - a[j + 1, i] = width[j + 1, i] * h[j] - p[j + 1, i] = (width[j + 1, i] - width[j, i]) + 2.0 * h[j] - segment_storage[j + 1, i] = a[j + 1, i] * flow_length[i] - if j == 1 - # for interpolation wetted perimeter at flood depth 0.0 is required - p[j, i] = p[j + 1, i] - 2.0 * h[j] - end - end - - p[2:end, i] = cumsum(p[2:end, i]) - a[:, i] = cumsum(a[:, i]) - storage[:, i] = cumsum(segment_storage[:, i]) - - riv_cells += riv_cell - end - - if incorrect_vol > 0 - perc_riv_cells = round(100.0 * (riv_cells / n); digits = 2) - perc_error_vol = round(100.0 * (error_vol / sum(start_storage[end, :])); digits = 2) - @warn string( - "The provided storage of $incorrect_vol rectangular floodplain schematization", - " segments for $riv_cells river cells ($perc_riv_cells % of total river cells)", - " is not correct and has been increased with $perc_error_vol % of provided storage.", - ) - end - - # set floodplain parameters for ghost points - storage = hcat(storage, storage[:, index_pit]) - width = hcat(width, width[:, index_pit]) - a = hcat(a, a[:, index_pit]) - p = hcat(p, p[:, index_pit]) - - # initialize floodplain profile parameters - profile = FloodPlainProfile{n_depths}(; storage, width, depth = flood_depths, a, p) - return profile -end - -"Struct to store floodplain flow model parameters" -@with_kw struct FloodPlainParameters{P} - profile::P # floodplain profile - mannings_n::Vector{Float64} # manning's roughness [s m-1/3] - mannings_n_sq::Vector{Float64} # manning's roughness squared at edge [(s m-1/3)2] - zb_max::Vector{Float64} # maximum bankfull elevation at edge [m] -end - -"Initialize floodplain flow model parameters" -function FloodPlainParameters( - dataset::NCDataset, - config::Config, - domain::DomainRiver, - zb_floodplain::Vector{Float64}, - index_pit::Vector{Int}, -) - (; indices, nodes_at_edge, graph) = domain.network - (; flow_length) = domain.parameters - n_edges = ne(graph) - profile = FloodPlainProfile(dataset, config, domain, index_pit) - - mannings_n = ncread( - dataset, - config, - "floodplain_water_flow__manning_n_parameter"; - sel = indices, - defaults = 0.072, - type = Float64, - ) - # manning roughness at edges - append!(mannings_n, mannings_n[index_pit]) # copy to ghost nodes - mannings_n_sq = fill(Float64(0), n_edges) - zb_max = fill(Float64(0), n_edges) - for i in 1:n_edges - src_node = nodes_at_edge.src[i] - dst_node = nodes_at_edge.dst[i] - mannings_n_i = - ( - mannings_n[dst_node] * flow_length[dst_node] + - mannings_n[src_node] * flow_length[src_node] - ) / (flow_length[dst_node] + flow_length[src_node]) - mannings_n_sq[i] = mannings_n_i * mannings_n_i - zb_max[i] = max(zb_floodplain[src_node], zb_floodplain[dst_node]) - end - parameters = FloodPlainParameters(profile, mannings_n, mannings_n_sq, zb_max) - return parameters -end - -"Struct to store floodplain flow model variables" -@with_kw struct FloodPlainVariables - n::Int - n_edges::Int - storage::Vector{Float64} = zeros(n) # storage [m³] - h::Vector{Float64} # water depth [m] - error::Vector{Float64} = zeros(n) # error storage [m³] - a::Vector{Float64} = zeros(n_edges) # flow area at edge [m²] - r::Vector{Float64} = zeros(n_edges) # hydraulic radius at edge [m] - hf::Vector{Float64} = zeros(n_edges) # water depth at edge [m] - q0::Vector{Float64} = zeros(n_edges) # discharge at edge at previous time step - q::Vector{Float64} = zeros(n_edges) # discharge at edge [m³ s⁻¹] - q_av::Vector{Float64} = zeros(n_edges) # average river discharge at edge [m³ s⁻¹] for model timestep Δt - hf_index::Vector{Int} = zeros(Int, n_edges) # edge index with `hf` [-] above depth threshold -end - -"Initialize floodplain flow model variables" -function FloodPlainVariables(n::Int, n_edges::Int, index_pit::Vector{Int}) - variables = FloodPlainVariables(; n, n_edges, h = zeros(n + length(index_pit))) - return variables -end - -"Floodplain flow model" -@with_kw struct FloodPlain{P} <: AbstractFloodPlain - parameters::FloodPlainParameters{P} - variables::FloodPlainVariables -end - -"Determine the initial floodplain storage" -function initialize_storage!(river, domain::Domain, nriv::Int) - (; flow_width, flow_length) = domain.river.parameters - (; floodplain) = river - (; profile) = floodplain.parameters - for i in 1:nriv - i1, i2 = interpolation_indices(floodplain.variables.h[i], profile.depth) - a = flow_area( - profile.width[i2, i], - profile.a[i1, i], - profile.depth[i1], - floodplain.variables.h[i], - ) - a = max(a - (flow_width[i] * floodplain.variables.h[i]), 0.0) - floodplain.variables.storage[i] = flow_length[i] * a - end - return nothing -end - -"helper function to get interpolation indices" -function interpolation_indices(x, v::AbstractVector) - i1 = 1 - for i in eachindex(v) - if v[i] <= x - i1 = i - end - end - if i1 == length(v) - i2 = i1 - else - i2 = i1 + 1 - end - return i1, i2 -end - -""" - flow_area(width, area, depth, h) - -Compute floodplain flow area based on flow depth `h` and floodplain `depth`, `area` and -`width` of a floodplain profile. -""" -function flow_area(width, area, depth, h) - dh = h - depth # depth at i1 - area = area + (width * dh) # area at i1, width at i2 - return area -end - -""" - function wetted_perimeter(p, depth, h) - -Compute floodplain wetted perimeter based on flow depth `h` and floodplain `depth` and -wetted perimeter `p` of a floodplain profile. -""" -function wetted_perimeter(p, depth, h) - dh = h - depth # depth at i1 - p += 2.0 * dh # p at i1 - return p -end - -"Compute flood depth by interpolating flood storage `flood_storage` using flood depth intervals." -function flood_depth( - profile::FloodPlainProfile, - flood_storage::Float64, - flow_length::Float64, - i::Int, -) - i1, i2 = interpolation_indices(flood_storage, @view profile.storage[:, i]) - ΔA = (flood_storage - profile.storage[i1, i]) / flow_length - dh = ΔA / profile.width[i2, i] - flood_depth = profile.depth[i1] + dh - return flood_depth -end - -"Initialize floodplain geometry and `FloodPlain` variables and parameters" -function FloodPlain( - dataset::NCDataset, - config::Config, - domain::DomainRiver, - zb_floodplain::Vector{Float64}, -) - (; indices, local_drain_direction, graph) = domain.network - n = length(indices) - index_pit = findall(x -> x == 5, local_drain_direction) - parameters = FloodPlainParameters(dataset, config, domain, zb_floodplain, index_pit) - n_edges = ne(graph) - variables = FloodPlainVariables(n, n_edges, index_pit) - - floodplain = FloodPlain(; parameters, variables) - return floodplain -end From bf8276ef13debc79047356dd4055702e8af026dd Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 11 Mar 2026 09:19:44 +0100 Subject: [PATCH 02/90] Move `floodplain` to separate file --- Wflow/src/routing/floodplain.jl | 279 ++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 Wflow/src/routing/floodplain.jl diff --git a/Wflow/src/routing/floodplain.jl b/Wflow/src/routing/floodplain.jl new file mode 100644 index 000000000..d431d4273 --- /dev/null +++ b/Wflow/src/routing/floodplain.jl @@ -0,0 +1,279 @@ +abstract type AbstractFloodPlain end + +""" + FloodPlainProfile + +Floodplain `storage` is a function of `depth` (flood depth intervals). Based on the +cumulative floodplain `storage` a floodplain profile as a function of `flood_depth` is +derived with floodplain area `a` (cumulative) and wetted perimeter radius `p` (cumulative). +""" +@with_kw struct FloodPlainProfile{N} + depth::Vector{Float64} # Flood depth [m] + storage::Array{Float64, 2} # Flood storage (cumulative) [m³] + width::Array{Float64, 2} # Flood width [m] + a::Array{Float64, 2} # Flow area (cumulative) [m²] + p::Array{Float64, 2} # Wetted perimeter (cumulative) [m] +end + +"Initialize floodplain profile `FloodPlainProfile`" +function FloodPlainProfile( + dataset::NCDataset, + config::Config, + domain::DomainRiver, + index_pit::Vector{Int}, +) + (; indices) = domain.network + (; flow_width, flow_length) = domain.parameters + storage = ncread( + dataset, + config, + "floodplain_water__sum_of_volume_per_depth"; + optional = false, + sel = indices, + type = Float64, + dimname = :flood_depth, + ) + n = length(indices) + + # for convenience (interpolation) flood depth 0.0 m is added, with associated area (a), + # storage, width (river width) and wetted perimeter (p). + storage = vcat(fill(Float64(0), n)', storage) + start_storage = storage + flood_depths = Float64.(dataset["flood_depth"][:]) + pushfirst!(flood_depths, 0.0) + n_depths = length(flood_depths) + + p = zeros(n_depths, n) + a = zeros(n_depths, n) + segment_storage = zeros(n_depths, n) + width = zeros(n_depths, n) + width[1, :] = flow_width[1:n] + + # determine flow area (a), width and wetted perimeter (p) FloodPlain + h = diff(flood_depths) + incorrect_vol = 0 + riv_cells = 0 + error_vol = 0 + for i in 1:n + riv_cell = 0 + diff_storage = diff(storage[:, i]) + + for j in 1:(n_depths - 1) + # assume rectangular shape of flood depth segment + width[j + 1, i] = diff_storage[j] / (h[j] * flow_length[i]) + # check provided flood storage (floodplain width should be constant or increasing + # as a function of flood depth) + if width[j + 1, i] < width[j, i] + # raise warning only if difference is larger than rounding error of 0.01 m³ + if ((width[j, i] - width[j + 1, i]) * h[j] * flow_length[i]) > 0.01 + incorrect_vol += 1 + riv_cell = 1 + error_vol = + error_vol + + ((width[j, i] - width[j + 1, i]) * h[j] * flow_length[i]) + end + width[j + 1, i] = width[j, i] + end + a[j + 1, i] = width[j + 1, i] * h[j] + p[j + 1, i] = (width[j + 1, i] - width[j, i]) + 2.0 * h[j] + segment_storage[j + 1, i] = a[j + 1, i] * flow_length[i] + if j == 1 + # for interpolation wetted perimeter at flood depth 0.0 is required + p[j, i] = p[j + 1, i] - 2.0 * h[j] + end + end + + p[2:end, i] = cumsum(p[2:end, i]) + a[:, i] = cumsum(a[:, i]) + storage[:, i] = cumsum(segment_storage[:, i]) + + riv_cells += riv_cell + end + + if incorrect_vol > 0 + perc_riv_cells = round(100.0 * (riv_cells / n); digits = 2) + perc_error_vol = round(100.0 * (error_vol / sum(start_storage[end, :])); digits = 2) + @warn string( + "The provided storage of $incorrect_vol rectangular floodplain schematization", + " segments for $riv_cells river cells ($perc_riv_cells % of total river cells)", + " is not correct and has been increased with $perc_error_vol % of provided storage.", + ) + end + + # set floodplain parameters for ghost points + storage = hcat(storage, storage[:, index_pit]) + width = hcat(width, width[:, index_pit]) + a = hcat(a, a[:, index_pit]) + p = hcat(p, p[:, index_pit]) + + # initialize floodplain profile parameters + profile = FloodPlainProfile{n_depths}(; storage, width, depth = flood_depths, a, p) + return profile +end + +"Struct to store floodplain flow model parameters" +@with_kw struct FloodPlainParameters{P} + profile::P # floodplain profile + mannings_n::Vector{Float64} # manning's roughness [s m-1/3] + mannings_n_sq::Vector{Float64} # manning's roughness squared at edge [(s m-1/3)2] + zb_max::Vector{Float64} # maximum bankfull elevation at edge [m] +end + +"Initialize floodplain flow model parameters" +function FloodPlainParameters( + dataset::NCDataset, + config::Config, + domain::DomainRiver, + zb_floodplain::Vector{Float64}, + index_pit::Vector{Int}, +) + (; indices, nodes_at_edge, graph) = domain.network + (; flow_length) = domain.parameters + n_edges = ne(graph) + profile = FloodPlainProfile(dataset, config, domain, index_pit) + + mannings_n = ncread( + dataset, + config, + "floodplain_water_flow__manning_n_parameter"; + sel = indices, + defaults = 0.072, + type = Float64, + ) + # manning roughness at edges + append!(mannings_n, mannings_n[index_pit]) # copy to ghost nodes + mannings_n_sq = fill(Float64(0), n_edges) + zb_max = fill(Float64(0), n_edges) + for i in 1:n_edges + src_node = nodes_at_edge.src[i] + dst_node = nodes_at_edge.dst[i] + mannings_n_i = + ( + mannings_n[dst_node] * flow_length[dst_node] + + mannings_n[src_node] * flow_length[src_node] + ) / (flow_length[dst_node] + flow_length[src_node]) + mannings_n_sq[i] = mannings_n_i * mannings_n_i + zb_max[i] = max(zb_floodplain[src_node], zb_floodplain[dst_node]) + end + parameters = FloodPlainParameters(profile, mannings_n, mannings_n_sq, zb_max) + return parameters +end + +"Struct to store floodplain flow model variables" +@with_kw struct FloodPlainVariables + n::Int + n_edges::Int + storage::Vector{Float64} = zeros(n) # storage [m³] + h::Vector{Float64} # water depth [m] + error::Vector{Float64} = zeros(n) # error storage [m³] + a::Vector{Float64} = zeros(n_edges) # flow area at edge [m²] + r::Vector{Float64} = zeros(n_edges) # hydraulic radius at edge [m] + hf::Vector{Float64} = zeros(n_edges) # water depth at edge [m] + q0::Vector{Float64} = zeros(n_edges) # discharge at edge at previous time step + q::Vector{Float64} = zeros(n_edges) # discharge at edge [m³ s⁻¹] + q_av::Vector{Float64} = zeros(n_edges) # average river discharge at edge [m³ s⁻¹] for model timestep Δt + hf_index::Vector{Int} = zeros(Int, n_edges) # edge index with `hf` [-] above depth threshold +end + +"Initialize floodplain flow model variables" +function FloodPlainVariables(n::Int, n_edges::Int, index_pit::Vector{Int}) + variables = FloodPlainVariables(; n, n_edges, h = zeros(n + length(index_pit))) + return variables +end + +"Floodplain flow model" +@with_kw struct FloodPlain{P} <: AbstractFloodPlain + parameters::FloodPlainParameters{P} + variables::FloodPlainVariables +end + +"Determine the initial floodplain storage" +function initialize_storage!(river, domain::Domain, nriv::Int) + (; flow_width, flow_length) = domain.river.parameters + (; floodplain) = river + (; profile) = floodplain.parameters + for i in 1:nriv + i1, i2 = interpolation_indices(floodplain.variables.h[i], profile.depth) + a = flow_area( + profile.width[i2, i], + profile.a[i1, i], + profile.depth[i1], + floodplain.variables.h[i], + ) + a = max(a - (flow_width[i] * floodplain.variables.h[i]), 0.0) + floodplain.variables.storage[i] = flow_length[i] * a + end + return nothing +end + +"helper function to get interpolation indices" +function interpolation_indices(x, v::AbstractVector) + i1 = 1 + for i in eachindex(v) + if v[i] <= x + i1 = i + end + end + if i1 == length(v) + i2 = i1 + else + i2 = i1 + 1 + end + return i1, i2 +end + +""" + flow_area(width, area, depth, h) + +Compute floodplain flow area based on flow depth `h` and floodplain `depth`, `area` and +`width` of a floodplain profile. +""" +function flow_area(width, area, depth, h) + dh = h - depth # depth at i1 + area = area + (width * dh) # area at i1, width at i2 + return area +end + +""" + function wetted_perimeter(p, depth, h) + +Compute floodplain wetted perimeter based on flow depth `h` and floodplain `depth` and +wetted perimeter `p` of a floodplain profile. +""" +function wetted_perimeter(p, depth, h) + dh = h - depth # depth at i1 + p += 2.0 * dh # p at i1 + return p +end + +"Compute flood depth by interpolating flood storage `flood_storage` using flood depth intervals." +function flood_depth( + profile::FloodPlainProfile, + flood_storage::Float64, + flow_length::Float64, + i::Int, +) + i1, i2 = interpolation_indices(flood_storage, @view profile.storage[:, i]) + ΔA = (flood_storage - profile.storage[i1, i]) / flow_length + dh = ΔA / profile.width[i2, i] + flood_depth = profile.depth[i1] + dh + return flood_depth +end + +"Initialize floodplain geometry and `FloodPlain` variables and parameters" +function FloodPlain( + dataset::NCDataset, + config::Config, + domain::DomainRiver, + zb_floodplain::Vector{Float64}, +) + (; indices, local_drain_direction, graph) = domain.network + n = length(indices) + index_pit = findall(x -> x == 5, local_drain_direction) + parameters = FloodPlainParameters(dataset, config, domain, zb_floodplain, index_pit) + n_edges = ne(graph) + variables = FloodPlainVariables(n, n_edges, index_pit) + + floodplain = FloodPlain(; parameters, variables) + return floodplain +end From 5032ea5ce798b1a479a9443652548c7609b4f2c8 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 11 Mar 2026 13:50:44 +0100 Subject: [PATCH 03/90] Add `bankfull_storage` to kinematic wave river --- Wflow/src/routing/surface_kinwave.jl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Wflow/src/routing/surface_kinwave.jl b/Wflow/src/routing/surface_kinwave.jl index 03cd960b2..34f43886b 100644 --- a/Wflow/src/routing/surface_kinwave.jl +++ b/Wflow/src/routing/surface_kinwave.jl @@ -53,13 +53,16 @@ end "Struct for storing river flow model parameters" @with_kw struct RiverFlowParameters flow::ManningFlowParameters - bankfull_depth::Vector{Float64} # Bankfull water level [m] + bankfull_depth::Vector{Float64} # Bankfull water level [m] + bankfull_storage::Vector{Float64} # Bankfull storage [m³] end "Overload `getproperty` for river flow model parameters" function Base.getproperty(v::RiverFlowParameters, s::Symbol) if s === :bankfull_depth getfield(v, s) + elseif s === :bankfull_storage + getfield(v, s) elseif s === :flow getfield(v, :flow) else @@ -70,7 +73,7 @@ end "Initialize river flow model parameters" function RiverFlowParameters(dataset::NCDataset, config::Config, domain::DomainRiver) (; indices) = domain.network - (; slope) = domain.parameters + (; slope, flow_length, flow_width) = domain.parameters mannings_n = ncread( dataset, config, @@ -89,7 +92,8 @@ function RiverFlowParameters(dataset::NCDataset, config::Config, domain::DomainR ) flow_params = ManningFlowParameters(mannings_n, slope) - parameters = RiverFlowParameters(; flow = flow_params, bankfull_depth) + bankfull_storage = bankfull_depth .* flow_length .* flow_width + parameters = RiverFlowParameters(; flow = flow_params, bankfull_depth, bankfull_storage) return parameters end From 935d4d91cea416051e8c5c17ff529f4aded0faeb Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 12 Mar 2026 16:13:48 +0100 Subject: [PATCH 04/90] Separate init kinwave and local inertial floodplain --- Wflow/src/routing/floodplain.jl | 97 ++++++++++++++------- Wflow/src/routing/surface_local_inertial.jl | 2 +- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/Wflow/src/routing/floodplain.jl b/Wflow/src/routing/floodplain.jl index d431d4273..3f97704c7 100644 --- a/Wflow/src/routing/floodplain.jl +++ b/Wflow/src/routing/floodplain.jl @@ -19,8 +19,8 @@ end function FloodPlainProfile( dataset::NCDataset, config::Config, - domain::DomainRiver, - index_pit::Vector{Int}, + domain::DomainRiver; + index_pit::Vector{Int} = [], ) (; indices) = domain.network (; flow_width, flow_length) = domain.parameters @@ -111,27 +111,21 @@ function FloodPlainProfile( return profile end -"Struct to store floodplain flow model parameters" -@with_kw struct FloodPlainParameters{P} +"Struct to store local inertial floodplain flow model parameters" +@with_kw struct LocalInertialFloodPlainParameters{P} profile::P # floodplain profile mannings_n::Vector{Float64} # manning's roughness [s m-1/3] mannings_n_sq::Vector{Float64} # manning's roughness squared at edge [(s m-1/3)2] zb_max::Vector{Float64} # maximum bankfull elevation at edge [m] end -"Initialize floodplain flow model parameters" -function FloodPlainParameters( - dataset::NCDataset, - config::Config, - domain::DomainRiver, - zb_floodplain::Vector{Float64}, - index_pit::Vector{Int}, -) - (; indices, nodes_at_edge, graph) = domain.network - (; flow_length) = domain.parameters - n_edges = ne(graph) - profile = FloodPlainProfile(dataset, config, domain, index_pit) +"Struct to store kinematic wave floodplain flow model parameters" +@with_kw struct KinWaveFloodPlainParameters{P} + profile::P # floodplain profile + mannings_n::Vector{Float64} # manning's roughness [s m-1/3] +end +function get_floodplain_mannings_n(dataset, config, indices) mannings_n = ncread( dataset, config, @@ -140,6 +134,22 @@ function FloodPlainParameters( defaults = 0.072, type = Float64, ) + return mannings_n +end + +"Initialize local inertial floodplain flow model parameters" +function LocalInertialFloodPlainParameters( + dataset::NCDataset, + config::Config, + domain::DomainRiver, + zb_floodplain::Vector{Float64}, + index_pit::Vector{Int}, +) + (; indices, nodes_at_edge, graph) = domain.network + (; flow_length) = domain.parameters + n_edges = ne(graph) + profile = FloodPlainProfile(dataset, config, domain; index_pit) + mannings_n = get_floodplain_mannings_n(dataset, config, indices) # manning roughness at edges append!(mannings_n, mannings_n[index_pit]) # copy to ghost nodes mannings_n_sq = fill(Float64(0), n_edges) @@ -155,12 +165,13 @@ function FloodPlainParameters( mannings_n_sq[i] = mannings_n_i * mannings_n_i zb_max[i] = max(zb_floodplain[src_node], zb_floodplain[dst_node]) end - parameters = FloodPlainParameters(profile, mannings_n, mannings_n_sq, zb_max) + parameters = + LocalInertialFloodPlainParameters(profile, mannings_n, mannings_n_sq, zb_max) return parameters end -"Struct to store floodplain flow model variables" -@with_kw struct FloodPlainVariables +"Struct to store local inertial floodplain flow model variables" +@with_kw struct LocalInertialFloodPlainVariables n::Int n_edges::Int storage::Vector{Float64} = zeros(n) # storage [m³] @@ -175,16 +186,25 @@ end hf_index::Vector{Int} = zeros(Int, n_edges) # edge index with `hf` [-] above depth threshold end -"Initialize floodplain flow model variables" -function FloodPlainVariables(n::Int, n_edges::Int, index_pit::Vector{Int}) - variables = FloodPlainVariables(; n, n_edges, h = zeros(n + length(index_pit))) - return variables +"Struct to store kinematic wave floodplain flow model variables" +@with_kw struct KinWaveFloodPlainVariables + n::Int + storage::Vector{Float64} = zeros(n) # storage [m³] + h::Vector{Float64} = zeros(n) # water depth [m] + q::Vector{Float64} = zeros(n) # discharge [m³ s⁻¹] + q_av::Vector{Float64} = zeros(n) # average river discharge [m³ s⁻¹] for model timestep Δt +end + +"Local inertial floodplain flow model" +@with_kw struct LocalInertialFloodPlain{P} <: AbstractFloodPlain + parameters::LocalInertialFloodPlainParameters{P} + variables::LocalInertialFloodPlainVariables end -"Floodplain flow model" -@with_kw struct FloodPlain{P} <: AbstractFloodPlain - parameters::FloodPlainParameters{P} - variables::FloodPlainVariables +"Kinematic wave floodplain flow model" +@with_kw struct KinWaveFloodPlain{P} <: AbstractFloodPlain + parameters::KinWaveFloodPlainParameters{P} + variables::KinWaveFloodPlainVariables end "Determine the initial floodplain storage" @@ -260,8 +280,8 @@ function flood_depth( return flood_depth end -"Initialize floodplain geometry and `FloodPlain` variables and parameters" -function FloodPlain( +"Initialize floodplain geometry and `LocalInerialFloodPlain` variables and parameters" +function LocalInertialFloodPlain( dataset::NCDataset, config::Config, domain::DomainRiver, @@ -270,10 +290,23 @@ function FloodPlain( (; indices, local_drain_direction, graph) = domain.network n = length(indices) index_pit = findall(x -> x == 5, local_drain_direction) - parameters = FloodPlainParameters(dataset, config, domain, zb_floodplain, index_pit) n_edges = ne(graph) - variables = FloodPlainVariables(n, n_edges, index_pit) + parameters = + LocalInertialFloodPlainParameters(dataset, config, domain, zb_floodplain, index_pit) + variables = + LocalInertialFloodPlainVariables(; n, n_edges, h = zeros(n + length(index_pit))) - floodplain = FloodPlain(; parameters, variables) + floodplain = LocalInertialFloodPlain(; parameters, variables) + return floodplain +end + +function KinWaveFloodPlain(dataset::NCDataset, config::Config, domain::DomainRiver) + (; indices) = domain.network + profile = FloodPlainProfile(dataset, config, domain) + mannings_n = get_floodplain_mannings_n(dataset, config, indices) + profile = FloodPlainProfile(dataset, config, domain) + parameters = KinWaveFloodPlainParameters(profile, mannings_n) + variables = KinWaveFloodPlainVariables() + floodplain = KinWaveFloodPlain(; parameters, variables) return floodplain end diff --git a/Wflow/src/routing/surface_local_inertial.jl b/Wflow/src/routing/surface_local_inertial.jl index b6774e365..b2e4bd53c 100644 --- a/Wflow/src/routing/surface_local_inertial.jl +++ b/Wflow/src/routing/surface_local_inertial.jl @@ -214,7 +214,7 @@ function LocalInertialRiverFlow( if config.model.floodplain_1d__flag zb_floodplain = parameters.zb .+ parameters.bankfull_depth - floodplain = FloodPlain(dataset, config, domain, zb_floodplain) + floodplain = LocalInertialFloodPlain(dataset, config, domain, zb_floodplain) else floodplain = nothing end From 4d2f16c0b4827f4f9d7f322a0af189a1342280ab Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 17 Mar 2026 22:03:47 +0100 Subject: [PATCH 05/90] Refactor surface flow routing Introduce one struct for riverflow routing `RiverFlowModel` and one struct for overlandflow routing `OverlandFlowModel`. The routing method (subtype of `AbstractRoutingMethod`) is used to differentiate between different routing approaches for river and overland flow. Extending to other routing methods is now easier. --- Wflow/src/Wflow.jl | 1 + Wflow/src/bmi.jl | 7 +- Wflow/src/mass_balance.jl | 22 ++- Wflow/src/routing/routing.jl | 18 ++- Wflow/src/routing/surface_flow.jl | 56 +++++++ Wflow/src/routing/surface_kinwave.jl | 94 ++++-------- Wflow/src/routing/surface_local_inertial.jl | 158 ++++++++++---------- Wflow/src/routing/surface_routing.jl | 7 +- Wflow/src/sediment_flux.jl | 4 +- Wflow/test/routing_process.jl | 7 +- Wflow/test/testing_utils.jl | 3 +- 11 files changed, 211 insertions(+), 166 deletions(-) create mode 100644 Wflow/src/routing/surface_flow.jl diff --git a/Wflow/src/Wflow.jl b/Wflow/src/Wflow.jl index 277164457..54f5299b9 100644 --- a/Wflow/src/Wflow.jl +++ b/Wflow/src/Wflow.jl @@ -203,6 +203,7 @@ include("groundwater/boundary_conditions.jl") include("routing/subsurface.jl") include("routing/reservoir.jl") include("routing/floodplain.jl") +include("routing/surface_flow.jl") include("routing/surface_kinwave.jl") include("routing/surface_local_inertial.jl") include("routing/surface_routing.jl") diff --git a/Wflow/src/bmi.jl b/Wflow/src/bmi.jl index 38f404c4d..3af79a3cd 100644 --- a/Wflow/src/bmi.jl +++ b/Wflow/src/bmi.jl @@ -401,7 +401,12 @@ Return the grid element type of a model variable (PropertyLens `var`) based on a function grid_element_type( ::T, var::PropertyLens, -) where {T <: Union{LocalInertialRiverFlow, LocalInertialOverlandFlow}} +) where { + T <: Union{ + AbstractRiverFlowModel{<:LocalInertial}, + AbstractOverlandFlowModel{<:LocalInertial}, + }, +} vars = (PropertyLens(x) for x in (:q, :q_av, :qx, :qy)) element_type = if var in vars "edge" diff --git a/Wflow/src/mass_balance.jl b/Wflow/src/mass_balance.jl index 658327268..acf0a0347 100644 --- a/Wflow/src/mass_balance.jl +++ b/Wflow/src/mass_balance.jl @@ -138,7 +138,7 @@ end Return storage of a river flow model at index `i`. For `LocalInertialRiverFlow` floodplain storage is added to river storage if an optional floodplain is included. """ -function get_storage(river_flow_model::LocalInertialRiverFlow, i) +function get_storage(river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, i) (; storage) = river_flow_model.variables if isnothing(river_flow_model.floodplain) return storage[i] @@ -147,7 +147,8 @@ function get_storage(river_flow_model::LocalInertialRiverFlow, i) return total_storage end end -get_storage(river_flow_model::KinWaveRiverFlow, i) = river_flow_model.variables.storage[i] +get_storage(river_flow_model::AbstractRiverFlowModel{<:KinematicWave}, i) = + river_flow_model.variables.storage[i] """ Save river (+ floodplain) storage at previous time step as `storage_prev` of river @@ -332,7 +333,7 @@ compute_flow_balance!(reservoir::Nothing, water_balance::NoMassBalance, dt::Floa "Compute water mass balance error and relative error for river kinematic wave routing." function compute_flow_balance!( - river_flow::KinWaveRiverFlow, + river_flow::AbstractRiverFlowModel{<:KinematicWave}, water_balance::MassBalance, network::NetworkRiver, dt::Float64, @@ -357,7 +358,7 @@ Compute water mass balance error and relative error for river (and floodplain) l inertial routing. """ function compute_flow_balance!( - river_flow::LocalInertialRiverFlow, + river_flow::AbstractRiverFlowModel{<:LocalInertial}, water_balance::MassBalance, network::NetworkRiver, dt::Float64, @@ -416,7 +417,7 @@ Compute water mass balance error and relative error for overland flow kinematic routing. """ function compute_flow_balance!( - overland_flow::KinWaveOverlandFlow, + overland_flow::AbstractOverlandFlowModel{<:KinematicWave}, water_balance::MassBalance, dt::Float64, ) @@ -440,8 +441,8 @@ Compute water mass balance error and relative error for 1D river local inertial computed for each land cell (total storage) considering both river and overland flow. """ function compute_flow_balance!( - river_flow::LocalInertialRiverFlow, - overland_flow::LocalInertialOverlandFlow, + river_flow::AbstractRiverFlowModel{<:LocalInertial}, + overland_flow::AbstractOverlandFlowModel{<:LocalInertial}, water_balance::MassBalance, domain::Domain, dt::Float64, @@ -576,7 +577,12 @@ routing. """ function compute_flow_routing_balance!( model::Model{R}, -) where {R <: Routing{<:LocalInertialOverlandFlow, <:LocalInertialRiverFlow}} +) where { + R <: Routing{ + <:AbstractOverlandFlowModel{<:LocalInertial}, + <:AbstractRiverFlowModel{<:LocalInertial}, + }, +} (; river_flow, overland_flow, subsurface_flow) = model.routing (; reservoir) = river_flow.boundary_conditions (; overland_water_balance, reservoir_water_balance, subsurface_water_balance) = diff --git a/Wflow/src/routing/routing.jl b/Wflow/src/routing/routing.jl index 0259fd524..69a0775f1 100644 --- a/Wflow/src/routing/routing.jl +++ b/Wflow/src/routing/routing.jl @@ -1,18 +1,24 @@ +abstract type AbstractRiverFlowModel{T} end +abstract type AbstractOverlandFlowModel{T} end abstract type AbstractSubsurfaceFlowModel end -abstract type AbstractOverlandFlowModel end -abstract type AbstractRiverFlowModel end + +abstract type AbstractRoutingMethod end +struct AccucapacityFlux <: AbstractRoutingMethod end +struct KinematicWave <: AbstractRoutingMethod end +struct KinematicWaveStaggered <: AbstractRoutingMethod end +struct LocalInertial <: AbstractRoutingMethod end struct NoSubsurfaceFlow <: AbstractSubsurfaceFlowModel end -struct NoOverlandFlow <: AbstractOverlandFlowModel end -struct NoRiverFlow <: AbstractRiverFlowModel end +struct NoOverlandFlow{T} <: AbstractOverlandFlowModel{T} end +struct NoRiverFlow{T} <: AbstractRiverFlowModel{T} end """ Struct for storing routing model components overland flow `overland_flow`, river flow `river_flow` and subsurface flow `subsurface_flow`. """ @kwdef struct Routing{ - O <: AbstractOverlandFlowModel, - R <: AbstractRiverFlowModel, + O <: AbstractOverlandFlowModel{<:AbstractRoutingMethod}, + R <: AbstractRiverFlowModel{<:AbstractRoutingMethod}, S <: AbstractSubsurfaceFlowModel, } overland_flow::O = NoOverlandFlow() diff --git a/Wflow/src/routing/surface_flow.jl b/Wflow/src/routing/surface_flow.jl new file mode 100644 index 000000000..3fab3a2e8 --- /dev/null +++ b/Wflow/src/routing/surface_flow.jl @@ -0,0 +1,56 @@ +abstract type AbstractRiverFlowParameters end +abstract type AbstractRiverFlowVariables end + +abstract type AbstractOverlandFlowBC end +abstract type AbstractOverlandFlowParameters end +abstract type AbstractOverlandFlowVariables end + +"Struct for storing river flow model boundary conditions" +@with_kw struct RiverFlowBC{R} + n::Int + inwater::Vector{Float64} = zeros(n) # Lateral inflow [m³ s⁻¹] + external_inflow::Vector{Float64} = zeros(n) # External inflow (abstraction/supply/demand) [m³ s⁻¹] + actual_external_abstraction_av::Vector{Float64} = zeros(n) # Actual abstraction from external negative inflow [m³ s⁻¹] + abstraction::Vector{Float64} = zeros(n) # Abstraction (computed as part of water demand and allocation) [m³ s⁻¹] + reservoir::R # Reservoir model struct of arrays +end + +"Struct for storing Manning flow parameters" +@with_kw struct ManningFlowParameters + n::Int + beta::Float64 # constant in Manning's equation [-] + slope::Vector{Float64} # Slope [m m⁻¹] + mannings_n::Vector{Float64} # Manning's roughness [s m⁻⅓] + alpha_pow::Float64 # Used in the power part of alpha [-] + alpha_term::Vector{Float64} = fill(MISSING_VALUE, n) # Term used in computation of alpha [-] + alpha::Vector{Float64} = fill(MISSING_VALUE, n) # Constant in momentum equation A = alpha*Q^beta, based on Manning's equation [s3/5 m1/5] +end + +@with_kw struct RiverFlowModel{ + P <: AbstractRiverFlowParameters, + V <: AbstractRiverFlowVariables, + F <: Union{AbstractFloodPlain, Nothing}, + A <: AbstractAllocationModel, + T <: AbstractRoutingMethod, +} <: AbstractRiverFlowModel{T} + timestepping::TimeStepping + boundary_conditions::RiverFlowBC + parameters::P + variables::V + floodplain::F + allocation::A + routing_method::T +end + +@with_kw struct OverlandFlowModel{ + B <: AbstractOverlandFlowBC, + P <: Union{ManningFlowParameters, AbstractOverlandFlowParameters}, + V <: AbstractOverlandFlowVariables, + T <: AbstractRoutingMethod, +} <: AbstractOverlandFlowModel{T} + timestepping::TimeStepping + boundary_conditions::B + parameters::P + variables::V + routing_method::T +end diff --git a/Wflow/src/routing/surface_kinwave.jl b/Wflow/src/routing/surface_kinwave.jl index 34f43886b..c3042fbe7 100644 --- a/Wflow/src/routing/surface_kinwave.jl +++ b/Wflow/src/routing/surface_kinwave.jl @@ -1,5 +1,5 @@ "Struct for storing (shared) variables for river and overland flow models" -@with_kw struct FlowVariables +@with_kw struct FlowVariables <: AbstractRiverFlowVariables n::Int q::Vector{Float64} = zeros(n) # Discharge [m³ s⁻¹] qlat::Vector{Float64} = zeros(n) # Lateral inflow per unit length [m² s⁻¹] @@ -26,17 +26,6 @@ function init_kinematic_wave_timestepping(config::Config, n::Int; domain::String return timestepping end -"Struct for storing Manning flow parameters" -@with_kw struct ManningFlowParameters - n::Int - beta::Float64 # constant in Manning's equation [-] - slope::Vector{Float64} # Slope [m m⁻¹] - mannings_n::Vector{Float64} # Manning's roughness [s m⁻⅓] - alpha_pow::Float64 # Used in the power part of alpha [-] - alpha_term::Vector{Float64} = fill(MISSING_VALUE, n) # Term used in computation of alpha [-] - alpha::Vector{Float64} = fill(MISSING_VALUE, n) # Constant in momentum equation A = alpha*Q^beta, based on Manning's equation [s3/5 m1/5] -end - "Initialize Manning flow parameters" function ManningFlowParameters(mannings_n::Vector{Float64}, slope::Vector{Float64}) n = length(slope) @@ -51,7 +40,7 @@ function ManningFlowParameters(mannings_n::Vector{Float64}, slope::Vector{Float6 end "Struct for storing river flow model parameters" -@with_kw struct RiverFlowParameters +@with_kw struct RiverFlowParameters <: AbstractRiverFlowParameters flow::ManningFlowParameters bankfull_depth::Vector{Float64} # Bankfull water level [m] bankfull_storage::Vector{Float64} # Bankfull storage [m³] @@ -97,16 +86,6 @@ function RiverFlowParameters(dataset::NCDataset, config::Config, domain::DomainR return parameters end -"Struct for storing river flow model boundary conditions" -@with_kw struct RiverFlowBC{R} - n::Int - inwater::Vector{Float64} = zeros(n) # Lateral inflow [m³ s⁻¹] - external_inflow::Vector{Float64} = zeros(n) # External inflow (abstraction/supply/demand) [m³ s⁻¹] - actual_external_abstraction_av::Vector{Float64} = zeros(n) # Actual abstraction from external negative inflow [m³ s⁻¹] - abstraction::Vector{Float64} = zeros(n) # Abstraction (computed as part of water demand and allocation) [m³ s⁻¹] - reservoir::R # Reservoir model struct of arrays -end - "Initialize river flow model boundary conditions" function RiverFlowBC( dataset::NCDataset, @@ -128,20 +107,6 @@ function RiverFlowBC( return bc end -"River flow model using the kinematic wave method and the Manning flow equation" -@with_kw struct KinWaveRiverFlow{ - R <: RiverFlowBC, - F <: Union{AbstractFloodPlain, Nothing}, - A <: AbstractAllocationModel, -} <: AbstractRiverFlowModel - timestepping::TimeStepping - boundary_conditions::R - parameters::RiverFlowParameters - variables::FlowVariables - floodplain::F # Floodplain (1D) schematization - allocation::A # Water allocation -end - "Initialize river flow model `KinWaveRiverFlow`" function KinWaveRiverFlow( dataset::NCDataset, @@ -160,21 +125,23 @@ function KinWaveRiverFlow( parameters = RiverFlowParameters(dataset, config, domain) boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir) floodplain = nothing + routing_method = KinematicWave() - river_flow = KinWaveRiverFlow(; + river_flow = RiverFlowModel(; timestepping, boundary_conditions, parameters, variables, floodplain, allocation, + routing_method, ) return river_flow end "Struct for storing overland flow model variables" -@with_kw struct OverLandFlowVariables +@with_kw struct OverLandFlowVariables <: AbstractOverlandFlowVariables n::Int flow::FlowVariables = FlowVariables(; n) to_river::Vector{Float64} = zeros(n) # Part of overland flow [m³ s⁻¹] that flows to the river @@ -192,19 +159,11 @@ function Base.getproperty(v::OverLandFlowVariables, s::Symbol) end "Struct for storing overland flow model boundary conditions" -@with_kw struct LandFlowBC +@with_kw struct LandFlowBC <: AbstractOverlandFlowBC n::Int inwater::Vector{Float64} = zeros(n) # Lateral inflow [m³ s⁻¹] end -"Overland flow model using the kinematic wave method and the Manning flow{ equation" -@with_kw struct KinWaveOverlandFlow <: AbstractOverlandFlowModel - timestepping::TimeStepping - boundary_conditions::LandFlowBC - parameters::ManningFlowParameters - variables::OverLandFlowVariables -end - "Initialize Overland flow model `KinWaveOverlandFlow`" function KinWaveOverlandFlow(dataset::NCDataset, config::Config, domain::DomainLand) (; indices) = domain.network @@ -224,9 +183,15 @@ function KinWaveOverlandFlow(dataset::NCDataset, config::Config, domain::DomainL variables = OverLandFlowVariables(; n) parameters = ManningFlowParameters(mannings_n, slope) boundary_conditions = LandFlowBC(; n) + routing_method = KinematicWave() - overland_flow = - KinWaveOverlandFlow(; timestepping, boundary_conditions, variables, parameters) + overland_flow = OverlandFlowModel(; + timestepping, + boundary_conditions, + variables, + parameters, + routing_method, + ) return overland_flow end @@ -291,7 +256,7 @@ end "Update overland flow model `KinWaveOverlandFlow` for a single timestep" function kinwave_land_update!( - overland_flow_model::KinWaveOverlandFlow, + overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, domain::DomainLand, dt::Float64, ) @@ -351,7 +316,7 @@ Update overland flow model `KinWaveOverlandFlow` for a single timestep `dt`. Tim `dt` is either with a fixed timestep `dt_fixed` or adaptive. """ function update_overland_flow_model!( - overland_flow_model::KinWaveOverlandFlow, + overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, domain::DomainLand, dt::Float64, ) @@ -387,7 +352,7 @@ end "Update river flow model `KinWaveRiverFlow` for a single timestep" function kinwave_river_update!( - river_flow_model::KinWaveRiverFlow, + river_flow_model::AbstractRiverFlowModel{<:KinematicWave}, domain::DomainRiver, dt::Float64, dt_forcing::Float64, @@ -498,7 +463,7 @@ Update river flow model `KinWaveRiverFlow` for a single timestep `dt`. Timestepp `dt` is either with a fixed timestep `dt_fixed` or adaptive. """ function update_river_flow_model!( - river_flow_model::KinWaveRiverFlow, + river_flow_model::AbstractRiverFlowModel{<:KinematicWave}, domain::Domain, clock::Clock, ) @@ -549,7 +514,12 @@ function stable_timestep( flow_model::S, flow_length::Vector{Float64}, p::Float64, -) where {S <: Union{KinWaveOverlandFlow, KinWaveRiverFlow}} +) where { + S <: Union{ + AbstractRiverFlowModel{<:KinematicWave}, + AbstractOverlandFlowModel{<:KinematicWave}, + }, +} (; q) = flow_model.variables (; alpha, beta) = flow_model.parameters (; stable_timesteps) = flow_model.timestepping @@ -581,7 +551,7 @@ Update boundary condition lateral inflow `inwater` of a river flow model for a s timestep. """ function update_lateral_inflow!( - river_flow_model::AbstractRiverFlowModel, + river_flow_model::AbstractRiverFlowModel{<:AbstractRoutingMethod}, external_models::NamedTuple, domain::Domain, dt::Float64, @@ -608,7 +578,7 @@ Update boundary condition lateral inflow `inwater` of a kinematic wave overland `KinWaveOverlandFlow` for a single timestep. """ function update_lateral_inflow!( - overland_flow_model::KinWaveOverlandFlow, + overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, external_models::NamedTuple, area::Vector{Float64}, config::Config, @@ -638,7 +608,7 @@ flow model `AbstractRiverFlowModel` for a single timestep. """ function update_inflow!( model::Union{Reservoir, Nothing}, - river_flow::AbstractRiverFlowModel, + river_flow::AbstractRiverFlowModel{<:AbstractRoutingMethod}, external_models::NamedTuple, network::NetworkReservoir, ) @@ -655,16 +625,16 @@ end # For the river kinematic wave, the variable `to_river` can be excluded, because this part # is added to the river kinematic wave. get_inflow_reservoir( - ::KinWaveRiverFlow, - overland_flow_model::KinWaveOverlandFlow, + ::AbstractRiverFlowModel{<:KinematicWave}, + overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, inds::Vector{Int}, ) = overland_flow_model.variables.q_av[inds] get_inflow_reservoir( - ::KinWaveRiverFlow, + ::AbstractRiverFlowModel{<:KinematicWave}, subsurface_flow_model::LateralSSF, inds::Vector{Int}, ) = subsurface_flow_model.variables.ssf[inds] ./ tosecond(BASETIMESTEP) # Exclude subsurface flow from `GroundwaterFlow`. -get_inflow_reservoir(::AbstractRiverFlowModel, ::GroundwaterFlow, inds::Vector{Int}) = +get_inflow_reservoir(::AbstractRiverFlowModel{T}, ::GroundwaterFlow, inds::Vector{Int}) where {T<:AbstractRoutingMethod} = zeros(length(inds)) diff --git a/Wflow/src/routing/surface_local_inertial.jl b/Wflow/src/routing/surface_local_inertial.jl index b2e4bd53c..ab61127ca 100644 --- a/Wflow/src/routing/surface_local_inertial.jl +++ b/Wflow/src/routing/surface_local_inertial.jl @@ -1,23 +1,23 @@ -"Struct for storing local inertial river flow model parameters" -@with_kw struct LocalInertialRiverFlowParameters - n::Int # number of cells [-] - ne::Int # number of edges [-] - active_n::Vector{Int} # active nodes [-] - active_e::Vector{Int} # active edges [-] - froude_limit::Bool # if true a check is performed if froude number > 1.0 (algorithm is modified) [-] - h_thresh::Float64 # depth threshold for calculating flow [m] - zb::Vector{Float64} # river bed elevation [m] - zb_max::Vector{Float64} # maximum channel bed elevation [m] - bankfull_storage::Vector{Float64} # bankfull storage [m³] - bankfull_depth::Vector{Float64} # bankfull depth [m] - mannings_n_sq::Vector{Float64} # Manning's roughness squared at edge [(s m-1/3)2] - mannings_n::Vector{Float64} # Manning's roughness [s m-1/3] at node - flow_length_at_edge::Vector{Float64} # flow (river) length at edge [m] - flow_width_at_edge::Vector{Float64} # flow (river) width at edge [m] +"Struct for storing river flow model (numerical staggered scheme) parameters" +@with_kw struct RiverFlowStaggeredParameters <: AbstractRiverFlowParameters + n::Int # number of cells [-] + ne::Int # number of edges [-] + active_n::Vector{Int} # active nodes [-] + active_e::Vector{Int} # active edges [-] + froude_limit::Bool = false # if true a check is performed if froude number > 1.0 (algorithm is modified) [-] + h_thresh::Float64 = 0.0 # depth threshold for calculating flow [m] + zb::Vector{Float64} = Float64[] # river bed elevation [m] + zb_max::Vector{Float64} = Float64[] # maximum channel bed elevation [m] + bankfull_storage::Vector{Float64} # bankfull storage [m³] + bankfull_depth::Vector{Float64} = Float64[] # bankfull depth [m] + mannings_n_sq::Vector{Float64} = Float64[] # Manning's roughness squared at edge [(s m-1/3)2] + mannings_n::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] at node + flow_length_at_edge::Vector{Float64} = Float64[] # flow (river) length at edge [m] + flow_width_at_edge::Vector{Float64} = Float64[] # flow (river) width at edge [m] end "Initialize local inertial river flow model parameters" -function LocalInertialRiverFlowParameters( +function RiverFlowStaggeredParameters( dataset::NCDataset, config::Config, domain::DomainRiver, @@ -100,7 +100,7 @@ function LocalInertialRiverFlowParameters( end active_index = findall(x -> x == 0, reservoir_outlet) - parameters = LocalInertialRiverFlowParameters(; + parameters = RiverFlowStaggeredParameters(; n, ne = n_edges, active_n = active_index, @@ -120,26 +120,26 @@ function LocalInertialRiverFlowParameters( end "Struct for storing local inertial river flow model variables" -@with_kw struct LocalInertialRiverFlowVariables +@with_kw struct RiverFlowStaggeredVariables <: AbstractRiverFlowVariables n::Int n_edges::Int - q::Vector{Float64} = zeros(n_edges) # river discharge at edge (subgrid channel) [m³ s⁻¹] - q0::Vector{Float64} = zeros(n_edges) # river discharge at edge (subgrid channel) at previous time step [m³ s⁻¹] - q_av::Vector{Float64} # average river channel (+ floodplain) discharge at edge [m³ s⁻¹] (model timestep Δt) - q_channel_av::Vector{Float64} # average river channel discharge at edge [m³ s⁻¹] (for model timestep Δt) - h::Vector{Float64} # water depth [m] - zs_max::Vector{Float64} = zeros(n_edges) # maximum water elevation at edge [m] - zs_src::Vector{Float64} = zeros(n_edges) # water elevation of source node of edge [m] - zs_dst::Vector{Float64} = zeros(n_edges) # water elevation of downstream node of edge [m] - hf::Vector{Float64} = zeros(n_edges) # water depth at edge [m] - a::Vector{Float64} = zeros(n_edges) # flow area at edge [m²] - r::Vector{Float64} = zeros(n_edges) # wetted perimeter at edge [m] - storage::Vector{Float64} = zeros(n) # river storage [m³] - error::Vector{Float64} = zeros(n) # error storage [m³] + q::Vector{Float64} = zeros(n_edges) # river discharge at edge (subgrid channel) [m³ s⁻¹] + q0::Vector{Float64} = Float64[] # river discharge at edge (subgrid channel) at previous time step [m³ s⁻¹] + q_av::Vector{Float64} # average river channel (+ floodplain) discharge at edge [m³ s⁻¹] (model timestep Δt) + q_channel_av::Vector{Float64} # average river channel discharge at edge [m³ s⁻¹] (for model timestep Δt) + h::Vector{Float64} # water depth [m] + zs_max::Vector{Float64} = Float64[] # maximum water elevation at edge [m] + zs_src::Vector{Float64} = Float64[] # water elevation of source node of edge [m] + zs_dst::Vector{Float64} = Float64[] # water elevation of downstream node of edge [m] + hf::Vector{Float64} = zeros(n_edges) # water depth at edge [m] + a::Vector{Float64} = zeros(n_edges) # flow area at edge [m²] + r::Vector{Float64} = zeros(n_edges) # wetted perimeter at edge [m] + storage::Vector{Float64} = zeros(n) # river storage [m³] + error::Vector{Float64} = zeros(n) # error storage [m³] end "Initialize shallow water river flow model variables" -function LocalInertialRiverFlowVariables( +function RiverFlowStaggeredVariables( dataset::NCDataset, config::Config, network::NetworkRiver, @@ -160,32 +160,27 @@ function LocalInertialRiverFlowVariables( # set river depth h to zero (including reservoir locations) h = zeros(n) q_av = zeros(n_edges) + q0 = zeros(n_edges) + zs_max = zeros(n_edges) + zs_src = zeros(n_edges) + zs_dst = zeros(n_edges) # set ghost points for boundary condition (downstream river outlet): river depth `h` append!(h, riverdepth_bc) - variables = LocalInertialRiverFlowVariables(; + + variables = RiverFlowStaggeredVariables(; n, n_edges, q_av, + q0, q_channel_av = config.model.floodplain_1d__flag ? zeros(n_edges) : q_av, h, + zs_max, + zs_src, + zs_dst, ) return variables end -"Shallow water river flow model using the local inertial method" -@with_kw struct LocalInertialRiverFlow{ - R <: RiverFlowBC, - F <: Union{AbstractFloodPlain, Nothing}, - A <: AbstractAllocationModel, -} <: AbstractRiverFlowModel - timestepping::TimeStepping - boundary_conditions::R - parameters::LocalInertialRiverFlowParameters - variables::LocalInertialRiverFlowVariables - floodplain::F # Floodplain (1D) schematization - allocation::A # Water allocation -end - "Initialize shallow water river flow model `LocalInertialRiverFlow`" function LocalInertialRiverFlow( dataset::NCDataset, @@ -208,8 +203,8 @@ function LocalInertialRiverFlow( cfl = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) timestepping = TimeStepping(; cfl) - parameters = LocalInertialRiverFlowParameters(dataset, config, domain) - variables = LocalInertialRiverFlowVariables(dataset, config, domain.network) + parameters = RiverFlowStaggeredParameters(dataset, config, domain) + variables = RiverFlowStaggeredVariables(dataset, config, domain.network) boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir) if config.model.floodplain_1d__flag @@ -218,22 +213,24 @@ function LocalInertialRiverFlow( else floodplain = nothing end + routing_method = LocalInertial() n = length(domain.network.indices) - river_flow = LocalInertialRiverFlow(; + river_flow = RiverFlowModel(; timestepping, boundary_conditions, parameters, variables, floodplain, allocation = do_water_demand(config) ? AllocationRiver(n) : NoAllocationRiver(n), + routing_method, ) return river_flow end "Return the upstream inflow for a reservoir in `LocalInertialRiverFlow`" function get_inflow_reservoir( - river_flow_model::LocalInertialRiverFlow, + river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, src_edge::Vector{Int}, ) q_in = sum_at(river_flow_model.variables.q, src_edge) @@ -246,13 +243,13 @@ end # For local inertial river routing, `to_river` is included, as reservoir cells are excluded # (boundary condition). get_inflow_reservoir( - ::LocalInertialRiverFlow, - overland_flow_model::KinWaveOverlandFlow, + ::AbstractRiverFlowModel{<:LocalInertial}, + overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, inds::Vector{Int}, ) = overland_flow_model.variables.q_av[inds] .+ overland_flow_model.variables.to_river[inds] get_inflow_reservoir( - ::LocalInertialRiverFlow, + ::AbstractRiverFlowModel{<:LocalInertial}, subsurface_flow_model::LateralSSF, inds::Vector{Int}, ) = @@ -263,7 +260,7 @@ get_inflow_reservoir( "Update local inertial river flow model `LocalInertialRiverFlow` for a single timestep" function local_inertial_river_update!( - river_flow_model::LocalInertialRiverFlow, + river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, domain::Domain, dt::Float64, dt_forcing::Float64, @@ -504,7 +501,7 @@ Update local inertial river flow model `LocalInertialRiverFlow` for a single tim timestepping method is used (computing a sub timestep `dt_s`). """ function update_river_flow_model!( - river_flow_model::LocalInertialRiverFlow, + river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, domain::Domain, clock::Clock; update_h = true, @@ -543,7 +540,7 @@ function update_river_flow_model!( end "Struct to store local inertial overland flow model variables" -@with_kw struct LocalInertialOverlandFlowVariables +@with_kw struct LocalInertialOverlandFlowVariables <: AbstractOverlandFlowVariables n::Int # flow in y direction at edge at previous time step [m³ s⁻¹] qy0::Vector{Float64} = zeros(n + 1) @@ -566,7 +563,7 @@ end end "Struct to store local inertial overland flow model parameters" -@with_kw struct LocalInertialOverlandFlowParameters +@with_kw struct LocalInertialOverlandFlowParameters <: AbstractOverlandFlowParameters n::Int # number of cells [-] xwidth::Vector{Float64} # effective flow width x direction at edge (floodplain) [m] ywidth::Vector{Float64} # effective flow width y direction at edge (floodplain) [m] @@ -649,19 +646,11 @@ function LocalInertialOverlandFlowParameters( end "Struct to store local inertial overland flow model boundary conditions" -@with_kw struct LocalInertialOverlandFlowBC +@with_kw struct LocalInertialOverlandFlowBC <: AbstractOverlandFlowBC n::Int runoff::Vector{Float64} = zeros(n) # runoff from hydrological model [m³ s⁻¹] end -"Local inertial overland flow model using the local inertial method" -@with_kw struct LocalInertialOverlandFlow <: AbstractOverlandFlowModel - timestepping::TimeStepping - boundary_conditions::LocalInertialOverlandFlowBC - parameters::LocalInertialOverlandFlowParameters - variables::LocalInertialOverlandFlowVariables -end - "Initialize local inertial overland flow model" function LocalInertialOverlandFlow(dataset::NCDataset, config::Config, domain::Domain) cfl = config.model.land_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) @@ -671,12 +660,14 @@ function LocalInertialOverlandFlow(dataset::NCDataset, config::Config, domain::D boundary_conditions = LocalInertialOverlandFlowBC(; n) parameters = LocalInertialOverlandFlowParameters(dataset, config, domain) variables = LocalInertialOverlandFlowVariables(; n) + routing_method = LocalInertial() - overland_flow = LocalInertialOverlandFlow(; + overland_flow = OverlandFlowModel(; timestepping, boundary_conditions, parameters, variables, + routing_method, ) return overland_flow @@ -691,7 +682,7 @@ Compute a stable timestep size for the local inertial approach, based on Bates e dt = cfl * (Δx / sqrt(g max(h)) """ function stable_timestep( - river_flow_model::LocalInertialRiverFlow, + river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, flow_length::Vector{Float64}, ) dt_min = Inf @@ -708,7 +699,7 @@ function stable_timestep( end function stable_timestep( - overland_flow_model::LocalInertialOverlandFlow, + overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}, parameters::LandParameters, ) dt_min = Inf @@ -733,7 +724,7 @@ Update boundary condition `runoff` overland flow model `LocalInertialOverlandFlo single timestep. """ function update_bc_overland_flow_model!( - overland_flow_model::LocalInertialOverlandFlow, + overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}, external_models::NamedTuple, domain::Domain, dt::Float64, @@ -757,7 +748,7 @@ Update subsurface flow contribution to inflow of a reservoir model for a river f """ function update_inflow!( reservoir_model::Reservoir, - river_flow_model::LocalInertialRiverFlow, + river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, subsurface_flow::AbstractSubsurfaceFlowModel, network::NetworkReservoir, ) @@ -769,7 +760,7 @@ function update_inflow!( end update_inflow!( ::Nothing, - ::LocalInertialRiverFlow, + ::AbstractRiverFlowModel{<:LocalInertial}, ::AbstractSubsurfaceFlowModel, ::NetworkReservoir, ) = nothing @@ -779,7 +770,7 @@ Helper function to set flow variables of the `LocalInertialOverlandFlow` model t is done at the start of each simulation timestep, during the timestep the total (weighted) sum is computed from values at each sub timestep. """ -function set_flow_vars!(overland_flow_model::LocalInertialOverlandFlow) +function set_flow_vars!(overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}) (; qx_av, qy_av) = overland_flow_model.variables qx_av .= 0.0 qy_av .= 0.0 @@ -790,7 +781,10 @@ end Helper function to compute average flow variables of the `LocalInertialOverlandFlow` model. This is done at the end of each simulation timestep. """ -function average_flow_vars!(overland_flow_model::LocalInertialOverlandFlow, dt::Float64) +function average_flow_vars!( + overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}, + dt::Float64, +) (; qx_av, qy_av) = overland_flow_model.variables qx_av ./= dt qy_av ./= dt @@ -803,8 +797,8 @@ models for a single timestep `dt`. An adaptive timestepping method is used (comp timestep `dt_s`). """ function update_overland_flow_model!( - overland_flow_model::LocalInertialOverlandFlow, - river_flow_model::LocalInertialRiverFlow, + overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}, + river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, domain::Domain, clock::Clock; update_h = false, @@ -850,7 +844,7 @@ Update fluxes for overland flow `LocalInertialOverlandFlow` model for a single t `dt`. """ function local_inertial_update_fluxes!( - land::LocalInertialOverlandFlow, + land::AbstractOverlandFlowModel{<:LocalInertial}, domain::Domain, dt::Float64, ) @@ -954,7 +948,7 @@ river `LocalInertialRiverFlow`and overland flow `LocalInertialOverlandFlow` mode single timestep. """ function update_inflow_reservoir!( - land::LocalInertialOverlandFlow, + land::AbstractOverlandFlowModel{<:LocalInertial}, reservoir::Union{Reservoir, Nothing}, domain::Domain, ) @@ -978,8 +972,8 @@ Update storage and water depth for combined river `LocalInertialRiverFlow`and ov `LocalInertialOverlandFlow` models for a single timestep `dt`. """ function local_inertial_update_water_depth!( - land::LocalInertialOverlandFlow, - river::LocalInertialRiverFlow, + land::AbstractOverlandFlowModel{<:LocalInertial}, + river::AbstractRiverFlowModel{<:LocalInertial}, domain::Domain, dt::Float64, ) diff --git a/Wflow/src/routing/surface_routing.jl b/Wflow/src/routing/surface_routing.jl index dee71ac93..13639e9f3 100644 --- a/Wflow/src/routing/surface_routing.jl +++ b/Wflow/src/routing/surface_routing.jl @@ -55,7 +55,12 @@ Run surface routing (land and river) for a model type that contains the routing """ function surface_routing!( model::Model{R}, -) where {R <: Routing{<:LocalInertialOverlandFlow, <:LocalInertialRiverFlow}} +) where { + R <: Routing{ + <:AbstractOverlandFlowModel{<:LocalInertial}, + <:AbstractRiverFlowModel{<:LocalInertial}, + }, +} (; routing, land, domain, clock, config) = model (; soil, runoff) = land (; overland_flow, river_flow, subsurface_flow) = routing diff --git a/Wflow/src/sediment_flux.jl b/Wflow/src/sediment_flux.jl index 9fec1d7d4..825afd59f 100644 --- a/Wflow/src/sediment_flux.jl +++ b/Wflow/src/sediment_flux.jl @@ -3,7 +3,7 @@ TT <: AbstractTransportCapacityModel, SF <: AbstractSedimentLandTransportModel, TR <: AbstractSedimentToRiverModel, -} <: AbstractOverlandFlowModel +} <: AbstractOverlandFlowModel{AccucapacityFlux} hydrological_forcing::HydrologicalForcing transport_capacity::TT sediment_flux::SF @@ -121,7 +121,7 @@ end ER <: AbstractRiverErosionModel, SFR <: AbstractSedimentRiverTransportModel, CR <: AbstractSedimentConcentrationsRiverModel, -} <: AbstractRiverFlowModel +} <: AbstractRiverFlowModel{AccucapacityFlux} hydrological_forcing::HydrologicalForcing transport_capacity::TTR potential_erosion::ER diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index d3919db35..9e7b548a9 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -341,7 +341,7 @@ end push!(h_init, h_a[n]) timestepping = Wflow.TimeStepping(; cfl = 0.7) - parameters = Wflow.LocalInertialRiverFlowParameters(; + parameters = Wflow.RiverFlowStaggeredParameters(; n, ne = _ne, active_n = collect(1:(n - 1)), @@ -358,7 +358,7 @@ end froude_limit, ) - variables = Wflow.LocalInertialRiverFlowVariables(; + variables = Wflow.RiverFlowStaggeredVariables(; n, n_edges = _ne, q0 = zeros(_ne), @@ -378,13 +378,14 @@ end boundary_conditions = Wflow.RiverFlowBC(; n, reservoir = nothing) - sw_river = Wflow.LocalInertialRiverFlow(; + sw_river = Wflow.RiverFlowModel(; timestepping, boundary_conditions, parameters, variables, floodplain = nothing, allocation = Wflow.NoAllocationRiver(n), + routing_method = Wflow.LocalInertial(), ) # run until steady state is reached diff --git a/Wflow/test/testing_utils.jl b/Wflow/test/testing_utils.jl index 970a530b7..9b43fc81b 100644 --- a/Wflow/test/testing_utils.jl +++ b/Wflow/test/testing_utils.jl @@ -195,8 +195,9 @@ end River Flow Model without any restrictions on the fields, so that only the data required in certain functions has to be supplied (e.g. in the form of NamedTuple). """ -@kwdef struct DummyRiver{A, B, V} <: Wflow.AbstractRiverFlowModel +@kwdef struct DummyRiver{A, B, V, T} <: Wflow.AbstractRiverFlowModel{T} allocation::A = nothing boundary_conditions::B = nothing variables::V = nothing + routing_method::T = nothing end From 94a1ec4a6bea47d8b39420df3b443024cffe1f2f Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 18 Mar 2026 10:38:17 +0100 Subject: [PATCH 06/90] Change abstract surface flow routing types Remove `AbstractRoutingMethod` from these abstract types and add the routing method as first field to `RiverFlowModel` and `OverlandFlowModel`. This simplifies dispatching a bit and looks a bit cleaner. --- Wflow/src/bmi.jl | 4 +-- Wflow/src/mass_balance.jl | 18 +++++----- Wflow/src/routing/routing.jl | 12 +++---- Wflow/src/routing/surface_flow.jl | 12 +++---- Wflow/src/routing/surface_kinwave.jl | 26 +++++++------- Wflow/src/routing/surface_local_inertial.jl | 38 ++++++++++----------- Wflow/src/routing/surface_routing.jl | 4 +-- Wflow/src/sediment_flux.jl | 4 +-- Wflow/test/testing_utils.jl | 3 +- 9 files changed, 60 insertions(+), 61 deletions(-) diff --git a/Wflow/src/bmi.jl b/Wflow/src/bmi.jl index 3af79a3cd..866bd7f86 100644 --- a/Wflow/src/bmi.jl +++ b/Wflow/src/bmi.jl @@ -403,8 +403,8 @@ function grid_element_type( var::PropertyLens, ) where { T <: Union{ - AbstractRiverFlowModel{<:LocalInertial}, - AbstractOverlandFlowModel{<:LocalInertial}, + RiverFlowModel{<:LocalInertial}, + OverlandFlowModel{<:LocalInertial}, }, } vars = (PropertyLens(x) for x in (:q, :q_av, :qx, :qy)) diff --git a/Wflow/src/mass_balance.jl b/Wflow/src/mass_balance.jl index acf0a0347..3b1fea823 100644 --- a/Wflow/src/mass_balance.jl +++ b/Wflow/src/mass_balance.jl @@ -138,7 +138,7 @@ end Return storage of a river flow model at index `i`. For `LocalInertialRiverFlow` floodplain storage is added to river storage if an optional floodplain is included. """ -function get_storage(river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, i) +function get_storage(river_flow_model::RiverFlowModel{<:LocalInertial}, i) (; storage) = river_flow_model.variables if isnothing(river_flow_model.floodplain) return storage[i] @@ -147,7 +147,7 @@ function get_storage(river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, return total_storage end end -get_storage(river_flow_model::AbstractRiverFlowModel{<:KinematicWave}, i) = +get_storage(river_flow_model::RiverFlowModel{<:KinematicWave}, i) = river_flow_model.variables.storage[i] """ @@ -333,7 +333,7 @@ compute_flow_balance!(reservoir::Nothing, water_balance::NoMassBalance, dt::Floa "Compute water mass balance error and relative error for river kinematic wave routing." function compute_flow_balance!( - river_flow::AbstractRiverFlowModel{<:KinematicWave}, + river_flow::RiverFlowModel{<:KinematicWave}, water_balance::MassBalance, network::NetworkRiver, dt::Float64, @@ -358,7 +358,7 @@ Compute water mass balance error and relative error for river (and floodplain) l inertial routing. """ function compute_flow_balance!( - river_flow::AbstractRiverFlowModel{<:LocalInertial}, + river_flow::RiverFlowModel{<:LocalInertial}, water_balance::MassBalance, network::NetworkRiver, dt::Float64, @@ -417,7 +417,7 @@ Compute water mass balance error and relative error for overland flow kinematic routing. """ function compute_flow_balance!( - overland_flow::AbstractOverlandFlowModel{<:KinematicWave}, + overland_flow::OverlandFlowModel{<:KinematicWave}, water_balance::MassBalance, dt::Float64, ) @@ -441,8 +441,8 @@ Compute water mass balance error and relative error for 1D river local inertial computed for each land cell (total storage) considering both river and overland flow. """ function compute_flow_balance!( - river_flow::AbstractRiverFlowModel{<:LocalInertial}, - overland_flow::AbstractOverlandFlowModel{<:LocalInertial}, + river_flow::RiverFlowModel{<:LocalInertial}, + overland_flow::OverlandFlowModel{<:LocalInertial}, water_balance::MassBalance, domain::Domain, dt::Float64, @@ -579,8 +579,8 @@ function compute_flow_routing_balance!( model::Model{R}, ) where { R <: Routing{ - <:AbstractOverlandFlowModel{<:LocalInertial}, - <:AbstractRiverFlowModel{<:LocalInertial}, + <:OverlandFlowModel{<:LocalInertial}, + <:RiverFlowModel{<:LocalInertial}, }, } (; river_flow, overland_flow, subsurface_flow) = model.routing diff --git a/Wflow/src/routing/routing.jl b/Wflow/src/routing/routing.jl index 69a0775f1..fe4da1591 100644 --- a/Wflow/src/routing/routing.jl +++ b/Wflow/src/routing/routing.jl @@ -1,5 +1,5 @@ -abstract type AbstractRiverFlowModel{T} end -abstract type AbstractOverlandFlowModel{T} end +abstract type AbstractRiverFlowModel end +abstract type AbstractOverlandFlowModel end abstract type AbstractSubsurfaceFlowModel end abstract type AbstractRoutingMethod end @@ -9,16 +9,16 @@ struct KinematicWaveStaggered <: AbstractRoutingMethod end struct LocalInertial <: AbstractRoutingMethod end struct NoSubsurfaceFlow <: AbstractSubsurfaceFlowModel end -struct NoOverlandFlow{T} <: AbstractOverlandFlowModel{T} end -struct NoRiverFlow{T} <: AbstractRiverFlowModel{T} end +struct NoOverlandFlow <: AbstractOverlandFlowModel end +struct NoRiverFlow <: AbstractRiverFlowModel end """ Struct for storing routing model components overland flow `overland_flow`, river flow `river_flow` and subsurface flow `subsurface_flow`. """ @kwdef struct Routing{ - O <: AbstractOverlandFlowModel{<:AbstractRoutingMethod}, - R <: AbstractRiverFlowModel{<:AbstractRoutingMethod}, + O <: AbstractOverlandFlowModel, + R <: AbstractRiverFlowModel, S <: AbstractSubsurfaceFlowModel, } overland_flow::O = NoOverlandFlow() diff --git a/Wflow/src/routing/surface_flow.jl b/Wflow/src/routing/surface_flow.jl index 3fab3a2e8..c19adfd41 100644 --- a/Wflow/src/routing/surface_flow.jl +++ b/Wflow/src/routing/surface_flow.jl @@ -27,30 +27,30 @@ end end @with_kw struct RiverFlowModel{ + T <: AbstractRoutingMethod, P <: AbstractRiverFlowParameters, V <: AbstractRiverFlowVariables, F <: Union{AbstractFloodPlain, Nothing}, A <: AbstractAllocationModel, - T <: AbstractRoutingMethod, -} <: AbstractRiverFlowModel{T} +} <: AbstractRiverFlowModel + routing_method::T timestepping::TimeStepping boundary_conditions::RiverFlowBC parameters::P variables::V floodplain::F allocation::A - routing_method::T end @with_kw struct OverlandFlowModel{ + T <: AbstractRoutingMethod, B <: AbstractOverlandFlowBC, P <: Union{ManningFlowParameters, AbstractOverlandFlowParameters}, V <: AbstractOverlandFlowVariables, - T <: AbstractRoutingMethod, -} <: AbstractOverlandFlowModel{T} +} <: AbstractOverlandFlowModel + routing_method::T timestepping::TimeStepping boundary_conditions::B parameters::P variables::V - routing_method::T end diff --git a/Wflow/src/routing/surface_kinwave.jl b/Wflow/src/routing/surface_kinwave.jl index c3042fbe7..5e58707f9 100644 --- a/Wflow/src/routing/surface_kinwave.jl +++ b/Wflow/src/routing/surface_kinwave.jl @@ -256,7 +256,7 @@ end "Update overland flow model `KinWaveOverlandFlow` for a single timestep" function kinwave_land_update!( - overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, + overland_flow_model::OverlandFlowModel{<:KinematicWave}, domain::DomainLand, dt::Float64, ) @@ -316,7 +316,7 @@ Update overland flow model `KinWaveOverlandFlow` for a single timestep `dt`. Tim `dt` is either with a fixed timestep `dt_fixed` or adaptive. """ function update_overland_flow_model!( - overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, + overland_flow_model::OverlandFlowModel{<:KinematicWave}, domain::DomainLand, dt::Float64, ) @@ -352,7 +352,7 @@ end "Update river flow model `KinWaveRiverFlow` for a single timestep" function kinwave_river_update!( - river_flow_model::AbstractRiverFlowModel{<:KinematicWave}, + river_flow_model::RiverFlowModel{<:KinematicWave}, domain::DomainRiver, dt::Float64, dt_forcing::Float64, @@ -463,7 +463,7 @@ Update river flow model `KinWaveRiverFlow` for a single timestep `dt`. Timestepp `dt` is either with a fixed timestep `dt_fixed` or adaptive. """ function update_river_flow_model!( - river_flow_model::AbstractRiverFlowModel{<:KinematicWave}, + river_flow_model::RiverFlowModel{<:KinematicWave}, domain::Domain, clock::Clock, ) @@ -516,8 +516,8 @@ function stable_timestep( p::Float64, ) where { S <: Union{ - AbstractRiverFlowModel{<:KinematicWave}, - AbstractOverlandFlowModel{<:KinematicWave}, + RiverFlowModel{<:KinematicWave}, + OverlandFlowModel{<:KinematicWave}, }, } (; q) = flow_model.variables @@ -551,7 +551,7 @@ Update boundary condition lateral inflow `inwater` of a river flow model for a s timestep. """ function update_lateral_inflow!( - river_flow_model::AbstractRiverFlowModel{<:AbstractRoutingMethod}, + river_flow_model::AbstractRiverFlowModel, external_models::NamedTuple, domain::Domain, dt::Float64, @@ -578,7 +578,7 @@ Update boundary condition lateral inflow `inwater` of a kinematic wave overland `KinWaveOverlandFlow` for a single timestep. """ function update_lateral_inflow!( - overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, + overland_flow_model::OverlandFlowModel{<:KinematicWave}, external_models::NamedTuple, area::Vector{Float64}, config::Config, @@ -608,7 +608,7 @@ flow model `AbstractRiverFlowModel` for a single timestep. """ function update_inflow!( model::Union{Reservoir, Nothing}, - river_flow::AbstractRiverFlowModel{<:AbstractRoutingMethod}, + river_flow::AbstractRiverFlowModel, external_models::NamedTuple, network::NetworkReservoir, ) @@ -625,16 +625,16 @@ end # For the river kinematic wave, the variable `to_river` can be excluded, because this part # is added to the river kinematic wave. get_inflow_reservoir( - ::AbstractRiverFlowModel{<:KinematicWave}, - overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, + ::RiverFlowModel{<:KinematicWave}, + overland_flow_model::OverlandFlowModel{<:KinematicWave}, inds::Vector{Int}, ) = overland_flow_model.variables.q_av[inds] get_inflow_reservoir( - ::AbstractRiverFlowModel{<:KinematicWave}, + ::RiverFlowModel{<:KinematicWave}, subsurface_flow_model::LateralSSF, inds::Vector{Int}, ) = subsurface_flow_model.variables.ssf[inds] ./ tosecond(BASETIMESTEP) # Exclude subsurface flow from `GroundwaterFlow`. -get_inflow_reservoir(::AbstractRiverFlowModel{T}, ::GroundwaterFlow, inds::Vector{Int}) where {T<:AbstractRoutingMethod} = +get_inflow_reservoir(::AbstractRiverFlowModel, ::GroundwaterFlow, inds::Vector{Int}) = zeros(length(inds)) diff --git a/Wflow/src/routing/surface_local_inertial.jl b/Wflow/src/routing/surface_local_inertial.jl index ab61127ca..5037aa0d2 100644 --- a/Wflow/src/routing/surface_local_inertial.jl +++ b/Wflow/src/routing/surface_local_inertial.jl @@ -230,7 +230,7 @@ end "Return the upstream inflow for a reservoir in `LocalInertialRiverFlow`" function get_inflow_reservoir( - river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:LocalInertial}, src_edge::Vector{Int}, ) q_in = sum_at(river_flow_model.variables.q, src_edge) @@ -243,13 +243,13 @@ end # For local inertial river routing, `to_river` is included, as reservoir cells are excluded # (boundary condition). get_inflow_reservoir( - ::AbstractRiverFlowModel{<:LocalInertial}, - overland_flow_model::AbstractOverlandFlowModel{<:KinematicWave}, + ::RiverFlowModel{<:LocalInertial}, + overland_flow_model::OverlandFlowModel{<:KinematicWave}, inds::Vector{Int}, ) = overland_flow_model.variables.q_av[inds] .+ overland_flow_model.variables.to_river[inds] get_inflow_reservoir( - ::AbstractRiverFlowModel{<:LocalInertial}, + ::RiverFlowModel{<:LocalInertial}, subsurface_flow_model::LateralSSF, inds::Vector{Int}, ) = @@ -260,7 +260,7 @@ get_inflow_reservoir( "Update local inertial river flow model `LocalInertialRiverFlow` for a single timestep" function local_inertial_river_update!( - river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:LocalInertial}, domain::Domain, dt::Float64, dt_forcing::Float64, @@ -501,7 +501,7 @@ Update local inertial river flow model `LocalInertialRiverFlow` for a single tim timestepping method is used (computing a sub timestep `dt_s`). """ function update_river_flow_model!( - river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:LocalInertial}, domain::Domain, clock::Clock; update_h = true, @@ -682,7 +682,7 @@ Compute a stable timestep size for the local inertial approach, based on Bates e dt = cfl * (Δx / sqrt(g max(h)) """ function stable_timestep( - river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:LocalInertial}, flow_length::Vector{Float64}, ) dt_min = Inf @@ -699,7 +699,7 @@ function stable_timestep( end function stable_timestep( - overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}, + overland_flow_model::OverlandFlowModel{<:LocalInertial}, parameters::LandParameters, ) dt_min = Inf @@ -724,7 +724,7 @@ Update boundary condition `runoff` overland flow model `LocalInertialOverlandFlo single timestep. """ function update_bc_overland_flow_model!( - overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}, + overland_flow_model::OverlandFlowModel{<:LocalInertial}, external_models::NamedTuple, domain::Domain, dt::Float64, @@ -748,7 +748,7 @@ Update subsurface flow contribution to inflow of a reservoir model for a river f """ function update_inflow!( reservoir_model::Reservoir, - river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:LocalInertial}, subsurface_flow::AbstractSubsurfaceFlowModel, network::NetworkReservoir, ) @@ -760,7 +760,7 @@ function update_inflow!( end update_inflow!( ::Nothing, - ::AbstractRiverFlowModel{<:LocalInertial}, + ::RiverFlowModel{<:LocalInertial}, ::AbstractSubsurfaceFlowModel, ::NetworkReservoir, ) = nothing @@ -770,7 +770,7 @@ Helper function to set flow variables of the `LocalInertialOverlandFlow` model t is done at the start of each simulation timestep, during the timestep the total (weighted) sum is computed from values at each sub timestep. """ -function set_flow_vars!(overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}) +function set_flow_vars!(overland_flow_model::OverlandFlowModel{<:LocalInertial}) (; qx_av, qy_av) = overland_flow_model.variables qx_av .= 0.0 qy_av .= 0.0 @@ -782,7 +782,7 @@ Helper function to compute average flow variables of the `LocalInertialOverlandF This is done at the end of each simulation timestep. """ function average_flow_vars!( - overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}, + overland_flow_model::OverlandFlowModel{<:LocalInertial}, dt::Float64, ) (; qx_av, qy_av) = overland_flow_model.variables @@ -797,8 +797,8 @@ models for a single timestep `dt`. An adaptive timestepping method is used (comp timestep `dt_s`). """ function update_overland_flow_model!( - overland_flow_model::AbstractOverlandFlowModel{<:LocalInertial}, - river_flow_model::AbstractRiverFlowModel{<:LocalInertial}, + overland_flow_model::OverlandFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:LocalInertial}, domain::Domain, clock::Clock; update_h = false, @@ -844,7 +844,7 @@ Update fluxes for overland flow `LocalInertialOverlandFlow` model for a single t `dt`. """ function local_inertial_update_fluxes!( - land::AbstractOverlandFlowModel{<:LocalInertial}, + land::OverlandFlowModel{<:LocalInertial}, domain::Domain, dt::Float64, ) @@ -948,7 +948,7 @@ river `LocalInertialRiverFlow`and overland flow `LocalInertialOverlandFlow` mode single timestep. """ function update_inflow_reservoir!( - land::AbstractOverlandFlowModel{<:LocalInertial}, + land::OverlandFlowModel{<:LocalInertial}, reservoir::Union{Reservoir, Nothing}, domain::Domain, ) @@ -972,8 +972,8 @@ Update storage and water depth for combined river `LocalInertialRiverFlow`and ov `LocalInertialOverlandFlow` models for a single timestep `dt`. """ function local_inertial_update_water_depth!( - land::AbstractOverlandFlowModel{<:LocalInertial}, - river::AbstractRiverFlowModel{<:LocalInertial}, + land::OverlandFlowModel{<:LocalInertial}, + river::RiverFlowModel{<:LocalInertial}, domain::Domain, dt::Float64, ) diff --git a/Wflow/src/routing/surface_routing.jl b/Wflow/src/routing/surface_routing.jl index 13639e9f3..520bfcff4 100644 --- a/Wflow/src/routing/surface_routing.jl +++ b/Wflow/src/routing/surface_routing.jl @@ -57,8 +57,8 @@ function surface_routing!( model::Model{R}, ) where { R <: Routing{ - <:AbstractOverlandFlowModel{<:LocalInertial}, - <:AbstractRiverFlowModel{<:LocalInertial}, + <:OverlandFlowModel{<:LocalInertial}, + <:RiverFlowModel{<:LocalInertial}, }, } (; routing, land, domain, clock, config) = model diff --git a/Wflow/src/sediment_flux.jl b/Wflow/src/sediment_flux.jl index 825afd59f..9fec1d7d4 100644 --- a/Wflow/src/sediment_flux.jl +++ b/Wflow/src/sediment_flux.jl @@ -3,7 +3,7 @@ TT <: AbstractTransportCapacityModel, SF <: AbstractSedimentLandTransportModel, TR <: AbstractSedimentToRiverModel, -} <: AbstractOverlandFlowModel{AccucapacityFlux} +} <: AbstractOverlandFlowModel hydrological_forcing::HydrologicalForcing transport_capacity::TT sediment_flux::SF @@ -121,7 +121,7 @@ end ER <: AbstractRiverErosionModel, SFR <: AbstractSedimentRiverTransportModel, CR <: AbstractSedimentConcentrationsRiverModel, -} <: AbstractRiverFlowModel{AccucapacityFlux} +} <: AbstractRiverFlowModel hydrological_forcing::HydrologicalForcing transport_capacity::TTR potential_erosion::ER diff --git a/Wflow/test/testing_utils.jl b/Wflow/test/testing_utils.jl index 9b43fc81b..970a530b7 100644 --- a/Wflow/test/testing_utils.jl +++ b/Wflow/test/testing_utils.jl @@ -195,9 +195,8 @@ end River Flow Model without any restrictions on the fields, so that only the data required in certain functions has to be supplied (e.g. in the form of NamedTuple). """ -@kwdef struct DummyRiver{A, B, V, T} <: Wflow.AbstractRiverFlowModel{T} +@kwdef struct DummyRiver{A, B, V} <: Wflow.AbstractRiverFlowModel allocation::A = nothing boundary_conditions::B = nothing variables::V = nothing - routing_method::T = nothing end From fcdc2338c39511903db2b2e87cddd302710ea69d Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 18 Mar 2026 11:28:05 +0100 Subject: [PATCH 07/90] Update docstrings --- Wflow/src/routing/surface_kinwave.jl | 14 +++--- Wflow/src/routing/surface_local_inertial.jl | 50 ++++++++++----------- Wflow/src/routing/surface_routing.jl | 12 +++-- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/Wflow/src/routing/surface_kinwave.jl b/Wflow/src/routing/surface_kinwave.jl index 5e58707f9..4d3328967 100644 --- a/Wflow/src/routing/surface_kinwave.jl +++ b/Wflow/src/routing/surface_kinwave.jl @@ -254,7 +254,7 @@ function average_flow_vars!(river_flow_model::AbstractRiverFlowModel, dt::Float6 return nothing end -"Update overland flow model `KinWaveOverlandFlow` for a single timestep" +"Update overland flow model `OverlandFlowModel{<:KinematicWave}` for a single timestep" function kinwave_land_update!( overland_flow_model::OverlandFlowModel{<:KinematicWave}, domain::DomainLand, @@ -312,8 +312,8 @@ function kinwave_land_update!( end """ -Update overland flow model `KinWaveOverlandFlow` for a single timestep `dt`. Timestepping within -`dt` is either with a fixed timestep `dt_fixed` or adaptive. +Update overland flow model `OverlandFlowModel{<:KinematicWave}` for a single timestep `dt`. +Timestepping within `dt` is either with a fixed timestep `dt_fixed` or adaptive. """ function update_overland_flow_model!( overland_flow_model::OverlandFlowModel{<:KinematicWave}, @@ -350,7 +350,7 @@ function update_overland_flow_model!( return nothing end -"Update river flow model `KinWaveRiverFlow` for a single timestep" +"Update river flow model `RiverFlowModel{<:KinematicWave}` for a single timestep" function kinwave_river_update!( river_flow_model::RiverFlowModel{<:KinematicWave}, domain::DomainRiver, @@ -459,8 +459,8 @@ function kinwave_river_update!( end """ -Update river flow model `KinWaveRiverFlow` for a single timestep `dt`. Timestepping within -`dt` is either with a fixed timestep `dt_fixed` or adaptive. +Update river flow model `RiverFlowModel{<:KinematicWave}` for a single timestep `dt`. +Timestepping within `dt` is either with a fixed timestep `dt_fixed` or adaptive. """ function update_river_flow_model!( river_flow_model::RiverFlowModel{<:KinematicWave}, @@ -575,7 +575,7 @@ end """ Update boundary condition lateral inflow `inwater` of a kinematic wave overland flow model -`KinWaveOverlandFlow` for a single timestep. +`OverlandFlowModel{<:KinematicWave}` for a single timestep. """ function update_lateral_inflow!( overland_flow_model::OverlandFlowModel{<:KinematicWave}, diff --git a/Wflow/src/routing/surface_local_inertial.jl b/Wflow/src/routing/surface_local_inertial.jl index 5037aa0d2..088dd73aa 100644 --- a/Wflow/src/routing/surface_local_inertial.jl +++ b/Wflow/src/routing/surface_local_inertial.jl @@ -181,7 +181,7 @@ function RiverFlowStaggeredVariables( return variables end -"Initialize shallow water river flow model `LocalInertialRiverFlow`" +"Initialize shallow water river flow model `RiverFlowModel{<:LocalInertial}`" function LocalInertialRiverFlow( dataset::NCDataset, config::Config, @@ -228,7 +228,7 @@ function LocalInertialRiverFlow( return river_flow end -"Return the upstream inflow for a reservoir in `LocalInertialRiverFlow`" +"Return the upstream inflow for a reservoir in `RiverFlowModel{<:LocalInertial}`" function get_inflow_reservoir( river_flow_model::RiverFlowModel{<:LocalInertial}, src_edge::Vector{Int}, @@ -258,7 +258,7 @@ get_inflow_reservoir( subsurface_flow_model.variables.to_river[inds] ) ./ tosecond(BASETIMESTEP) -"Update local inertial river flow model `LocalInertialRiverFlow` for a single timestep" +"Update local inertial river flow model `RiverFlowModel{<:LocalInertial}` for a single timestep" function local_inertial_river_update!( river_flow_model::RiverFlowModel{<:LocalInertial}, domain::Domain, @@ -497,8 +497,8 @@ function local_inertial_river_update!( end """ -Update local inertial river flow model `LocalInertialRiverFlow` for a single timestep `dt`. An adaptive -timestepping method is used (computing a sub timestep `dt_s`). +Update local inertial river flow model `RiverFlowModel{<:LocalInertial}` for a single +timestep `dt`. An adaptive timestepping method is used (computing a sub timestep `dt_s`). """ function update_river_flow_model!( river_flow_model::RiverFlowModel{<:LocalInertial}, @@ -674,8 +674,8 @@ function LocalInertialOverlandFlow(dataset::NCDataset, config::Config, domain::D end """ - stable_timestep(river_flow_model::LocalInertialRiverFlow, flow_length::Vector{Float64}) - stable_timestep(overland_flow_model::LocalInertialOverlandFlow, parameters::LandParameters) + stable_timestep(river_flow_model::RiverFlowModel{<:LocalInertial}, flow_length::Vector{Float64}) + stable_timestep(overland_flow_model::OverlandFlowModel{<:LocalInertial}, parameters::LandParameters) Compute a stable timestep size for the local inertial approach, based on Bates et al. (2010). @@ -720,8 +720,8 @@ function stable_timestep( end """ -Update boundary condition `runoff` overland flow model `LocalInertialOverlandFlow` for a -single timestep. +Update boundary condition `runoff` overland flow model `OverlandFlowModel{<:LocalInertial}` +for a single timestep. """ function update_bc_overland_flow_model!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -744,7 +744,7 @@ end """ Update subsurface flow contribution to inflow of a reservoir model for a river flow model -`LocalInertialRiverFlow` for a single timestep. +`RiverFlowModel{<:LocalInertial}` for a single timestep. """ function update_inflow!( reservoir_model::Reservoir, @@ -766,9 +766,9 @@ update_inflow!( ) = nothing """ -Helper function to set flow variables of the `LocalInertialOverlandFlow` model to zero. This -is done at the start of each simulation timestep, during the timestep the total (weighted) -sum is computed from values at each sub timestep. +Helper function to set flow variables of the `OverlandFlowModel{<:LocalInertial}` model to +zero. This is done at the start of each simulation timestep, during the timestep the total +(weighted) sum is computed from values at each sub timestep. """ function set_flow_vars!(overland_flow_model::OverlandFlowModel{<:LocalInertial}) (; qx_av, qy_av) = overland_flow_model.variables @@ -778,8 +778,9 @@ function set_flow_vars!(overland_flow_model::OverlandFlowModel{<:LocalInertial}) end """ -Helper function to compute average flow variables of the `LocalInertialOverlandFlow` model. -This is done at the end of each simulation timestep. +Helper function to compute average flow variables of the +`OverlandFlowModel{<:LocalInertial}` model. This is done at the end of each simulation +timestep. """ function average_flow_vars!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -792,9 +793,9 @@ function average_flow_vars!( end """ -Update combined river `LocalInertialRiverFlow` and overland flow `LocalInertialOverlandFlow` -models for a single timestep `dt`. An adaptive timestepping method is used (computing a sub -timestep `dt_s`). +Update combined river `RiverFlowModel{<:LocalInertial}` and overland flow +`OverlandFlowModel{<:LocalInertial}` models for a single timestep `dt`. An adaptive +timestepping method is used (computing a sub timestep `dt_s`). """ function update_overland_flow_model!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -840,8 +841,8 @@ function update_overland_flow_model!( end """ -Update fluxes for overland flow `LocalInertialOverlandFlow` model for a single timestep -`dt`. +Update fluxes for overland flow `OverlandFlowModel{<:LocalInertial}` model for a single +timestep `dt`. """ function local_inertial_update_fluxes!( land::OverlandFlowModel{<:LocalInertial}, @@ -943,9 +944,8 @@ function local_inertial_update_fluxes!( end """ -Update boundary condition inflow to a reservoir from land `inflow_reservoir` of combined -river `LocalInertialRiverFlow`and overland flow `LocalInertialOverlandFlow` models for a -single timestep. +Update boundary condition inflow to a reservoir from land of combined local inertial river +and overland flow models for a single timestep. """ function update_inflow_reservoir!( land::OverlandFlowModel{<:LocalInertial}, @@ -968,8 +968,8 @@ function update_inflow_reservoir!( end """ -Update storage and water depth for combined river `LocalInertialRiverFlow`and overland flow -`LocalInertialOverlandFlow` models for a single timestep `dt`. +Update storage and water depth for combined river `RiverFlowModel{<:LocalInertial}`and +overland flow `OverlandFlowModel{<:LocalInertial}` models for a single timestep `dt`. """ function local_inertial_update_water_depth!( land::OverlandFlowModel{<:LocalInertial}, diff --git a/Wflow/src/routing/surface_routing.jl b/Wflow/src/routing/surface_routing.jl index 520bfcff4..2d2e9f78f 100644 --- a/Wflow/src/routing/surface_routing.jl +++ b/Wflow/src/routing/surface_routing.jl @@ -47,11 +47,17 @@ end """ surface_routing!( - model::Model{R} - ) where {R <: Routing{<:LocalInertialOverlandFlow, <:LocalInertialRiverFlow}} + model::Model{R}, +) where { + R <: Routing{ + <:OverlandFlowModel{<:LocalInertial}, + <:RiverFlowModel{<:LocalInertial}, + }, +} Run surface routing (land and river) for a model type that contains the routing components -`LocalInertialOverlandFlow` and `LocalInertialRiverFlow` for a single timestep. +`OverlandFlowModel{<:LocalInertial}` and `RiverFlowModel{<:LocalInertial}` for a single +timestep. """ function surface_routing!( model::Model{R}, From 7131e20f7f4dbba70ab9b70db3d9e891afe4b110 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 18 Mar 2026 13:29:58 +0100 Subject: [PATCH 08/90] Renaming of init functions surface flow routing Also add `slope` parameter that can be used by kinematic wave routing (as alternative to local inertial routing) using a staggered scheme. --- Wflow/src/routing/initialize_routing.jl | 8 ++++---- Wflow/src/routing/surface_kinwave.jl | 8 ++++---- Wflow/src/routing/surface_local_inertial.jl | 7 ++++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Wflow/src/routing/initialize_routing.jl b/Wflow/src/routing/initialize_routing.jl index 7d3be1a43..cdfa5d1b1 100644 --- a/Wflow/src/routing/initialize_routing.jl +++ b/Wflow/src/routing/initialize_routing.jl @@ -142,9 +142,9 @@ function initialize_overland_flow(dataset::NCDataset, config::Config, domain::Do (; land_routing) = config.model if land_routing == RoutingType.kinematic_wave - overland_flow = KinWaveOverlandFlow(dataset, config, domain.land) + overland_flow = init_kinematic_wave_overland_flow(dataset, config, domain.land) elseif land_routing == RoutingType.local_inertial - overland_flow = LocalInertialOverlandFlow(dataset, config, domain) + overland_flow = init_local_inertial_overland_flow(dataset, config, domain) end return overland_flow end @@ -161,9 +161,9 @@ function initialize_river_flow(dataset::NCDataset, config::Config, domain::Domai Reservoir(dataset, config, domain.reservoir.network) : nothing if river_routing == RoutingType.kinematic_wave - river_flow = KinWaveRiverFlow(dataset, config, domain.river, reservoir) + river_flow = init_kinematic_wave_river_flow(dataset, config, domain.river, reservoir) elseif river_routing == RoutingType.local_inertial - river_flow = LocalInertialRiverFlow(dataset, config, domain.river, reservoir) + river_flow = init_local_inertial_river_flow(dataset, config, domain.river, reservoir) end end diff --git a/Wflow/src/routing/surface_kinwave.jl b/Wflow/src/routing/surface_kinwave.jl index 4d3328967..cac89644e 100644 --- a/Wflow/src/routing/surface_kinwave.jl +++ b/Wflow/src/routing/surface_kinwave.jl @@ -107,8 +107,8 @@ function RiverFlowBC( return bc end -"Initialize river flow model `KinWaveRiverFlow`" -function KinWaveRiverFlow( +"Initialize kinematic wave river flow model" +function init_kinematic_wave_river_flow( dataset::NCDataset, config::Config, domain::DomainRiver, @@ -164,8 +164,8 @@ end inwater::Vector{Float64} = zeros(n) # Lateral inflow [m³ s⁻¹] end -"Initialize Overland flow model `KinWaveOverlandFlow`" -function KinWaveOverlandFlow(dataset::NCDataset, config::Config, domain::DomainLand) +"Initialize kinematic wave overland flow model" +function init_kinematic_wave_overland_flow(dataset::NCDataset, config::Config, domain::DomainLand) (; indices) = domain.network (; slope) = domain.parameters mannings_n = ncread( diff --git a/Wflow/src/routing/surface_local_inertial.jl b/Wflow/src/routing/surface_local_inertial.jl index 088dd73aa..831871dfe 100644 --- a/Wflow/src/routing/surface_local_inertial.jl +++ b/Wflow/src/routing/surface_local_inertial.jl @@ -12,6 +12,7 @@ bankfull_depth::Vector{Float64} = Float64[] # bankfull depth [m] mannings_n_sq::Vector{Float64} = Float64[] # Manning's roughness squared at edge [(s m-1/3)2] mannings_n::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] at node + slope::Vector{Float64} = Float64[] # slope at edge [-] flow_length_at_edge::Vector{Float64} = Float64[] # flow (river) length at edge [m] flow_width_at_edge::Vector{Float64} = Float64[] # flow (river) width at edge [m] end @@ -181,8 +182,8 @@ function RiverFlowStaggeredVariables( return variables end -"Initialize shallow water river flow model `RiverFlowModel{<:LocalInertial}`" -function LocalInertialRiverFlow( +"Initialize local inertial river flow model" +function init_local_inertial_river_flow( dataset::NCDataset, config::Config, domain::DomainRiver, @@ -652,7 +653,7 @@ end end "Initialize local inertial overland flow model" -function LocalInertialOverlandFlow(dataset::NCDataset, config::Config, domain::Domain) +function init_local_inertial_overland_flow(dataset::NCDataset, config::Config, domain::Domain) cfl = config.model.land_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) timestepping = TimeStepping(; cfl) From 832dbc532ac9747fcb508b0cf7f0a19cdc3b9cbe Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 19 Mar 2026 17:31:12 +0100 Subject: [PATCH 09/90] Refactor init local inertial river flow parameters Use a separate function for reading river parameters that can also be used by kinematic wave routing with a staggered scheme. --- Wflow/src/config_structure.jl | 2 +- Wflow/src/routing/initialize_routing.jl | 9 +- Wflow/src/routing/surface_local_inertial.jl | 104 +++++++++++++------- Wflow/test/routing_process.jl | 38 +++---- 4 files changed, 95 insertions(+), 58 deletions(-) diff --git a/Wflow/src/config_structure.jl b/Wflow/src/config_structure.jl index 0de87f8eb..4afd1dbdf 100644 --- a/Wflow/src/config_structure.jl +++ b/Wflow/src/config_structure.jl @@ -6,7 +6,7 @@ For configuration files we use TOML. =# # Option enumerators -@enumx RoutingType kinematic_wave local_inertial +@enumx RoutingType kinematic_wave kinematic_wave_staggered local_inertial @enumx ModelType sbm sbm_gwf sediment @enumx CalendarType standard gregorian proleptic_gregorian julian noleap _365_day all_leap _366_day _360_day @enumx GwfConductivityProfileType uniform exponential diff --git a/Wflow/src/routing/initialize_routing.jl b/Wflow/src/routing/initialize_routing.jl index cdfa5d1b1..d52a26afa 100644 --- a/Wflow/src/routing/initialize_routing.jl +++ b/Wflow/src/routing/initialize_routing.jl @@ -161,9 +161,14 @@ function initialize_river_flow(dataset::NCDataset, config::Config, domain::Domai Reservoir(dataset, config, domain.reservoir.network) : nothing if river_routing == RoutingType.kinematic_wave - river_flow = init_kinematic_wave_river_flow(dataset, config, domain.river, reservoir) + river_flow = + init_kinematic_wave_river_flow(dataset, config, domain.river, reservoir) elseif river_routing == RoutingType.local_inertial - river_flow = init_local_inertial_river_flow(dataset, config, domain.river, reservoir) + river_flow = + init_local_inertial_river_flow(dataset, config, domain.river, reservoir) + elseif river_routing == RoutingType.kinematic_wave_staggered + river_flow = + init_kinwave_staggered_river_flow(dataset, config, domain.river, reservoir) end end diff --git a/Wflow/src/routing/surface_local_inertial.jl b/Wflow/src/routing/surface_local_inertial.jl index 831871dfe..a4c16523f 100644 --- a/Wflow/src/routing/surface_local_inertial.jl +++ b/Wflow/src/routing/surface_local_inertial.jl @@ -1,14 +1,14 @@ "Struct for storing river flow model (numerical staggered scheme) parameters" @with_kw struct RiverFlowStaggeredParameters <: AbstractRiverFlowParameters n::Int # number of cells [-] - ne::Int # number of edges [-] + n_edges::Int # number of edges [-] active_n::Vector{Int} # active nodes [-] active_e::Vector{Int} # active edges [-] froude_limit::Bool = false # if true a check is performed if froude number > 1.0 (algorithm is modified) [-] h_thresh::Float64 = 0.0 # depth threshold for calculating flow [m] zb::Vector{Float64} = Float64[] # river bed elevation [m] zb_max::Vector{Float64} = Float64[] # maximum channel bed elevation [m] - bankfull_storage::Vector{Float64} # bankfull storage [m³] + bankfull_storage::Vector{Float64} = Float64[] # bankfull storage [m³] bankfull_depth::Vector{Float64} = Float64[] # bankfull depth [m] mannings_n_sq::Vector{Float64} = Float64[] # Manning's roughness squared at edge [(s m-1/3)2] mannings_n::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] at node @@ -17,30 +17,15 @@ flow_width_at_edge::Vector{Float64} = Float64[] # flow (river) width at edge [m] end -"Initialize local inertial river flow model parameters" -function RiverFlowStaggeredParameters( +function get_river_parameters( dataset::NCDataset, config::Config, domain::DomainRiver, + index_pit::Vector{Int}, ) - alpha = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) - waterdepth_threshold = config.model.river_water_flow_threshold__depth # depth threshold for flow at edge - froude_limit = config.model.river_water_flow__froude_limit_flag # limit flow to subcritical according to Froude number - floodplain_1d = config.model.floodplain_1d__flag + (; pit_indices, indices) = domain.network + (; flow_length, flow_width) = domain.parameters - @info "Local inertial approach is used for river flow." alpha waterdepth_threshold froude_limit floodplain_1d - - (; pit_indices, indices, graph, local_drain_direction, nodes_at_edge) = domain.network - (; flow_width, flow_length, reservoir_outlet) = domain.parameters - - riverlength_bc = ncread( - dataset, - config, - "model_boundary_condition_river__length"; - sel = pit_indices, - defaults = 1.0e04, - type = Float64, - ) bankfull_elevation_2d = ncread( dataset, config, @@ -57,10 +42,6 @@ function RiverFlowStaggeredParameters( type = Float64, fill = 0, ) - bankfull_depth = bankfull_depth_2d[indices] - zb = bankfull_elevation_2d[indices] - bankfull_depth # river bed elevation - - bankfull_storage = bankfull_depth .* flow_width .* flow_length mannings_n = ncread( dataset, config, @@ -69,19 +50,56 @@ function RiverFlowStaggeredParameters( defaults = 0.036, type = Float64, ) - - n = length(indices) - index_pit = findall(x -> x == 5, local_drain_direction) - # set ghost points for boundary condition (downstream river outlet): river width, bed - # elevation, manning n is copied from the upstream cell. + riverlength_bc = ncread( + dataset, + config, + "model_boundary_condition_river__length"; + sel = pit_indices, + defaults = 1.0e04, + type = Float64, + ) append!(flow_length, riverlength_bc) - append!(zb, zb[index_pit]) + + bankfull_depth = bankfull_depth_2d[indices] + bankfull_elevation = bankfull_elevation_2d[indices] + + # set ghost points for boundary condition (downstream river outlet): flow width, + # mannings n, bankfull depth and elevation is copied from the upstream cell. append!(flow_width, flow_width[index_pit]) append!(mannings_n, mannings_n[index_pit]) append!(bankfull_depth, bankfull_depth[index_pit]) + append!(bankfull_elevation, bankfull_elevation[index_pit]) - # determine z, width, length and manning's n at edges + return mannings_n, bankfull_depth, bankfull_elevation, flow_length, flow_width +end + +"Initialize local inertial river flow model parameters" +function init_local_inertial_river_flow_parameters( + dataset::NCDataset, + config::Config, + domain::DomainRiver, +) + alpha = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) + waterdepth_threshold = config.model.river_water_flow_threshold__depth # depth threshold for flow at edge + froude_limit = config.model.river_water_flow__froude_limit_flag # limit flow to subcritical according to Froude number + floodplain_1d = config.model.floodplain_1d__flag + + @info "Local inertial approach is used for river flow." alpha waterdepth_threshold froude_limit floodplain_1d + + (; graph, indices, local_drain_direction, nodes_at_edge) = domain.network + (; reservoir_outlet, flow_width) = domain.parameters + + n = length(indices) n_edges = ne(graph) + active_index = findall(x -> x == 0, reservoir_outlet) + index_pit = findall(x -> x == 5, local_drain_direction) + + mannings_n, bankfull_depth, bankfull_elevation, flow_length, flow_width = + get_river_parameters(dataset, config, domain, index_pit) + zb = bankfull_elevation - bankfull_depth # river bed elevation + bankfull_storage = bankfull_depth .* flow_width .* flow_length + + # determine z, width, length and manning's n at edges zb_max = fill(Float64(0), n_edges) width_at_edge = fill(Float64(0), n_edges) length_at_edge = fill(Float64(0), n_edges) @@ -99,11 +117,10 @@ function RiverFlowStaggeredParameters( ) / (flow_length[dst_node] + flow_length[src_node]) mannings_n_sq[i] = mannings_n_i * mannings_n_i end - active_index = findall(x -> x == 0, reservoir_outlet) parameters = RiverFlowStaggeredParameters(; n, - ne = n_edges, + n_edges, active_n = active_index, active_e = active_index, froude_limit, @@ -120,6 +137,17 @@ function RiverFlowStaggeredParameters( return parameters end +function init_kinwave_staggered_river_flow_parameters( + dataset::NCDataset, + config::Config, + domain::DomainRiver, +) + alpha = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) + floodplain_1d = config.model.floodplain_1d__flag + + @info "Local inertial approach is used for river flow." alpha floodplain_1d +end + "Struct for storing local inertial river flow model variables" @with_kw struct RiverFlowStaggeredVariables <: AbstractRiverFlowVariables n::Int @@ -204,7 +232,7 @@ function init_local_inertial_river_flow( cfl = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) timestepping = TimeStepping(; cfl) - parameters = RiverFlowStaggeredParameters(dataset, config, domain) + parameters = init_local_inertial_river_flow_parameters(dataset, config, domain) variables = RiverFlowStaggeredVariables(dataset, config, domain.network) boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir) @@ -653,7 +681,11 @@ end end "Initialize local inertial overland flow model" -function init_local_inertial_overland_flow(dataset::NCDataset, config::Config, domain::Domain) +function init_local_inertial_overland_flow( + dataset::NCDataset, + config::Config, + domain::Domain, +) cfl = config.model.land_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) timestepping = TimeStepping(; cfl) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 9e7b548a9..e93de04d5 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -301,14 +301,14 @@ end # for each edge the src and dst node is required nodes_at_edge = Wflow.adjacent_nodes_at_edge(graph) - _ne = ne(graph) + n_edges = ne(graph) # determine z, width, length and manning's n at edges - zb_max = fill(0.0, _ne) - width_at_edge = fill(0.0, _ne) - length_at_edge = fill(0.0, _ne) - mannings_n_sq = fill(0.0, _ne) - for i in 1:_ne + zb_max = fill(0.0, n_edges) + width_at_edge = fill(0.0, n_edges) + length_at_edge = fill(0.0, n_edges) + mannings_n_sq = fill(0.0, n_edges) + for i in 1:n_edges zb_max[i] = max(zb[nodes_at_edge.src[i]], zb[nodes_at_edge.dst[i]]) width_at_edge[i] = min(width[nodes_at_edge.dst[i]], width[nodes_at_edge.src[i]]) length_at_edge[i] = 0.5 * (dl[nodes_at_edge.dst[i]] + dl[nodes_at_edge.src[i]]) @@ -343,9 +343,9 @@ end timestepping = Wflow.TimeStepping(; cfl = 0.7) parameters = Wflow.RiverFlowStaggeredParameters(; n, - ne = _ne, + n_edges, active_n = collect(1:(n - 1)), - active_e = collect(1:_ne), + active_e = collect(1:n_edges), h_thresh, zb_max, mannings_n_sq, @@ -360,18 +360,18 @@ end variables = Wflow.RiverFlowStaggeredVariables(; n, - n_edges = _ne, - q0 = zeros(_ne), - q = zeros(_ne), - q_av = zeros(_ne), - q_channel_av = zeros(_ne), + n_edges, + q0 = zeros(n_edges), + q = zeros(n_edges), + q_av = zeros(n_edges), + q_channel_av = zeros(n_edges), h = h_init, - zs_max = zeros(_ne), - zs_src = zeros(_ne), - zs_dst = zeros(_ne), - hf = zeros(_ne), - a = zeros(_ne), - r = zeros(_ne), + zs_max = zeros(n_edges), + zs_src = zeros(n_edges), + zs_dst = zeros(n_edges), + hf = zeros(n_edges), + a = zeros(n_edges), + r = zeros(n_edges), storage = fill(0.0, n), error = zeros(n), ) From 7e85c4937e669cd77c26b0ff933c6d57f276540d Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 24 Mar 2026 11:29:17 +0100 Subject: [PATCH 10/90] Update info log message --- Wflow/src/routing/surface/surface_local_inertial.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index 02dd42d44..f0a8d24a3 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -145,7 +145,7 @@ function init_kinwave_staggered_river_flow_parameters( alpha = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) floodplain_1d = config.model.floodplain_1d__flag - @info "Local inertial approach is used for river flow." alpha floodplain_1d + @info "Kinematic wave on a staggered grid is used for river flow." alpha floodplain_1d end "Struct for storing local inertial river flow model variables" From c4bf8be47309c5ca0d3d78215a510a53a902fe4d Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 24 Mar 2026 13:46:01 +0100 Subject: [PATCH 11/90] Add init kinwave staggered river flow --- Wflow/src/domain.jl | 3 +- .../routing/surface/surface_local_inertial.jl | 103 +++++++++++++++++- 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/Wflow/src/domain.jl b/Wflow/src/domain.jl index f56618abc..9ceb401f9 100644 --- a/Wflow/src/domain.jl +++ b/Wflow/src/domain.jl @@ -107,7 +107,8 @@ function Domain(dataset::NCDataset, config::Config, ::Union{SbmModel, SbmGwfMode if river_routing == RoutingType.kinematic_wave @reset network_river.upstream_nodes = filter_upstream_nodes(network_river.graph, pits[network_river.indices]) - elseif river_routing == RoutingType.local_inertial + elseif river_routing == RoutingType.local_inertial || + river_routing == RoutingType.kinematic_wave_staggered nodes_at_edge, index_pit = NodesAtEdge(network_river) @reset network_river.nodes_at_edge = nodes_at_edge @reset network_river.pit_indices = network_river.indices[index_pit] diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index f0a8d24a3..bf34496e3 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -1,4 +1,4 @@ -"Struct for storing river flow model (numerical staggered scheme) parameters" +"Struct for storing river flow model parameters on a staggered grid" @with_kw struct RiverFlowStaggeredParameters <: AbstractRiverFlowParameters n::Int # number of cells [-] n_edges::Int # number of edges [-] @@ -87,7 +87,7 @@ function init_local_inertial_river_flow_parameters( @info "Local inertial approach is used for river flow." alpha waterdepth_threshold froude_limit floodplain_1d (; graph, indices, local_drain_direction, nodes_at_edge) = domain.network - (; reservoir_outlet, flow_width) = domain.parameters + (; reservoir_outlet) = domain.parameters n = length(indices) n_edges = ne(graph) @@ -146,6 +146,46 @@ function init_kinwave_staggered_river_flow_parameters( floodplain_1d = config.model.floodplain_1d__flag @info "Kinematic wave on a staggered grid is used for river flow." alpha floodplain_1d + + (; graph, indices, local_drain_direction, nodes_at_edge) = domain.network + (; reservoir_outlet) = domain.parameters + + n = length(indices) + n_edges = ne(graph) + active_index = findall(x -> x == 0, reservoir_outlet) + index_pit = findall(x -> x == 5, local_drain_direction) + + mannings_n, bankfull_depth, bankfull_elevation, flow_length, flow_width = + get_river_parameters(dataset, config, domain, index_pit) + zb = bankfull_elevation - bankfull_depth # river bed elevation + bankfull_storage = bankfull_depth .* flow_width .* flow_length + + # determine width, length and slope at edges + width_at_edge = fill(Float64(0), n_edges) + length_at_edge = fill(Float64(0), n_edges) + slope = fill(Float64(0), n_edges) + for i in 1:n_edges + src_node = nodes_at_edge.src[i] + dst_node = nodes_at_edge.dst[i] + width_at_edge[i] = min(flow_width[src_node], flow_width[dst_node]) + length_at_edge[i] = 0.5 * (flow_length[dst_node] + flow_length[src_node]) + slope[i] = min((zb[src_node] - zb[dst_node]) / length_at_edge[i], 0.00001) + end + + parameters = RiverFlowStaggeredParameters(; + n, + n_edges, + active_n = active_index, + active_e = active_index, + zb, + bankfull_storage, + bankfull_depth, + mannings_n, + slope, + flow_length_at_edge = length_at_edge, + flow_width_at_edge = width_at_edge, + ) + return parameters end "Struct for storing local inertial river flow model variables" @@ -167,8 +207,31 @@ end error::Vector{Float64} = zeros(n) # error storage [m³] end -"Initialize shallow water river flow model variables" -function RiverFlowStaggeredVariables( +function init_kinwave_staggered_river_flow_variables( + dataset::NCDataset, + config::Config, + network::NetworkRiver, +) + (; indices, graph) = network + + n = length(indices) + n_edges = ne(graph) + # set river depth h to zero (including reservoir locations) + h = zeros(n) + q_av = zeros(n_edges) + + variables = RiverFlowStaggeredVariables(; + n, + n_edges, + q_av, + q_channel_av = config.model.floodplain_1d__flag ? zeros(n_edges) : q_av, + h, + ) + return variables +end + +"Initialize local inertial river flow model variables" +function init_local_inertial_river_flow_variables( dataset::NCDataset, config::Config, network::NetworkRiver, @@ -233,7 +296,7 @@ function init_local_inertial_river_flow( timestepping = TimeStepping(; alpha_coefficient) parameters = init_local_inertial_river_flow_parameters(dataset, config, domain) - variables = RiverFlowStaggeredVariables(dataset, config, domain.network) + variables = init_local_inertial_river_flow_variables(dataset, config, domain.network) boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir_model) if config.model.floodplain_1d__flag @@ -258,6 +321,36 @@ function init_local_inertial_river_flow( return river_flow end +"Initialize kinematic wave river flow model on a staggered grid" +function init_kinwave_staggered_river_flow( + dataset::NCDataset, + config::Config, + domain::DomainRiver, + reservoir_model::Union{ReservoirModel, Nothing}, +) + alpha_coefficient = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) + + timestepping = TimeStepping(; alpha_coefficient) + parameters = init_kinwave_staggered_river_flow_parameters(dataset, config, domain) + variables = init_kinwave_staggered_river_flow_variables(dataset, config, domain.network) + boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir_model) + floodplain = nothing + routing_method = KinematicWaveStaggered() + + n = length(domain.network.indices) + river_flow = RiverFlowModel(; + timestepping, + boundary_conditions, + parameters, + variables, + floodplain, + allocation = do_water_demand(config) ? AllocationRiverModel(n) : + NoAllocationRiverModel(n), + routing_method, + ) + return river_flow +end + "Return the upstream inflow for a reservoir in `RiverFlowModel{<:LocalInertial}`" function get_inflow_reservoir( river_flow_model::RiverFlowModel{<:LocalInertial}, From ab7ab0cc47f2988c3faf64adcdfd7a0358f8b6cf Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 25 Mar 2026 10:46:48 +0100 Subject: [PATCH 12/90] Refactor init river flow staggered a bit --- .../routing/surface/surface_local_inertial.jl | 103 ++++++++++-------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index bf34496e3..b1e9c1a74 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -11,7 +11,7 @@ bankfull_storage::Vector{Float64} = Float64[] # bankfull storage [m³] bankfull_depth::Vector{Float64} = Float64[] # bankfull depth [m] mannings_n_sq::Vector{Float64} = Float64[] # Manning's roughness squared at edge [(s m-1/3)2] - mannings_n::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] at node + mannings_n::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] at edge slope::Vector{Float64} = Float64[] # slope at edge [-] flow_length_at_edge::Vector{Float64} = Float64[] # flow (river) length at edge [m] flow_width_at_edge::Vector{Float64} = Float64[] # flow (river) width at edge [m] @@ -73,6 +73,40 @@ function get_river_parameters( return mannings_n, bankfull_depth, bankfull_elevation, flow_length, flow_width end +function compute_value_at_edge(v, nodes_at_edge, n_edges, func::Function) + x = fill(Float64(0), n_edges) + for i in 1:n_edges + src_node = nodes_at_edge.src[i] + dst_node = nodes_at_edge.dst[i] + x[i] = func((v[src_node], v[dst_node])) + end + return x +end + +function compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) + mannings_n_at_edge = fill(Float64(0), n_edges) + for i in 1:n_edges + src_node = nodes_at_edge.src[i] + dst_node = nodes_at_edge.dst[i] + mannings_n_at_edge[i] = + ( + mannings_n[dst_node] * flow_length[dst_node] + + mannings_n[src_node] * flow_length[src_node] + ) / (flow_length[dst_node] + flow_length[src_node]) + end + return mannings_n_at_edge +end + +function compute_slope_at_edge(elev, length_at_edge, nodes_at_edge, n_edges) + slope = fill(Float64(0), n_edges) + for i in 1:n_edges + src_node = nodes_at_edge.src[i] + dst_node = nodes_at_edge.dst[i] + slope[i] = min((elev[src_node] - elev[dst_node]) / length_at_edge[i], 0.00001) + end + return slope +end + "Initialize local inertial river flow model parameters" function init_local_inertial_river_flow_parameters( dataset::NCDataset, @@ -100,23 +134,12 @@ function init_local_inertial_river_flow_parameters( bankfull_storage = bankfull_depth .* flow_width .* flow_length # determine z, width, length and manning's n at edges - zb_max = fill(Float64(0), n_edges) - width_at_edge = fill(Float64(0), n_edges) - length_at_edge = fill(Float64(0), n_edges) - mannings_n_sq = fill(Float64(0), n_edges) - for i in 1:n_edges - src_node = nodes_at_edge.src[i] - dst_node = nodes_at_edge.dst[i] - zb_max[i] = max(zb[src_node], zb[dst_node]) - width_at_edge[i] = min(flow_width[src_node], flow_width[dst_node]) - length_at_edge[i] = 0.5 * (flow_length[dst_node] + flow_length[src_node]) - mannings_n_i = - ( - mannings_n[dst_node] * flow_length[dst_node] + - mannings_n[src_node] * flow_length[src_node] - ) / (flow_length[dst_node] + flow_length[src_node]) - mannings_n_sq[i] = mannings_n_i * mannings_n_i - end + zb_max = compute_value_at_edge(zb, nodes_at_edge, n_edges, maximum) + flow_width_at_edge = compute_value_at_edge(flow_width, nodes_at_edge, n_edges, minimum) + flow_length_at_edge = compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) + mannings_n_at_edge = + compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) + mannings_n_sq = mannings_n_at_edge .* mannings_n_at_edge parameters = RiverFlowStaggeredParameters(; n, @@ -129,10 +152,9 @@ function init_local_inertial_river_flow_parameters( zb_max, bankfull_storage, bankfull_depth, - mannings_n, mannings_n_sq, - flow_length_at_edge = length_at_edge, - flow_width_at_edge = width_at_edge, + flow_length_at_edge, + flow_width_at_edge, ) return parameters end @@ -160,17 +182,13 @@ function init_kinwave_staggered_river_flow_parameters( zb = bankfull_elevation - bankfull_depth # river bed elevation bankfull_storage = bankfull_depth .* flow_width .* flow_length - # determine width, length and slope at edges - width_at_edge = fill(Float64(0), n_edges) - length_at_edge = fill(Float64(0), n_edges) - slope = fill(Float64(0), n_edges) - for i in 1:n_edges - src_node = nodes_at_edge.src[i] - dst_node = nodes_at_edge.dst[i] - width_at_edge[i] = min(flow_width[src_node], flow_width[dst_node]) - length_at_edge[i] = 0.5 * (flow_length[dst_node] + flow_length[src_node]) - slope[i] = min((zb[src_node] - zb[dst_node]) / length_at_edge[i], 0.00001) - end + # determine z, width, length and manning's n and slope at edges + zb_max = compute_value_at_edge(zb, nodes_at_edge, n_edges, maximum) + flow_width_at_edge = compute_value_at_edge(flow_width, nodes_at_edge, n_edges, minimum) + flow_length_at_edge = compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) + mannings_n_at_edge = + compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) + slope_at_edge = compute_slope_at_edge(zb, length_at_edge, nodes_at_edge, n_edges) parameters = RiverFlowStaggeredParameters(; n, @@ -178,12 +196,13 @@ function init_kinwave_staggered_river_flow_parameters( active_n = active_index, active_e = active_index, zb, + zb_max, bankfull_storage, bankfull_depth, - mannings_n, - slope, - flow_length_at_edge = length_at_edge, - flow_width_at_edge = width_at_edge, + mannings_n = mannings_n_at_edge, + slope = slope_at_edge, + flow_length_at_edge, + flow_width_at_edge, ) return parameters end @@ -197,9 +216,9 @@ end q_av::Vector{Float64} # average river channel (+ floodplain) discharge at edge [m³ s⁻¹] (model timestep Δt) q_channel_av::Vector{Float64} # average river channel discharge at edge [m³ s⁻¹] (for model timestep Δt) h::Vector{Float64} # water depth [m] - zs_max::Vector{Float64} = Float64[] # maximum water elevation at edge [m] - zs_src::Vector{Float64} = Float64[] # water elevation of source node of edge [m] - zs_dst::Vector{Float64} = Float64[] # water elevation of downstream node of edge [m] + zs_max::Vector{Float64} = zeros(n_edges) # maximum water elevation at edge [m] + zs_src::Vector{Float64} = zeros(n_edges) # water elevation of source node of edge [m] + zs_dst::Vector{Float64} = zeros(n_edges) # water elevation of downstream node of edge [m] hf::Vector{Float64} = zeros(n_edges) # water depth at edge [m] a::Vector{Float64} = zeros(n_edges) # flow area at edge [m²] r::Vector{Float64} = zeros(n_edges) # wetted perimeter at edge [m] @@ -253,9 +272,6 @@ function init_local_inertial_river_flow_variables( h = zeros(n) q_av = zeros(n_edges) q0 = zeros(n_edges) - zs_max = zeros(n_edges) - zs_src = zeros(n_edges) - zs_dst = zeros(n_edges) # set ghost points for boundary condition (downstream river outlet): river depth `h` append!(h, riverdepth_bc) @@ -266,9 +282,6 @@ function init_local_inertial_river_flow_variables( q0, q_channel_av = config.model.floodplain_1d__flag ? zeros(n_edges) : q_av, h, - zs_max, - zs_src, - zs_dst, ) return variables end From a58cccfff31ba7cc27d86f35fc3b58adfc541f33 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 25 Mar 2026 14:43:01 +0100 Subject: [PATCH 13/90] Use split up of local_inertial_river_update! function As implemented in PR#835. --- .../routing/surface/surface_local_inertial.jl | 417 +++++++++++------- 1 file changed, 260 insertions(+), 157 deletions(-) diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index b1e9c1a74..f46a4c2d4 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -394,19 +394,15 @@ get_inflow_reservoir( subsurface_flow_model.variables.to_river[inds] ) ./ tosecond(BASETIMESTEP) -"Update local inertial river flow model `RiverFlowModel{<:LocalInertial}` for a single timestep" -function local_inertial_river_update!( +""" +Update river channel flow for the local inertial river flow model. +""" +function update_river_channel_flow!( river_flow_model::RiverFlowModel{<:LocalInertial}, - domain::Domain, + domain::DomainRiver, dt::Float64, - dt_forcing::Float64, - update_h::Bool, ) - (; nodes_at_edge, edges_at_node) = domain.river.network - (; flow_length, flow_width) = domain.river.parameters - (; inwater, abstraction, external_inflow, actual_external_abstraction_av) = - river_flow_model.boundary_conditions - + (; nodes_at_edge) = domain.network river_v = river_flow_model.variables river_p = river_flow_model.parameters @@ -414,6 +410,7 @@ function local_inertial_river_update!( if !isnothing(river_flow_model.floodplain) river_flow_model.floodplain.variables.q0 .= river_flow_model.floodplain.variables.q end + @batch per = thread minbatch = 1000 for j in eachindex(river_p.active_e) i = river_p.active_e[j] i_src = nodes_at_edge.src[i] @@ -450,188 +447,294 @@ function local_inertial_river_update!( # average river discharge (here accumulated for model timestep Δt) river_v.q_av[i] += river_v.q[i] * dt end - if !isnothing(river_flow_model.floodplain) - floodplain_p = river_flow_model.floodplain.parameters - floodplain_v = river_flow_model.floodplain.variables + return nothing +end + +""" +Update floodplain flow for the local inertial river flow model. +""" +function update_floodplain_flow!( + river_flow_model::RiverFlowModel{T, P, V, F}, + domain::DomainRiver, + dt::Float64, +) where {T <: LocalInertial, P, V, F <: AbstractFloodPlainModel} + (; nodes_at_edge) = domain.network + (; flow_width) = domain.parameters - @batch per = thread minbatch = 1000 for i in 1:length(floodplain_v.hf) - floodplain_v.hf[i] = max(river_v.zs_max[i] - floodplain_p.zb_max[i], 0.0) + river_v = river_flow_model.variables + river_p = river_flow_model.parameters + floodplain_p = river_flow_model.floodplain.parameters + floodplain_v = river_flow_model.floodplain.variables + + @batch per = thread minbatch = 1000 for i in 1:length(floodplain_v.hf) + floodplain_v.hf[i] = max(river_v.zs_max[i] - floodplain_p.zb_max[i], 0.0) + end + + n = 0 + @inbounds for i in river_p.active_e + @inbounds if river_v.hf[i] > river_p.h_thresh + n += 1 + floodplain_v.hf_index[n] = i + else + floodplain_v.q[i] = 0.0 end + end - n = 0 - @inbounds for i in river_p.active_e - @inbounds if river_v.hf[i] > river_p.h_thresh - n += 1 - floodplain_v.hf_index[n] = i - else - floodplain_v.q[i] = 0.0 - end + get_area(i, i1, i2, idx) = flow_area( + floodplain_p.profile.width[i2, idx], + floodplain_p.profile.a[i1, idx], + floodplain_p.profile.depth[i1], + floodplain_v.hf[i], + ) + + get_wetted_perimeter(i, i1, idx) = wetted_perimeter( + floodplain_p.profile.p[i1, idx], + floodplain_p.profile.depth[i1], + floodplain_v.hf[i], + ) + + @batch per = thread minbatch = 1000 for j in 1:n + i = floodplain_v.hf_index[j] + i_src = nodes_at_edge.src[i] + i_dst = nodes_at_edge.dst[i] + + i0 = 0 + for k in eachindex(floodplain_p.profile.depth) + i0 += 1 * (floodplain_p.profile.depth[k] <= floodplain_v.hf[i]) end + i1 = max(i0, 1) + i2 = ifelse(i1 == length(floodplain_p.profile.depth), i1, i1 + 1) - @batch per = thread minbatch = 1000 for j in 1:n - i = floodplain_v.hf_index[j] - i_src = nodes_at_edge.src[i] - i_dst = nodes_at_edge.dst[i] + a_src = get_area(i, i1, i2, i_src) + a_src = max(a_src - (floodplain_v.hf[i] * flow_width[i_src]), 0.0) - i0 = 0 - for k in eachindex(floodplain_p.profile.depth) - i0 += 1 * (floodplain_p.profile.depth[k] <= floodplain_v.hf[i]) - end - i1 = max(i0, 1) - i2 = ifelse(i1 == length(floodplain_p.profile.depth), i1, i1 + 1) + a_dst = get_area(i, i1, i2, i_dst) + a_dst = max(a_dst - (floodplain_v.hf[i] * flow_width[i_dst]), 0.0) - a_src = flow_area( - floodplain_p.profile.width[i2, i_src], - floodplain_p.profile.a[i1, i_src], - floodplain_p.profile.depth[i1], - floodplain_v.hf[i], - ) - a_src = max(a_src - (floodplain_v.hf[i] * flow_width[i_src]), 0.0) + floodplain_v.a[i] = min(a_src, a_dst) + + floodplain_v.r[i] = if a_src < a_dst + a_src / get_wetted_perimeter(i, i1, i_src) + else + a_dst / get_wetted_perimeter(i, i1, i_dst) + end - a_dst = flow_area( - floodplain_p.profile.width[i2, i_dst], - floodplain_p.profile.a[i1, i_dst], - floodplain_p.profile.depth[i1], + floodplain_v.q[i] = if floodplain_v.a[i] > 1.0e-05 + local_inertial_flow( + floodplain_v.q0[i], + river_v.zs_src[i], + river_v.zs_dst[i], floodplain_v.hf[i], + floodplain_v.a[i], + floodplain_v.r[i], + river_p.flow_length_at_edge[i], + floodplain_p.mannings_n_sq[i], + river_p.froude_limit, + dt, ) - a_dst = max(a_dst - (floodplain_v.hf[i] * flow_width[i_dst]), 0.0) - - floodplain_v.a[i] = min(a_src, a_dst) - - floodplain_v.r[i] = ifelse( - a_src < a_dst, - a_src / wetted_perimeter( - floodplain_p.profile.p[i1, i_src], - floodplain_p.profile.depth[i1], - floodplain_v.hf[i], - ), - a_dst / wetted_perimeter( - floodplain_p.profile.p[i1, i_dst], - floodplain_p.profile.depth[i1], - floodplain_v.hf[i], - ), - ) + else + 0.0 + end - floodplain_v.q[i] = ifelse( - floodplain_v.a[i] > 1.0e-05, - local_inertial_flow( - floodplain_v.q0[i], - river_v.zs_src[i], - river_v.zs_dst[i], - floodplain_v.hf[i], - floodplain_v.a[i], - floodplain_v.r[i], - river_p.flow_length_at_edge[i], - floodplain_p.mannings_n_sq[i], - river_p.froude_limit, - dt, - ), - 0.0, - ) + # limit floodplain q in case water is not available + if floodplain_v.h[i_src] <= 0.0 + floodplain_v.q[i] = min(floodplain_v.q[i], 0.0) + end - # limit floodplain q in case water is not available - floodplain_v.q[i] = ifelse( - floodplain_v.h[i_src] <= 0.0, - min(floodplain_v.q[i], 0.0), - floodplain_v.q[i], - ) - floodplain_v.q[i] = ifelse( - floodplain_v.h[i_dst] <= 0.0, - max(floodplain_v.q[i], 0.0), - floodplain_v.q[i], - ) + if floodplain_v.h[i_dst] <= 0.0 + floodplain_v.q[i] = max(floodplain_v.q[i], 0.0) + end - floodplain_v.q[i] = - ifelse(floodplain_v.q[i] * river_v.q[i] < 0.0, 0.0, floodplain_v.q[i]) - # average floodplain discharge (here accumulated for model timestep Δt) - floodplain_v.q_av[i] += floodplain_v.q[i] * dt + if floodplain_v.q[i] * river_v.q[i] < 0.0 + floodplain_v.q[i] = 0.0 end + + # average floodplain discharge (here accumulated for model timestep Δt) + floodplain_v.q_av[i] += floodplain_v.q[i] * dt end - # For reservoir locations the local inertial solution is replaced by the reservoir - # model. These locations are handled as boundary conditions in the local inertial model - # (fixed h). - (; reservoir) = river_flow_model.boundary_conditions + return nothing +end + +update_floodplain_flow!( + model::RiverFlowModel{T, P, V, F}, + domain::DomainRiver, + dt::Float64, +) where {T <: LocalInertial, P, V, F <: Nothing} = nothing + +""" +Update reservoir boundary conditions for the local inertial river flow model. +""" +function update_bc_reservoir_model!( + reservoir_model::ReservoirModel, + river_flow_model::RiverFlowModel{<:LocalInertial}, + domain::Domain, + dt::Float64, + dt_forcing::Float64, +) + (; edges_at_node) = domain.river.network inds_reservoir = domain.reservoir.network.river_indices - if !isnothing(reservoir) - res_bc = reservoir.boundary_conditions - end + + river_v = river_flow_model.variables + res_bc = reservoir_model.boundary_conditions + for v in eachindex(inds_reservoir) i = inds_reservoir[v] q_in = get_inflow_reservoir(river_flow_model, edges_at_node.src[i]) # If external_inflow < 0, abstraction is limited if res_bc.external_inflow[v] < 0.0 - _abstraction = min( + abstraction = min( -res_bc.external_inflow[v], - (reservoir.variables.storage[v] / dt) * 0.98, + (reservoir_model.variables.storage[v] / dt) * 0.98, ) - res_bc.actual_external_abstraction_av[v] += _abstraction * dt - _inflow = -_abstraction + res_bc.actual_external_abstraction_av[v] += abstraction * dt + inflow = -abstraction else - _inflow = res_bc.external_inflow[v] + inflow = res_bc.external_inflow[v] end - net_inflow = - q_in + res_bc.inflow_overland[v] + res_bc.inflow_subsurface[v] + _inflow - update_reservoir_model!(reservoir, v, net_inflow, dt, dt_forcing) - river_v.q[i] = reservoir.variables.outflow[v] + net_inflow = q_in + res_bc.inflow_overland[v] + res_bc.inflow_subsurface[v] + inflow + update_reservoir_model!(reservoir_model, v, net_inflow, dt, dt_forcing) + river_v.q[i] = reservoir_model.variables.outflow[v] # average river discharge (here accumulated for model timestep Δt) river_v.q_av[i] += river_v.q[i] * dt end - if update_h - @batch per = thread minbatch = 1000 for i in river_p.active_n - q_src = sum_at(river_v.q, edges_at_node.src[i]) - q_dst = sum_at(river_v.q, edges_at_node.dst[i]) - # internal abstraction (water demand) is limited by river storage and negative - # external inflow as part of water allocation computations. - river_v.storage[i] = - river_v.storage[i] + (q_src - q_dst + inwater[i] - abstraction[i]) * dt + return nothing +end +update_bc_reservoir_model!( + reservoir_model::Nothing, + river_flow_model::RiverFlowModel{<:LocalInertial}, + domain::Domain, + dt::Float64, + dt_forcing::Float64, +) = nothing - if river_v.storage[i] < 0.0 - river_v.error[i] = river_v.error[i] + abs(river_v.storage[i]) - river_v.storage[i] = 0.0 # set storage to zero - end - # limit negative external inflow - if external_inflow[i] < 0.0 - _abstraction = min(-external_inflow[i], river_v.storage[i] / dt * 0.80) - actual_external_abstraction_av[i] += _abstraction * dt - _inflow = -_abstraction - else - _inflow = external_inflow[i] - end - river_v.storage[i] += _inflow * dt # add external inflow - river_v.h[i] = river_v.storage[i] / (flow_length[i] * flow_width[i]) - - if !isnothing(river_flow_model.floodplain) - floodplain_v = river_flow_model.floodplain.variables - floodplain_p = river_flow_model.floodplain.parameters - q_src = sum_at(floodplain_v.q, edges_at_node.src[i]) - q_dst = sum_at(floodplain_v.q, edges_at_node.dst[i]) - floodplain_v.storage[i] = floodplain_v.storage[i] + (q_src - q_dst) * dt - if floodplain_v.storage[i] < 0.0 - floodplain_v.error[i] = - floodplain_v.error[i] + abs(floodplain_v.storage[i]) - floodplain_v.storage[i] = 0.0 - end - storage_total = river_v.storage[i] + floodplain_v.storage[i] - if storage_total > river_p.bankfull_storage[i] - flood_storage = storage_total - river_p.bankfull_storage[i] - h = flood_depth(floodplain_p.profile, flood_storage, flow_length[i], i) - river_v.h[i] = river_p.bankfull_depth[i] + h - river_v.storage[i] = river_v.h[i] * flow_width[i] * flow_length[i] - floodplain_v.storage[i] = max(storage_total - river_v.storage[i], 0.0) - floodplain_v.h[i] = floodplain_v.storage[i] > 0.0 ? h : 0.0 - else - river_v.h[i] = storage_total / (flow_length[i] * flow_width[i]) - river_v.storage[i] = storage_total - floodplain_v.h[i] = 0.0 - floodplain_v.storage[i] = 0.0 - end - end +""" +Update floodplain water depth and storage. +""" +function update_water_depth_and_storage!( + floodplain_model::AbstractFloodPlainModel, + river_flow_model::RiverFlowModel{<:LocalInertial}, + domain::DomainRiver, + dt::Float64, +) + (; edges_at_node) = domain.network + (; flow_length, flow_width) = domain.parameters + + river_v = river_flow_model.variables + river_p = river_flow_model.parameters + floodplain_v = floodplain_model.variables + floodplain_p = floodplain_model.parameters + + @batch per = thread minbatch = 1000 for i in river_p.active_n + q_src = sum_at(floodplain_v.q, edges_at_node.src[i]) + q_dst = sum_at(floodplain_v.q, edges_at_node.dst[i]) + floodplain_v.storage[i] = floodplain_v.storage[i] + (q_src - q_dst) * dt + if floodplain_v.storage[i] < 0.0 + floodplain_v.error[i] += abs(floodplain_v.storage[i]) + floodplain_v.storage[i] = 0.0 + end + storage_total = river_v.storage[i] + floodplain_v.storage[i] + if storage_total > river_p.bankfull_storage[i] + flood_storage = storage_total - river_p.bankfull_storage[i] + h = flood_depth(floodplain_p.profile, flood_storage, flow_length[i], i) + river_v.h[i] = river_p.bankfull_depth[i] + h + river_v.storage[i] = river_v.h[i] * flow_width[i] * flow_length[i] + floodplain_v.storage[i] = max(storage_total - river_v.storage[i], 0.0) + floodplain_v.h[i] = floodplain_v.storage[i] > 0.0 ? h : 0.0 + else + river_v.h[i] = storage_total / (flow_length[i] * flow_width[i]) + river_v.storage[i] = storage_total + floodplain_v.h[i] = 0.0 + floodplain_v.storage[i] = 0.0 end end return nothing end +""" +Update floodplain water depth and storage (no-op for Nothing floodplain). +""" +update_water_depth_and_storage!( + ::Nothing, + ::RiverFlowModel{<:LocalInertial}, + ::DomainRiver, + ::Float64, +) = nothing + +""" +Update water depth and storage for river. +""" +function update_water_depth_and_storage!( + model::RiverFlowModel{<:LocalInertial}, + domain::DomainRiver, + dt::Float64, +) + (; edges_at_node) = domain.network + (; flow_length, flow_width) = domain.parameters + (; inwater, abstraction, external_inflow, actual_external_abstraction_av) = + model.boundary_conditions + + river_v = model.variables + river_p = model.parameters + + @batch per = thread minbatch = 1000 for i in river_p.active_n + q_src = sum_at(river_v.q, edges_at_node.src[i]) + q_dst = sum_at(river_v.q, edges_at_node.dst[i]) + # internal abstraction (water demand) is limited by river storage and negative + # external inflow as part of water allocation computations. + river_v.storage[i] = + river_v.storage[i] + (q_src - q_dst + inwater[i] - abstraction[i]) * dt + + if river_v.storage[i] < 0.0 + river_v.error[i] = river_v.error[i] + abs(river_v.storage[i]) + river_v.storage[i] = 0.0 # set storage to zero + end + # limit negative external inflow + if external_inflow[i] < 0.0 + _abstraction = min(-external_inflow[i], river_v.storage[i] / dt * 0.80) + actual_external_abstraction_av[i] += _abstraction * dt + _inflow = -_abstraction + else + _inflow = external_inflow[i] + end + river_v.storage[i] += _inflow * dt # add external inflow + river_v.h[i] = river_v.storage[i] / (flow_length[i] * flow_width[i]) + end + return nothing +end + +"Update local inertial river flow model `RiverFlowModel{<:LocalInertial}` for a single timestep" +function local_inertial_river_update!( + model::RiverFlowModel{<:LocalInertial}, + domain::Domain, + dt::Float64, + dt_forcing::Float64, + update_h::Bool, +) + # Update river channel flow + update_river_channel_flow!(model, domain.river, dt) + + # Update floodplain flow if present + update_floodplain_flow!(model, domain.river, dt) + + # Handle reservoir boundary conditions + update_bc_reservoir_model!( + model.boundary_conditions.reservoir, + model, + domain, + dt, + dt_forcing, + ) + + # Update water depth and storage if requested + if update_h + update_water_depth_and_storage!(model, domain.river, dt) + update_water_depth_and_storage!(model.floodplain, model, domain.river, dt) + end + + return nothing +end + """ Update local inertial river flow model `RiverFlowModel{<:LocalInertial}` for a single timestep `dt`. An adaptive timestepping method is used (computing a sub timestep `dt_s`). From d7b1e7eee9d26c67c4fad1bf7dc7a5ac57fab91d Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 25 Mar 2026 15:02:19 +0100 Subject: [PATCH 14/90] Init parameter `h_thresh` staggered kinematic wave --- Wflow/src/routing/surface/surface_local_inertial.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index f46a4c2d4..3c93d0adc 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -165,6 +165,7 @@ function init_kinwave_staggered_river_flow_parameters( domain::DomainRiver, ) alpha = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) + waterdepth_threshold = config.model.river_water_flow_threshold__depth # depth threshold for flow at edge floodplain_1d = config.model.floodplain_1d__flag @info "Kinematic wave on a staggered grid is used for river flow." alpha floodplain_1d @@ -195,6 +196,7 @@ function init_kinwave_staggered_river_flow_parameters( n_edges, active_n = active_index, active_e = active_index, + h_thresh = waterdepth_threshold, zb, zb_max, bankfull_storage, From ac0f1f8565a3498741b0d14632cf8c955622089a Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 26 Mar 2026 10:09:47 +0100 Subject: [PATCH 15/90] Fix typo --- Wflow/src/routing/surface/surface_local_inertial.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index 3c93d0adc..b24bbc31c 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -189,7 +189,7 @@ function init_kinwave_staggered_river_flow_parameters( flow_length_at_edge = compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) mannings_n_at_edge = compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) - slope_at_edge = compute_slope_at_edge(zb, length_at_edge, nodes_at_edge, n_edges) + slope_at_edge = compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) parameters = RiverFlowStaggeredParameters(; n, From 54cd40d7e4fc4a13b5c1466019b5dd9e57a3793b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 26 Mar 2026 11:17:33 +0100 Subject: [PATCH 16/90] Fix slope computation --- Wflow/src/routing/surface/surface_local_inertial.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index b24bbc31c..438869b65 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -102,7 +102,7 @@ function compute_slope_at_edge(elev, length_at_edge, nodes_at_edge, n_edges) for i in 1:n_edges src_node = nodes_at_edge.src[i] dst_node = nodes_at_edge.dst[i] - slope[i] = min((elev[src_node] - elev[dst_node]) / length_at_edge[i], 0.00001) + slope[i] = max((elev[src_node] - elev[dst_node]) / length_at_edge[i], 0.00001) end return slope end From 88a918bee635fdc4673b06b2a3fbd20da07c1cd6 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 26 Mar 2026 15:25:44 +0100 Subject: [PATCH 17/90] Add update manning's river flow staggered grid Using the term kinematic wave is not entirely correct as the Manning's equation is used as an alternative for the local inertial approach on a staggered grid. --- Wflow/src/routing/routing.jl | 6 +- .../routing/surface/surface_local_inertial.jl | 248 ++++++++++++++---- Wflow/src/routing/surface/surface_process.jl | 5 + Wflow/test/routing_process.jl | 2 +- 4 files changed, 203 insertions(+), 58 deletions(-) diff --git a/Wflow/src/routing/routing.jl b/Wflow/src/routing/routing.jl index fe4da1591..2cdaeda59 100644 --- a/Wflow/src/routing/routing.jl +++ b/Wflow/src/routing/routing.jl @@ -3,10 +3,12 @@ abstract type AbstractOverlandFlowModel end abstract type AbstractSubsurfaceFlowModel end abstract type AbstractRoutingMethod end +abstract type AbstractStaggeredRoutingMethod <: AbstractRoutingMethod end + struct AccucapacityFlux <: AbstractRoutingMethod end struct KinematicWave <: AbstractRoutingMethod end -struct KinematicWaveStaggered <: AbstractRoutingMethod end -struct LocalInertial <: AbstractRoutingMethod end +struct ManningStaggered <: AbstractStaggeredRoutingMethod end +struct LocalInertial <: AbstractStaggeredRoutingMethod end struct NoSubsurfaceFlow <: AbstractSubsurfaceFlowModel end struct NoOverlandFlow <: AbstractOverlandFlowModel end diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index 438869b65..9172e4900 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -159,7 +159,7 @@ function init_local_inertial_river_flow_parameters( return parameters end -function init_kinwave_staggered_river_flow_parameters( +function init_manning_staggered_river_flow_parameters( dataset::NCDataset, config::Config, domain::DomainRiver, @@ -168,7 +168,7 @@ function init_kinwave_staggered_river_flow_parameters( waterdepth_threshold = config.model.river_water_flow_threshold__depth # depth threshold for flow at edge floodplain_1d = config.model.floodplain_1d__flag - @info "Kinematic wave on a staggered grid is used for river flow." alpha floodplain_1d + @info "Manning's equation on a staggered grid is used for river flow." alpha floodplain_1d (; graph, indices, local_drain_direction, nodes_at_edge) = domain.network (; reservoir_outlet) = domain.parameters @@ -228,18 +228,33 @@ end error::Vector{Float64} = zeros(n) # error storage [m³] end -function init_kinwave_staggered_river_flow_variables( +function get_river_depth_bc(dataset, config, indices) + riverdepth_bc = ncread( + dataset, + config, + "model_boundary_condition_river_bank_water__depth"; + sel = indices, + defaults = 0.0, + type = Float64, + ) + return riverdepth_bc +end + +function init_manning_staggered_river_flow_variables( dataset::NCDataset, config::Config, network::NetworkRiver, ) - (; indices, graph) = network + (; pit_indices, indices, graph) = network + riverdepth_bc = get_river_depth_bc(dataset, config, pit_indices) n = length(indices) n_edges = ne(graph) # set river depth h to zero (including reservoir locations) h = zeros(n) q_av = zeros(n_edges) + # set ghost points for boundary condition (downstream river outlet): river depth `h` + append!(h, riverdepth_bc) variables = RiverFlowStaggeredVariables(; n, @@ -259,15 +274,7 @@ function init_local_inertial_river_flow_variables( ) (; pit_indices, indices, graph) = network - riverdepth_bc = ncread( - dataset, - config, - "model_boundary_condition_river_bank_water__depth"; - sel = pit_indices, - defaults = 0.0, - type = Float64, - ) - + riverdepth_bc = get_river_depth_bc(dataset, config, pit_indices) n = length(indices) n_edges = ne(graph) # set river depth h to zero (including reservoir locations) @@ -336,8 +343,8 @@ function init_local_inertial_river_flow( return river_flow end -"Initialize kinematic wave river flow model on a staggered grid" -function init_kinwave_staggered_river_flow( +"Initialize river manning's flow model on a staggered grid" +function init_manning_staggered_river_flow( dataset::NCDataset, config::Config, domain::DomainRiver, @@ -346,11 +353,11 @@ function init_kinwave_staggered_river_flow( alpha_coefficient = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) timestepping = TimeStepping(; alpha_coefficient) - parameters = init_kinwave_staggered_river_flow_parameters(dataset, config, domain) - variables = init_kinwave_staggered_river_flow_variables(dataset, config, domain.network) + parameters = init_manning_staggered_river_flow_parameters(dataset, config, domain) + variables = init_manning_staggered_river_flow_variables(dataset, config, domain.network) boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir_model) floodplain = nothing - routing_method = KinematicWaveStaggered() + routing_method = ManningStaggered() n = length(domain.network.indices) river_flow = RiverFlowModel(; @@ -366,9 +373,11 @@ function init_kinwave_staggered_river_flow( return river_flow end -"Return the upstream inflow for a reservoir in `RiverFlowModel{<:LocalInertial}`" +""" +Return the upstream inflow for a reservoir in a river flow model on a staggered grid. +""" function get_inflow_reservoir( - river_flow_model::RiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, src_edge::Vector{Int}, ) q_in = sum_at(river_flow_model.variables.q, src_edge) @@ -381,13 +390,13 @@ end # For local inertial river routing, `to_river` is included, as reservoir cells are excluded # (boundary condition). get_inflow_reservoir( - ::RiverFlowModel{<:LocalInertial}, + ::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, overland_flow_model::OverlandFlowModel{<:KinematicWave}, inds::Vector{Int}, ) = overland_flow_model.variables.q_av[inds] .+ overland_flow_model.variables.to_river[inds] get_inflow_reservoir( - ::RiverFlowModel{<:LocalInertial}, + ::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, subsurface_flow_model::LateralSSFModel, inds::Vector{Int}, ) = @@ -452,6 +461,50 @@ function update_river_channel_flow!( return nothing end +""" +Update river channel flow using Manning's equation on a staggered grid. +""" +function update_river_channel_flow!( + river_flow_model::RiverFlowModel{<:ManningStaggered}, + domain::DomainRiver, + dt::Float64, +) + (; nodes_at_edge) = domain.network + river_v = river_flow_model.variables + river_p = river_flow_model.parameters + + @batch per = thread minbatch = 1000 for j in eachindex(river_p.active_e) + i = river_p.active_e[j] + i_src = nodes_at_edge.src[i] + i_dst = nodes_at_edge.dst[i] + river_v.zs_src[i] = river_p.zb[i_src] + river_v.h[i_src] + river_v.zs_dst[i] = river_p.zb[i_dst] + river_v.h[i_dst] + + river_v.zs_max[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) + river_v.hf[i] = (river_v.zs_max[i] - river_p.zb_max[i]) + + river_v.a[i] = river_p.flow_width_at_edge[i] * river_v.hf[i] # flow area (rectangular channel) + river_v.r[i] = river_v.a[i] / (river_p.flow_width_at_edge[i] + 2.0 * river_v.hf[i]) # hydraulic radius (rectangular channel) + + river_v.q[i] = ifelse( + river_v.hf[i] > river_p.h_thresh, + manning_flow( + river_p.mannings_n[i], + river_v.r[i], + river_p.slope[i], + river_v.a[i], + ), + 0.0, + ) + + # limit q in case water is not available + river_v.q[i] = min(river_v.q[i], river_v.storage[i_src]/dt) + # average river discharge (here accumulated for model timestep Δt) + river_v.q_av[i] += river_v.q[i] * dt + end + return nothing +end + """ Update floodplain flow for the local inertial river flow model. """ @@ -557,18 +610,107 @@ function update_floodplain_flow!( return nothing end +""" +Update floodplain flow for the manning river flow model on a staggered grid. +""" +function update_floodplain_flow!( + river_flow_model::RiverFlowModel{T, P, V, F}, + domain::DomainRiver, + dt::Float64, +) where {T <: ManningStaggered, P, V, F <: AbstractFloodPlainModel} + (; nodes_at_edge) = domain.network + (; flow_width) = domain.parameters + + river_v = river_flow_model.variables + river_p = river_flow_model.parameters + floodplain_p = river_flow_model.floodplain.parameters + floodplain_v = river_flow_model.floodplain.variables + + @batch per = thread minbatch = 1000 for i in 1:length(floodplain_v.hf) + floodplain_v.hf[i] = max(river_v.zs_max[i] - floodplain_p.zb_max[i], 0.0) + end + + n = 0 + @inbounds for i in river_p.active_e + @inbounds if river_v.hf[i] > river_p.h_thresh + n += 1 + floodplain_v.hf_index[n] = i + else + floodplain_v.q[i] = 0.0 + end + end + + get_area(i, i1, i2, idx) = flow_area( + floodplain_p.profile.width[i2, idx], + floodplain_p.profile.a[i1, idx], + floodplain_p.profile.depth[i1], + floodplain_v.hf[i], + ) + + get_wetted_perimeter(i, i1, idx) = wetted_perimeter( + floodplain_p.profile.p[i1, idx], + floodplain_p.profile.depth[i1], + floodplain_v.hf[i], + ) + + @batch per = thread minbatch = 1000 for j in 1:n + i = floodplain_v.hf_index[j] + i_src = nodes_at_edge.src[i] + i_dst = nodes_at_edge.dst[i] + + i0 = 0 + for k in eachindex(floodplain_p.profile.depth) + i0 += 1 * (floodplain_p.profile.depth[k] <= floodplain_v.hf[i]) + end + i1 = max(i0, 1) + i2 = ifelse(i1 == length(floodplain_p.profile.depth), i1, i1 + 1) + + a_src = get_area(i, i1, i2, i_src) + a_src = max(a_src - (floodplain_v.hf[i] * flow_width[i_src]), 0.0) + + a_dst = get_area(i, i1, i2, i_dst) + a_dst = max(a_dst - (floodplain_v.hf[i] * flow_width[i_dst]), 0.0) + + floodplain_v.a[i] = min(a_src, a_dst) + + floodplain_v.r[i] = if a_src < a_dst + a_src / get_wetted_perimeter(i, i1, i_src) + else + a_dst / get_wetted_perimeter(i, i1, i_dst) + end + + floodplain_v.q[i] = if floodplain_v.a[i] > 1.0e-05 + mannings_flow( + floodplain_p.mannings_n[i], + floodplain_v.r[i], + floodplain_p.slope[i], + floodplain_v.a[i], + ) + else + 0.0 + end + + # limit floodplain q in case water is not available + floodplain_v.q[i] = min(floodplain_v.q[i], floodplain_v.storage[i_src]/dt) + + # average floodplain discharge (here accumulated for model timestep Δt) + floodplain_v.q_av[i] += floodplain_v.q[i] * dt + end + return nothing +end + update_floodplain_flow!( model::RiverFlowModel{T, P, V, F}, domain::DomainRiver, dt::Float64, -) where {T <: LocalInertial, P, V, F <: Nothing} = nothing +) where {T <: AbstractStaggeredRoutingMethod, P, V, F <: Nothing} = nothing """ -Update reservoir boundary conditions for the local inertial river flow model. +Update reservoir boundary conditions for a river flow model on a staggered grid. """ function update_bc_reservoir_model!( reservoir_model::ReservoirModel, - river_flow_model::RiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, domain::Domain, dt::Float64, dt_forcing::Float64, @@ -604,7 +746,7 @@ function update_bc_reservoir_model!( end update_bc_reservoir_model!( reservoir_model::Nothing, - river_flow_model::RiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, domain::Domain, dt::Float64, dt_forcing::Float64, @@ -615,7 +757,7 @@ Update floodplain water depth and storage. """ function update_water_depth_and_storage!( floodplain_model::AbstractFloodPlainModel, - river_flow_model::RiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, domain::DomainRiver, dt::Float64, ) @@ -658,7 +800,7 @@ Update floodplain water depth and storage (no-op for Nothing floodplain). """ update_water_depth_and_storage!( ::Nothing, - ::RiverFlowModel{<:LocalInertial}, + ::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, ::DomainRiver, ::Float64, ) = nothing @@ -667,7 +809,7 @@ update_water_depth_and_storage!( Update water depth and storage for river. """ function update_water_depth_and_storage!( - model::RiverFlowModel{<:LocalInertial}, + model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, domain::DomainRiver, dt::Float64, ) @@ -705,9 +847,9 @@ function update_water_depth_and_storage!( return nothing end -"Update local inertial river flow model `RiverFlowModel{<:LocalInertial}` for a single timestep" -function local_inertial_river_update!( - model::RiverFlowModel{<:LocalInertial}, +"Update river flow model on a staggered grid for a single timestep" +function staggered_scheme_river_update!( + model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, domain::Domain, dt::Float64, dt_forcing::Float64, @@ -738,11 +880,11 @@ function local_inertial_river_update!( end """ -Update local inertial river flow model `RiverFlowModel{<:LocalInertial}` for a single -timestep `dt`. An adaptive timestepping method is used (computing a sub timestep `dt_s`). +Update river flow model on a staggered grid for a single timestep `dt`. An adaptive +timestepping method is used (computing a sub timestep `dt_s`). """ function update_river_flow_model!( - river_flow_model::RiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, domain::Domain, clock::Clock; update_h = true, @@ -763,7 +905,7 @@ function update_river_flow_model!( while t < dt dt_s = stable_timestep(river_flow_model, flow_length) dt_s = check_timestepsize(dt_s, t, dt) - local_inertial_river_update!(river_flow_model, domain, dt_s, dt, update_h) + staggered_scheme_river_update!(river_flow_model, domain, dt_s, dt, update_h) t += dt_s end average_flow_vars!(river_flow_model, dt) @@ -927,7 +1069,7 @@ Compute a stable timestep size for the local inertial approach, based on Bates e dt = α * (Δx / sqrt(g max(h)) """ function stable_timestep( - river_flow_model::RiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, flow_length::Vector{Float64}, ) dt_min = Inf @@ -966,8 +1108,7 @@ function stable_timestep( end """ -Update boundary condition `runoff` overland flow model `OverlandFlowModel{<:LocalInertial}` -for a single timestep. +Update boundary condition `runoff` local inertial overland flow model for a single timestep. """ function update_bc_overland_flow_model!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -989,12 +1130,12 @@ function update_bc_overland_flow_model!( end """ -Update subsurface flow contribution to inflow of a reservoir model for a river flow model -`RiverFlowModel{<:LocalInertial}` for a single timestep. +Update subsurface flow contribution to inflow of a reservoir model for a river flow model on +a staggered grid for a single timestep. """ function update_inflow!( reservoir_model::ReservoirModel, - river_flow_model::RiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, subsurface_flow_model::AbstractSubsurfaceFlowModel, network::NetworkReservoir, ) @@ -1012,8 +1153,8 @@ update_inflow!( ) = nothing """ -Helper function to set flow variables of the `OverlandFlowModel{<:LocalInertial}` model to -zero. This is done at the start of each simulation timestep, during the timestep the total +Helper function to set flow variables of the local inertial overland flow model to zero. +This is done at the start of each simulation timestep, during the timestep the total (weighted) sum is computed from values at each sub timestep. """ function set_flow_vars!(overland_flow_model::OverlandFlowModel{<:LocalInertial}) @@ -1024,9 +1165,8 @@ function set_flow_vars!(overland_flow_model::OverlandFlowModel{<:LocalInertial}) end """ -Helper function to compute average flow variables of the -`OverlandFlowModel{<:LocalInertial}` model. This is done at the end of each simulation -timestep. +Helper function to compute average flow variables of the local inertial overland flow model. +This is done at the end of each simulation timestep. """ function average_flow_vars!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -1039,9 +1179,8 @@ function average_flow_vars!( end """ -Update combined river `RiverFlowModel{<:LocalInertial}` and overland flow -`OverlandFlowModel{<:LocalInertial}` models for a single timestep `dt`. An adaptive -timestepping method is used (computing a sub timestep `dt_s`). +Update combined local inertial river and overland flow models for a single timestep `dt`. An +adaptive timestepping method is used (computing a sub timestep `dt_s`). """ function update_overland_flow_model!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -1069,7 +1208,7 @@ function update_overland_flow_model!( local_inertial_update_fluxes!(overland_flow_model, domain, dt_s) update_inflow_reservoir!(overland_flow_model, reservoir, domain) - local_inertial_river_update!(river_flow_model, domain, dt_s, dt, update_h) + staggered_scheme_river_update!(river_flow_model, domain, dt_s, dt, update_h) local_inertial_update_water_depth!( overland_flow_model, river_flow_model, @@ -1087,8 +1226,7 @@ function update_overland_flow_model!( end """ -Update fluxes for overland flow `OverlandFlowModel{<:LocalInertial}` model for a single -timestep `dt`. +Update fluxes for local inertial overland flow model for a single timestep `dt`. """ function local_inertial_update_fluxes!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -1214,8 +1352,8 @@ function update_inflow_reservoir!( end """ -Update storage and water depth for combined river `RiverFlowModel{<:LocalInertial}`and -overland flow `OverlandFlowModel{<:LocalInertial}` models for a single timestep `dt`. +Update storage and water depth for combined local inertial river and overland flow models +for a single timestep `dt`. """ function local_inertial_update_water_depth!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, diff --git a/Wflow/src/routing/surface/surface_process.jl b/Wflow/src/routing/surface/surface_process.jl index df607f791..36544241a 100644 --- a/Wflow/src/routing/surface/surface_process.jl +++ b/Wflow/src/routing/surface/surface_process.jl @@ -145,3 +145,8 @@ function local_inertial_flow( return q end + +function manning_flow(mannings_n, hydraulic_radius, slope, area) + q = 1.0 / mannings_n * pow(hydraulic_radius, 2.0 / 3.0) * sqrt(slope) * area + return q +end diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 832aa0b4d..6843e165a 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -395,7 +395,7 @@ end sw_river.boundary_conditions.inwater[1] = 20.0 h0 = mean(sw_river.variables.h) dt = Wflow.stable_timestep(sw_river, flow_length) - Wflow.local_inertial_river_update!(sw_river, domain, dt, 86400.0, true) + Wflow.staggered_scheme_river_update!(sw_river, domain, dt, 86400.0, true) d = abs(h0 - mean(sw_river.variables.h)) if d <= epsilon break From cc924c04df56ac5959f603944a211bca99a90e7f Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 30 Mar 2026 09:11:12 +0200 Subject: [PATCH 18/90] Refactor init staggered routing --- Wflow/src/config_structure.jl | 2 +- Wflow/src/routing/initialize_routing.jl | 9 +- Wflow/src/routing/surface/floodplain.jl | 137 +++++------ .../routing/surface/surface_local_inertial.jl | 221 ++++-------------- Wflow/src/routing/utils.jl | 34 +++ 5 files changed, 146 insertions(+), 257 deletions(-) diff --git a/Wflow/src/config_structure.jl b/Wflow/src/config_structure.jl index db5b2c66b..2d212ef95 100644 --- a/Wflow/src/config_structure.jl +++ b/Wflow/src/config_structure.jl @@ -6,7 +6,7 @@ For configuration files we use TOML. =# # Option enumerators -@enumx RoutingType kinematic_wave kinematic_wave_staggered local_inertial +@enumx RoutingType kinematic_wave manning_staggered local_inertial @enumx ModelType sbm sbm_gwf sediment @enumx CalendarType standard gregorian proleptic_gregorian julian noleap _365_day all_leap _366_day _360_day @enumx GwfConductivityProfileType uniform exponential diff --git a/Wflow/src/routing/initialize_routing.jl b/Wflow/src/routing/initialize_routing.jl index ec132a29e..1f95f671b 100644 --- a/Wflow/src/routing/initialize_routing.jl +++ b/Wflow/src/routing/initialize_routing.jl @@ -72,12 +72,9 @@ function initialize_river_flow(dataset::NCDataset, config::Config, domain::Domai if river_routing == RoutingType.kinematic_wave river_flow = init_kinematic_wave_river_flow(dataset, config, domain.river, reservoir) - elseif river_routing == RoutingType.local_inertial - river_flow = - init_local_inertial_river_flow(dataset, config, domain.river, reservoir) - elseif river_routing == RoutingType.kinematic_wave_staggered - river_flow = - init_kinwave_staggered_river_flow(dataset, config, domain.river, reservoir) + elseif river_routing == RoutingType.local_inertial || + river_routing == RoutingType.kinematic_wave_staggered + river_flow = init_staggered_river_flow(dataset, config, domain.river, reservoir) end end diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 5f9d4d469..799878b37 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -112,33 +112,16 @@ function FloodPlainProfile( end "Struct to store local inertial floodplain flow model parameters" -@with_kw struct LocalInertialFloodPlainParameters{P} - profile::P # floodplain profile - mannings_n::Vector{Float64} # manning's roughness [s m-1/3] - mannings_n_sq::Vector{Float64} # manning's roughness squared at edge [(s m-1/3)2] - zb_max::Vector{Float64} # maximum bankfull elevation at edge [m] +@with_kw struct FloodPlainParameters{P} + profile::P # floodplain profile + mannings_n::Vector{Float64} = Float64[] # manning's roughness at edge [s m-1/3] + mannings_n_sq::Vector{Float64} = Float64[] # manning's roughness squared at edge [(s m-1/3)2] + zb_max::Vector{Float64} = Float64[] # maximum bankfull elevation at edge [m] + slope::Vector{Float64} = Float64[] # slope at edge [-] end -"Struct to store kinematic wave floodplain flow model parameters" -@with_kw struct KinWaveFloodPlainParameters{P} - profile::P # floodplain profile - mannings_n::Vector{Float64} # manning's roughness [s m-1/3] -end - -function get_floodplain_mannings_n(dataset, config, indices) - mannings_n = ncread( - dataset, - config, - "floodplain_water_flow__manning_n_parameter"; - sel = indices, - defaults = 0.072, - type = Float64, - ) - return mannings_n -end - -"Initialize local inertial floodplain flow model parameters" -function LocalInertialFloodPlainParameters( +"Initialize floodplain flow model parameters" +function FloodPlainParameters( dataset::NCDataset, config::Config, domain::DomainRiver, @@ -147,31 +130,44 @@ function LocalInertialFloodPlainParameters( ) (; indices, nodes_at_edge, graph) = domain.network (; flow_length) = domain.parameters + (; river_routing) = config.model n_edges = ne(graph) profile = FloodPlainProfile(dataset, config, domain; index_pit) - mannings_n = get_floodplain_mannings_n(dataset, config, indices) + mannings_n = ncread( + dataset, + config, + "floodplain_water_flow__manning_n_parameter"; + sel = indices, + defaults = 0.072, + type = Float64, + ) # manning roughness at edges append!(mannings_n, mannings_n[index_pit]) # copy to ghost nodes - mannings_n_sq = fill(Float64(0), n_edges) - zb_max = fill(Float64(0), n_edges) - for i in 1:n_edges - src_node = nodes_at_edge.src[i] - dst_node = nodes_at_edge.dst[i] - mannings_n_i = - ( - mannings_n[dst_node] * flow_length[dst_node] + - mannings_n[src_node] * flow_length[src_node] - ) / (flow_length[dst_node] + flow_length[src_node]) - mannings_n_sq[i] = mannings_n_i * mannings_n_i - zb_max[i] = max(zb_floodplain[src_node], zb_floodplain[dst_node]) + mannings_n_at_edge = + compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) + zb_max = compute_value_at_edge(zb_floodplain, nodes_at_edge, n_edges, maximum) + + if river_routing == RoutingType.local_inertial + mannings_n_sq = mannings_n_at_edge .* mannings_n_at_edge + slope_at_edge = [] + elseif river_routing == RoutingType.manning_staggered + mannings_n_sq = [] + slope_at_edge = + compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) end - parameters = - LocalInertialFloodPlainParameters(profile, mannings_n, mannings_n_sq, zb_max) + + parameters = FloodPlainParameters(; + profile, + mannings_n = mannings_n_at_edge, + mannings_n_sq, + zb_max, + slope = slope_at_edge, + ) return parameters end -"Struct to store local inertial floodplain flow model variables" -@with_kw struct LocalInertialFloodPlainVariables +"Struct to store floodplain flow model variables" +@with_kw struct FloodPlainVariables n::Int n_edges::Int storage::Vector{Float64} = zeros(n) # storage [m³] @@ -180,31 +176,17 @@ end a::Vector{Float64} = zeros(n_edges) # flow area at edge [m²] r::Vector{Float64} = zeros(n_edges) # hydraulic radius at edge [m] hf::Vector{Float64} = zeros(n_edges) # water depth at edge [m] - q0::Vector{Float64} = zeros(n_edges) # discharge at edge at previous time step + q0::Vector{Float64} = Float64[] # discharge at edge at previous time step q::Vector{Float64} = zeros(n_edges) # discharge at edge [m³ s⁻¹] q_av::Vector{Float64} = zeros(n_edges) # average river discharge at edge [m³ s⁻¹] for model timestep Δt hf_index::Vector{Int} = zeros(Int, n_edges) # edge index with `hf` [-] above depth threshold end -"Struct to store kinematic wave floodplain flow model variables" -@with_kw struct KinWaveFloodPlainVariables - n::Int - storage::Vector{Float64} = zeros(n) # storage [m³] - h::Vector{Float64} = zeros(n) # water depth [m] - q::Vector{Float64} = zeros(n) # discharge [m³ s⁻¹] - q_av::Vector{Float64} = zeros(n) # average river discharge [m³ s⁻¹] for model timestep Δt -end - -"Local inertial floodplain flow model" -@with_kw struct LocalInertialFloodPlainModel{P} <: AbstractFloodPlainModel - parameters::LocalInertialFloodPlainParameters{P} - variables::LocalInertialFloodPlainVariables -end - -"Kinematic wave floodplain flow model" -@with_kw struct KinWaveFloodPlainModel{P} <: AbstractFloodPlainModel - parameters::KinWaveFloodPlainParameters{P} - variables::KinWaveFloodPlainVariables +"Floodplain flow model" +@with_kw struct FloodPlainModel{T, P} <: AbstractFloodPlainModel + routing_method::T + parameters::FloodPlainParameters{P} + variables::FloodPlainVariables end "Determine the initial floodplain storage" @@ -280,33 +262,28 @@ function flood_depth( return flood_depth end -"Initialize floodplain geometry and `LocalInerialFloodPlain` variables and parameters" -function LocalInertialFloodPlainModel( +"Initialize floodplain geometry and model variables and parameters" +function FloodPlainModel( dataset::NCDataset, config::Config, domain::DomainRiver, zb_floodplain::Vector{Float64}, ) (; indices, local_drain_direction, graph) = domain.network + (; river_routing) = config.model n = length(indices) index_pit = findall(x -> x == 5, local_drain_direction) n_edges = ne(graph) - parameters = - LocalInertialFloodPlainParameters(dataset, config, domain, zb_floodplain, index_pit) - variables = - LocalInertialFloodPlainVariables(; n, n_edges, h = zeros(n + length(index_pit))) - - floodplain_model = LocalInertialFloodPlainModel(; parameters, variables) - return floodplain_model -end + parameters = FloodPlainParameters(dataset, config, domain, zb_floodplain, index_pit) + if river_routing == RoutingType.local_inertial + q0 = zeros(n_edges) + routing_method = LocalInertial() + elseif river_routing == RoutingType.manning_staggered + q0 = [] + routing_method = ManningStaggered() + end + variables = FloodPlainVariables(; n, n_edges, q0, h = zeros(n + length(index_pit))) -function KinWaveFloodPlainModel(dataset::NCDataset, config::Config, domain::DomainRiver) - (; indices) = domain.network - profile = FloodPlainProfile(dataset, config, domain) - mannings_n = get_floodplain_mannings_n(dataset, config, indices) - profile = FloodPlainProfile(dataset, config, domain) - parameters = KinWaveFloodPlainParameters(profile, mannings_n) - variables = KinWaveFloodPlainVariables() - floodplain_model = KinWaveFloodPlainModel(; parameters, variables) + floodplain_model = FloodPlainModel(; routing_method, parameters, variables) return floodplain_model end diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index 9172e4900..05c9f3399 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -73,106 +73,34 @@ function get_river_parameters( return mannings_n, bankfull_depth, bankfull_elevation, flow_length, flow_width end -function compute_value_at_edge(v, nodes_at_edge, n_edges, func::Function) - x = fill(Float64(0), n_edges) - for i in 1:n_edges - src_node = nodes_at_edge.src[i] - dst_node = nodes_at_edge.dst[i] - x[i] = func((v[src_node], v[dst_node])) - end - return x -end - -function compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) - mannings_n_at_edge = fill(Float64(0), n_edges) - for i in 1:n_edges - src_node = nodes_at_edge.src[i] - dst_node = nodes_at_edge.dst[i] - mannings_n_at_edge[i] = - ( - mannings_n[dst_node] * flow_length[dst_node] + - mannings_n[src_node] * flow_length[src_node] - ) / (flow_length[dst_node] + flow_length[src_node]) - end - return mannings_n_at_edge -end - -function compute_slope_at_edge(elev, length_at_edge, nodes_at_edge, n_edges) - slope = fill(Float64(0), n_edges) - for i in 1:n_edges - src_node = nodes_at_edge.src[i] - dst_node = nodes_at_edge.dst[i] - slope[i] = max((elev[src_node] - elev[dst_node]) / length_at_edge[i], 0.00001) - end - return slope -end - -"Initialize local inertial river flow model parameters" -function init_local_inertial_river_flow_parameters( - dataset::NCDataset, - config::Config, - domain::DomainRiver, -) +function log_message_staggered_flow(config::Config) + (; river_routing) = config.model alpha = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) waterdepth_threshold = config.model.river_water_flow_threshold__depth # depth threshold for flow at edge froude_limit = config.model.river_water_flow__froude_limit_flag # limit flow to subcritical according to Froude number floodplain_1d = config.model.floodplain_1d__flag - @info "Local inertial approach is used for river flow." alpha waterdepth_threshold froude_limit floodplain_1d - - (; graph, indices, local_drain_direction, nodes_at_edge) = domain.network - (; reservoir_outlet) = domain.parameters - - n = length(indices) - n_edges = ne(graph) - active_index = findall(x -> x == 0, reservoir_outlet) - index_pit = findall(x -> x == 5, local_drain_direction) - - mannings_n, bankfull_depth, bankfull_elevation, flow_length, flow_width = - get_river_parameters(dataset, config, domain, index_pit) - zb = bankfull_elevation - bankfull_depth # river bed elevation - bankfull_storage = bankfull_depth .* flow_width .* flow_length - - # determine z, width, length and manning's n at edges - zb_max = compute_value_at_edge(zb, nodes_at_edge, n_edges, maximum) - flow_width_at_edge = compute_value_at_edge(flow_width, nodes_at_edge, n_edges, minimum) - flow_length_at_edge = compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) - mannings_n_at_edge = - compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) - mannings_n_sq = mannings_n_at_edge .* mannings_n_at_edge - - parameters = RiverFlowStaggeredParameters(; - n, - n_edges, - active_n = active_index, - active_e = active_index, - froude_limit, - h_thresh = waterdepth_threshold, - zb, - zb_max, - bankfull_storage, - bankfull_depth, - mannings_n_sq, - flow_length_at_edge, - flow_width_at_edge, - ) - return parameters + if river_routing == RoutingType.local_inertial + @info "Local inertial approach is used for river flow." alpha waterdepth_threshold froude_limit floodplain_1d + elseif river_routing == RoutingType.manning_staggered + @info "Manning's equation on a staggered grid is used for river flow." alpha floodplain_1d + froude_limit = false + end + return waterdepth_threshold, froude_limit end -function init_manning_staggered_river_flow_parameters( +"Initialize river flow model parameters on a staggered grid" +function RiverFlowStaggeredParameters( dataset::NCDataset, config::Config, domain::DomainRiver, ) - alpha = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) - waterdepth_threshold = config.model.river_water_flow_threshold__depth # depth threshold for flow at edge - floodplain_1d = config.model.floodplain_1d__flag - - @info "Manning's equation on a staggered grid is used for river flow." alpha floodplain_1d - + (; river_routing) = config.model (; graph, indices, local_drain_direction, nodes_at_edge) = domain.network (; reservoir_outlet) = domain.parameters + waterdepth_threshold, froude_limit = log_message_staggered_flow(config) + n = length(indices) n_edges = ne(graph) active_index = findall(x -> x == 0, reservoir_outlet) @@ -183,25 +111,34 @@ function init_manning_staggered_river_flow_parameters( zb = bankfull_elevation - bankfull_depth # river bed elevation bankfull_storage = bankfull_depth .* flow_width .* flow_length - # determine z, width, length and manning's n and slope at edges + # determine parameters at edges zb_max = compute_value_at_edge(zb, nodes_at_edge, n_edges, maximum) flow_width_at_edge = compute_value_at_edge(flow_width, nodes_at_edge, n_edges, minimum) flow_length_at_edge = compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) mannings_n_at_edge = compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) - slope_at_edge = compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) + if river_routing == RoutingType.local_inertial + mannings_n_sq = mannings_n_at_edge .* mannings_n_at_edge + slope_at_edge = [] + elseif river_routing == RoutingType.manning_staggered + mannings_n_sq = [] + slope_at_edge = + compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) + end parameters = RiverFlowStaggeredParameters(; n, n_edges, active_n = active_index, active_e = active_index, + froude_limit, h_thresh = waterdepth_threshold, zb, zb_max, bankfull_storage, bankfull_depth, mannings_n = mannings_n_at_edge, + mannings_n_sq, slope = slope_at_edge, flow_length_at_edge, flow_width_at_edge, @@ -228,84 +165,58 @@ end error::Vector{Float64} = zeros(n) # error storage [m³] end -function get_river_depth_bc(dataset, config, indices) - riverdepth_bc = ncread( - dataset, - config, - "model_boundary_condition_river_bank_water__depth"; - sel = indices, - defaults = 0.0, - type = Float64, - ) - return riverdepth_bc -end - -function init_manning_staggered_river_flow_variables( +function RiverFlowStaggeredVariables( dataset::NCDataset, config::Config, network::NetworkRiver, ) (; pit_indices, indices, graph) = network + (; river_routing) = config.model - riverdepth_bc = get_river_depth_bc(dataset, config, pit_indices) - n = length(indices) - n_edges = ne(graph) - # set river depth h to zero (including reservoir locations) - h = zeros(n) - q_av = zeros(n_edges) - # set ghost points for boundary condition (downstream river outlet): river depth `h` - append!(h, riverdepth_bc) - - variables = RiverFlowStaggeredVariables(; - n, - n_edges, - q_av, - q_channel_av = config.model.floodplain_1d__flag ? zeros(n_edges) : q_av, - h, + riverdepth_bc = ncread( + dataset, + config, + "model_boundary_condition_river_bank_water__depth"; + sel = pit_indices, + defaults = 0.0, + type = Float64, ) - return variables -end - -"Initialize local inertial river flow model variables" -function init_local_inertial_river_flow_variables( - dataset::NCDataset, - config::Config, - network::NetworkRiver, -) - (; pit_indices, indices, graph) = network - riverdepth_bc = get_river_depth_bc(dataset, config, pit_indices) n = length(indices) n_edges = ne(graph) # set river depth h to zero (including reservoir locations) h = zeros(n) q_av = zeros(n_edges) - q0 = zeros(n_edges) + if river_routing == RoutingType.local_inertial + q0 = zeros(n_edges) + elseif river_routing == RoutingType.manning_staggered + q0 = [] + end # set ghost points for boundary condition (downstream river outlet): river depth `h` append!(h, riverdepth_bc) variables = RiverFlowStaggeredVariables(; n, n_edges, - q_av, q0, + q_av, q_channel_av = config.model.floodplain_1d__flag ? zeros(n_edges) : q_av, h, ) return variables end -"Initialize local inertial river flow model" -function init_local_inertial_river_flow( +"Initialize river flow model on a staggered grid" +function init_staggered_river_flow( dataset::NCDataset, config::Config, domain::DomainRiver, reservoir_model::Union{ReservoirModel, Nothing}, ) - # The local inertial approach makes use of a staggered grid (Bates et al. (2010)), - # with nodes and edges. This information is extracted from the directed graph of the - # river. Discharge q is calculated at edges between nodes and mapped to the source - # nodes for gridded output (index of edge is equal to source node index, e.g.: + # This river flow model makes use of a staggered grid (Bates et al. (2010)), with nodes + # and edges. This information is extracted from the directed graph of the river. + # Discharge q is calculated at edges between nodes and mapped to the source nodes for + # gridded output (index of edge is equal to source node index, e.g.: # Edge 1 => 5 # Edge 2 => 1 # Edge 3 => 2 @@ -317,13 +228,13 @@ function init_local_inertial_river_flow( alpha_coefficient = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) timestepping = TimeStepping(; alpha_coefficient) - parameters = init_local_inertial_river_flow_parameters(dataset, config, domain) - variables = init_local_inertial_river_flow_variables(dataset, config, domain.network) + parameters = RiverFlowStaggeredParameters(dataset, config, domain) + variables = RiverFlowStaggeredVariables(dataset, config, domain.network) boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir_model) if config.model.floodplain_1d__flag zb_floodplain = parameters.zb .+ parameters.bankfull_depth - floodplain = LocalInertialFloodPlainModel(dataset, config, domain, zb_floodplain) + floodplain = FloodPlainModel(dataset, config, domain, zb_floodplain) else floodplain = nothing end @@ -343,36 +254,6 @@ function init_local_inertial_river_flow( return river_flow end -"Initialize river manning's flow model on a staggered grid" -function init_manning_staggered_river_flow( - dataset::NCDataset, - config::Config, - domain::DomainRiver, - reservoir_model::Union{ReservoirModel, Nothing}, -) - alpha_coefficient = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) - - timestepping = TimeStepping(; alpha_coefficient) - parameters = init_manning_staggered_river_flow_parameters(dataset, config, domain) - variables = init_manning_staggered_river_flow_variables(dataset, config, domain.network) - boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir_model) - floodplain = nothing - routing_method = ManningStaggered() - - n = length(domain.network.indices) - river_flow = RiverFlowModel(; - timestepping, - boundary_conditions, - parameters, - variables, - floodplain, - allocation = do_water_demand(config) ? AllocationRiverModel(n) : - NoAllocationRiverModel(n), - routing_method, - ) - return river_flow -end - """ Return the upstream inflow for a reservoir in a river flow model on a staggered grid. """ @@ -512,7 +393,7 @@ function update_floodplain_flow!( river_flow_model::RiverFlowModel{T, P, V, F}, domain::DomainRiver, dt::Float64, -) where {T <: LocalInertial, P, V, F <: AbstractFloodPlainModel} +) where {T <: LocalInertial, P, V, F <: FloodPlainModel{<:LocalInertial}} (; nodes_at_edge) = domain.network (; flow_width) = domain.parameters @@ -617,7 +498,7 @@ function update_floodplain_flow!( river_flow_model::RiverFlowModel{T, P, V, F}, domain::DomainRiver, dt::Float64, -) where {T <: ManningStaggered, P, V, F <: AbstractFloodPlainModel} +) where {T <: ManningStaggered, P, V, F <: FloodPlainModel{<:ManningStaggered}} (; nodes_at_edge) = domain.network (; flow_width) = domain.parameters diff --git a/Wflow/src/routing/utils.jl b/Wflow/src/routing/utils.jl index 19d142325..bf88814fa 100644 --- a/Wflow/src/routing/utils.jl +++ b/Wflow/src/routing/utils.jl @@ -122,3 +122,37 @@ function flux_in!(flux_in, flux, network) end return nothing end + +function compute_value_at_edge(v, nodes_at_edge, n_edges, func::Function) + x = fill(Float64(0), n_edges) + for i in 1:n_edges + src_node = nodes_at_edge.src[i] + dst_node = nodes_at_edge.dst[i] + x[i] = func((v[src_node], v[dst_node])) + end + return x +end + +function compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) + mannings_n_at_edge = fill(Float64(0), n_edges) + for i in 1:n_edges + src_node = nodes_at_edge.src[i] + dst_node = nodes_at_edge.dst[i] + mannings_n_at_edge[i] = + ( + mannings_n[dst_node] * flow_length[dst_node] + + mannings_n[src_node] * flow_length[src_node] + ) / (flow_length[dst_node] + flow_length[src_node]) + end + return mannings_n_at_edge +end + +function compute_slope_at_edge(elev, length_at_edge, nodes_at_edge, n_edges) + slope = fill(Float64(0), n_edges) + for i in 1:n_edges + src_node = nodes_at_edge.src[i] + dst_node = nodes_at_edge.dst[i] + slope[i] = max((elev[src_node] - elev[dst_node]) / length_at_edge[i], 0.00001) + end + return slope +end From fd91dd92e7a18dc219ec051cad5da4fcec5f25a8 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 30 Mar 2026 15:45:05 +0200 Subject: [PATCH 19/90] Add `stable_timestep` function `ManningStaggered` For manning flow on a staggered grid. --- .../routing/surface/surface_local_inertial.jl | 57 +++++++++++++------ Wflow/test/routing_process.jl | 4 +- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index 05c9f3399..ebeb19b46 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -11,8 +11,9 @@ bankfull_storage::Vector{Float64} = Float64[] # bankfull storage [m³] bankfull_depth::Vector{Float64} = Float64[] # bankfull depth [m] mannings_n_sq::Vector{Float64} = Float64[] # Manning's roughness squared at edge [(s m-1/3)2] - mannings_n::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] at edge - slope::Vector{Float64} = Float64[] # slope at edge [-] + mannings_n::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] + mannings_n_at_edge::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] at edge + slope_at_edge::Vector{Float64} = Float64[] # slope at edge [-] flow_length_at_edge::Vector{Float64} = Float64[] # flow (river) length at edge [m] flow_width_at_edge::Vector{Float64} = Float64[] # flow (river) width at edge [m] end @@ -137,9 +138,10 @@ function RiverFlowStaggeredParameters( zb_max, bankfull_storage, bankfull_depth, - mannings_n = mannings_n_at_edge, + mannings_n, + mannings_n_at_edge, mannings_n_sq, - slope = slope_at_edge, + slope_at_edge, flow_length_at_edge, flow_width_at_edge, ) @@ -370,9 +372,9 @@ function update_river_channel_flow!( river_v.q[i] = ifelse( river_v.hf[i] > river_p.h_thresh, manning_flow( - river_p.mannings_n[i], + river_p.mannings_n_at_edge[i], river_v.r[i], - river_p.slope[i], + river_p.slope_at_edge[i], river_v.a[i], ), 0.0, @@ -562,9 +564,9 @@ function update_floodplain_flow!( floodplain_v.q[i] = if floodplain_v.a[i] > 1.0e-05 mannings_flow( - floodplain_p.mannings_n[i], + floodplain_p.mannings_n_at_edge[i], floodplain_v.r[i], - floodplain_p.slope[i], + floodplain_p.slope_at_edge[i], floodplain_v.a[i], ) else @@ -771,7 +773,7 @@ function update_river_flow_model!( update_h = true, ) (; reservoir) = river_flow_model.boundary_conditions - (; flow_length) = domain.river.parameters + (; parameters) = domain.river set_reservoir_vars!(reservoir) update_index_hq!(reservoir, clock) @@ -784,7 +786,7 @@ function update_river_flow_model!( dt = tosecond(clock.dt) t = 0.0 while t < dt - dt_s = stable_timestep(river_flow_model, flow_length) + dt_s = stable_timestep(river_flow_model, parameters) dt_s = check_timestepsize(dt_s, t, dt) staggered_scheme_river_update!(river_flow_model, domain, dt_s, dt, update_h) t += dt_s @@ -942,7 +944,7 @@ function init_local_inertial_overland_flow( end """ - stable_timestep(river_flow_model::RiverFlowModel{<:LocalInertial}, flow_length::Vector{Float64}) + stable_timestep(river_flow_model::RiverFlowModel{<:LocalInertial}, parameters::RiverParameters) stable_timestep(overland_flow_model::OverlandFlowModel{<:LocalInertial}, parameters::LandParameters) Compute a stable timestep size for the local inertial approach, based on Bates et al. (2010). @@ -950,13 +952,14 @@ Compute a stable timestep size for the local inertial approach, based on Bates e dt = α * (Δx / sqrt(g max(h)) """ function stable_timestep( - river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, - flow_length::Vector{Float64}, + river_flow_model::RiverFlowModel{<:LocalInertial}, + parameters::RiverParameters, ) dt_min = Inf (; alpha_coefficient) = river_flow_model.timestepping (; n) = river_flow_model.parameters (; h) = river_flow_model.variables + (; flow_length) = parameters @batch per = thread reduction = ((min, dt_min),) for i in 1:(n) @fastmath @inbounds dt = alpha_coefficient * flow_length[i] / sqrt(GRAVITATIONAL_ACCELERATION * h[i]) @@ -988,6 +991,27 @@ function stable_timestep( return dt_min end +function stable_timestep( + river_flow_model::RiverFlowModel{<:ManningStaggered}, + parameters::RiverParameters, +) + dt_min = Inf + (; alpha_coefficient) = river_flow_model.timestepping + (; n, mannings_n) = river_flow_model.parameters + (; h) = river_flow_model.variables + (; flow_length, flow_width, slope) = parameters + + beta = 5.0/3.0 + @batch per = thread reduction = ((min, dt_min),) for i in 1:(n) + @fastmath @inbounds h_r = (flow_width[i] * h[i]) / (flow_width[i] + 2.0 * h[i]) + celerity = beta * 1.0/mannings_n[i] * pow(h_r, 2.0/3.0) * sqrt(slope[i]) + dt = alpha_coefficient * flow_length[i] / celerity + dt_min = min(dt, dt_min) + end + dt_min = isinf(dt_min) ? 600.0 : dt_min + return dt_min +end + """ Update boundary condition `runoff` local inertial overland flow model for a single timestep. """ @@ -1072,7 +1096,8 @@ function update_overland_flow_model!( ) (; reservoir) = river_flow_model.boundary_conditions (; flow_length) = domain.river.parameters - (; parameters) = domain.land + land_params = domain.land.parameters + river_params = domain.river.parameters set_reservoir_vars!(reservoir) update_index_hq!(reservoir, clock) @@ -1082,8 +1107,8 @@ function update_overland_flow_model!( dt = tosecond(clock.dt) t = 0.0 while t < dt - dt_river = stable_timestep(river_flow_model, flow_length) - dt_land = stable_timestep(overland_flow_model, parameters) + dt_river = stable_timestep(river_flow_model, river_params) + dt_land = stable_timestep(overland_flow_model, land_params) dt_s = min(dt_river, dt_land) dt_s = check_timestepsize(dt_s, t, dt) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 6843e165a..e6dab58de 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -390,11 +390,11 @@ end # run until steady state is reached epsilon = 1.0e-12 - (; flow_length) = domain_river.parameters + river_params = domain_river.parameters while true sw_river.boundary_conditions.inwater[1] = 20.0 h0 = mean(sw_river.variables.h) - dt = Wflow.stable_timestep(sw_river, flow_length) + dt = Wflow.stable_timestep(sw_river, river_params) Wflow.staggered_scheme_river_update!(sw_river, domain, dt, 86400.0, true) d = abs(h0 - mean(sw_river.variables.h)) if d <= epsilon From 121ba994e0281f06128feecc3bb16320adfbed4e Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 30 Mar 2026 17:05:21 +0200 Subject: [PATCH 20/90] Some fixes To run manning river and floodplain flow on a staggered grid. --- Wflow/src/domain.jl | 2 +- Wflow/src/routing/initialize_routing.jl | 2 +- Wflow/src/routing/surface/floodplain.jl | 24 ++++++++++++------- .../routing/surface/surface_local_inertial.jl | 10 ++++++-- ...iver-floodplain-local-inertial_config.toml | 2 +- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Wflow/src/domain.jl b/Wflow/src/domain.jl index 9ceb401f9..1c99c63ef 100644 --- a/Wflow/src/domain.jl +++ b/Wflow/src/domain.jl @@ -108,7 +108,7 @@ function Domain(dataset::NCDataset, config::Config, ::Union{SbmModel, SbmGwfMode @reset network_river.upstream_nodes = filter_upstream_nodes(network_river.graph, pits[network_river.indices]) elseif river_routing == RoutingType.local_inertial || - river_routing == RoutingType.kinematic_wave_staggered + river_routing == RoutingType.manning_staggered nodes_at_edge, index_pit = NodesAtEdge(network_river) @reset network_river.nodes_at_edge = nodes_at_edge @reset network_river.pit_indices = network_river.indices[index_pit] diff --git a/Wflow/src/routing/initialize_routing.jl b/Wflow/src/routing/initialize_routing.jl index 1f95f671b..20b7088bf 100644 --- a/Wflow/src/routing/initialize_routing.jl +++ b/Wflow/src/routing/initialize_routing.jl @@ -73,7 +73,7 @@ function initialize_river_flow(dataset::NCDataset, config::Config, domain::Domai river_flow = init_kinematic_wave_river_flow(dataset, config, domain.river, reservoir) elseif river_routing == RoutingType.local_inertial || - river_routing == RoutingType.kinematic_wave_staggered + river_routing == RoutingType.manning_staggered river_flow = init_staggered_river_flow(dataset, config, domain.river, reservoir) end end diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 799878b37..4c84e26cd 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -113,11 +113,11 @@ end "Struct to store local inertial floodplain flow model parameters" @with_kw struct FloodPlainParameters{P} - profile::P # floodplain profile - mannings_n::Vector{Float64} = Float64[] # manning's roughness at edge [s m-1/3] - mannings_n_sq::Vector{Float64} = Float64[] # manning's roughness squared at edge [(s m-1/3)2] - zb_max::Vector{Float64} = Float64[] # maximum bankfull elevation at edge [m] - slope::Vector{Float64} = Float64[] # slope at edge [-] + profile::P # floodplain profile + mannings_n_at_edge::Vector{Float64} = Float64[] # manning's roughness at edge [s m-1/3] + mannings_n_sq::Vector{Float64} = Float64[] # manning's roughness squared at edge [(s m-1/3)2] + zb_max::Vector{Float64} = Float64[] # maximum bankfull elevation at edge [m] + slope_at_edge::Vector{Float64} = Float64[] # slope at edge [-] end "Initialize floodplain flow model parameters" @@ -152,16 +152,22 @@ function FloodPlainParameters( slope_at_edge = [] elseif river_routing == RoutingType.manning_staggered mannings_n_sq = [] - slope_at_edge = - compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) + flow_length_at_edge = + compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) + slope_at_edge = compute_slope_at_edge( + zb_floodplain, + flow_length_at_edge, + nodes_at_edge, + n_edges, + ) end parameters = FloodPlainParameters(; profile, - mannings_n = mannings_n_at_edge, + mannings_n_at_edge, mannings_n_sq, zb_max, - slope = slope_at_edge, + slope_at_edge, ) return parameters end diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index ebeb19b46..da320a9f6 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -240,7 +240,13 @@ function init_staggered_river_flow( else floodplain = nothing end - routing_method = LocalInertial() + + (; river_routing) = config.model + routing_method = if river_routing == RoutingType.local_inertial + LocalInertial() + elseif river_routing == RoutingType.manning_staggered + ManningStaggered() + end n = length(domain.network.indices) river_flow = RiverFlowModel(; @@ -563,7 +569,7 @@ function update_floodplain_flow!( end floodplain_v.q[i] = if floodplain_v.a[i] > 1.0e-05 - mannings_flow( + manning_flow( floodplain_p.mannings_n_at_edge[i], floodplain_v.r[i], floodplain_p.slope_at_edge[i], diff --git a/Wflow/test/sbm_river-floodplain-local-inertial_config.toml b/Wflow/test/sbm_river-floodplain-local-inertial_config.toml index 13d2f5920..d0f62a5d4 100644 --- a/Wflow/test/sbm_river-floodplain-local-inertial_config.toml +++ b/Wflow/test/sbm_river-floodplain-local-inertial_config.toml @@ -124,7 +124,7 @@ offset = 0.0 [model] kinematic_wave__adaptive_time_step_flag = true -river_routing = "local_inertial" +river_routing = "manning_staggered" floodplain_1d__flag = true snow_gravitational_transport__flag = true cold_start__flag = true From b86f3ab2bff5bc02e6a25b0ee647cff017d59d83 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 30 Mar 2026 20:18:52 +0200 Subject: [PATCH 21/90] Revert `river_routing` TOML setting change --- Wflow/test/sbm_river-floodplain-local-inertial_config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wflow/test/sbm_river-floodplain-local-inertial_config.toml b/Wflow/test/sbm_river-floodplain-local-inertial_config.toml index d0f62a5d4..13d2f5920 100644 --- a/Wflow/test/sbm_river-floodplain-local-inertial_config.toml +++ b/Wflow/test/sbm_river-floodplain-local-inertial_config.toml @@ -124,7 +124,7 @@ offset = 0.0 [model] kinematic_wave__adaptive_time_step_flag = true -river_routing = "manning_staggered" +river_routing = "local_inertial" floodplain_1d__flag = true snow_gravitational_transport__flag = true cold_start__flag = true From 992fe9294b9ab1ff2c8a9af9adf568e2a3753e4b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 31 Mar 2026 11:21:38 +0200 Subject: [PATCH 22/90] Add TOML setting `alpha` staggered manning flow --- Wflow/src/config_structure.jl | 5 ++++- Wflow/src/routing/surface/surface_local_inertial.jl | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Wflow/src/config_structure.jl b/Wflow/src/config_structure.jl index 2d212ef95..8b2a9030f 100644 --- a/Wflow/src/config_structure.jl +++ b/Wflow/src/config_structure.jl @@ -91,11 +91,14 @@ end river_local_inertial_flow__alpha_coefficient::Float64 = 0.7 land_local_inertial_flow__alpha_coefficient::Float64 = 0.7 land_local_inertial_flow__theta_coefficient::Float64 = 1.0 - river_water_flow_threshold__depth = 1e-3 land_surface_water_flow_threshold__depth = 1e-3 river_water_flow__froude_limit_flag = true land_surface_water_flow__froude_limit_flag = true + # Local inertial and staggered manning river flow routing + river_water_flow_threshold__depth = 1e-3 floodplain_1d__flag::Bool = false + # Staggered manning river flow routing + river_staggered_manning_flow__alpha_coefficient::Float64 = 0.7 # Groundwater flow conductivity_profile::GwfConductivityProfileType.T = GwfConductivityProfileType.uniform drain__flag::Bool = false diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_local_inertial.jl index da320a9f6..8287c705a 100644 --- a/Wflow/src/routing/surface/surface_local_inertial.jl +++ b/Wflow/src/routing/surface/surface_local_inertial.jl @@ -76,16 +76,17 @@ end function log_message_staggered_flow(config::Config) (; river_routing) = config.model - alpha = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) waterdepth_threshold = config.model.river_water_flow_threshold__depth # depth threshold for flow at edge - froude_limit = config.model.river_water_flow__froude_limit_flag # limit flow to subcritical according to Froude number floodplain_1d = config.model.floodplain_1d__flag if river_routing == RoutingType.local_inertial + alpha = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) + froude_limit = config.model.river_water_flow__froude_limit_flag # limit flow to subcritical according to Froude number @info "Local inertial approach is used for river flow." alpha waterdepth_threshold froude_limit floodplain_1d elseif river_routing == RoutingType.manning_staggered - @info "Manning's equation on a staggered grid is used for river flow." alpha floodplain_1d + alpha = config.model.river_staggered_manning_flow__alpha_coefficient froude_limit = false + @info "Manning's equation on a staggered grid is used for river flow." alpha floodplain_1d end return waterdepth_threshold, froude_limit end From 22936f705a0cdba255140d1637f79e94f51bb6fa Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 31 Mar 2026 14:49:36 +0200 Subject: [PATCH 23/90] Rename file `surface_local_inertial.jl` --- Wflow/src/Wflow.jl | 2 +- .../{surface_local_inertial.jl => surface_staggered_scheme.jl} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Wflow/src/routing/surface/{surface_local_inertial.jl => surface_staggered_scheme.jl} (100%) diff --git a/Wflow/src/Wflow.jl b/Wflow/src/Wflow.jl index 4407b5e11..e27476a0e 100644 --- a/Wflow/src/Wflow.jl +++ b/Wflow/src/Wflow.jl @@ -207,7 +207,7 @@ include("routing/surface/reservoir.jl") include("routing/surface/floodplain.jl") include("routing/surface/surface_flow.jl") include("routing/surface/surface_kinwave.jl") -include("routing/surface/surface_local_inertial.jl") +include("routing/surface/surface_staggered_scheme.jl") include("routing/surface/surface_routing.jl") include("routing/surface/surface_process.jl") include("demand/water_demand.jl") diff --git a/Wflow/src/routing/surface/surface_local_inertial.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl similarity index 100% rename from Wflow/src/routing/surface/surface_local_inertial.jl rename to Wflow/src/routing/surface/surface_staggered_scheme.jl From e5ffb8c8d80bf8d1d867cbe4e514dd62358e5768 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 1 Apr 2026 14:14:37 +0200 Subject: [PATCH 24/90] Add test river and floodplain Manning's flow on staggered grid --- Wflow/test/run_sbm.jl | 30 ++- ...iver-floodplain-local-inertial_config.toml | 229 ------------------ Wflow/test/utils.jl | 2 +- 3 files changed, 29 insertions(+), 232 deletions(-) delete mode 100644 Wflow/test/sbm_river-floodplain-local-inertial_config.toml diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index 58cbe2ce2..7012f265f 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -442,7 +442,7 @@ end end @testitem "Local-inertial option for river flow including 1D floodplain schematization" begin - tomlpath = joinpath(@__DIR__, "sbm_river-floodplain-local-inertial_config.toml") + tomlpath = joinpath(@__DIR__, "sbm_river-floodplain-staggered-scheme_config.toml") config = Wflow.Config(tomlpath) config.dir_output = mktempdir() model = Wflow.Model(config) @@ -717,6 +717,32 @@ end end end +@testitem "River flow including 1D floodplain schematization using Manning's equation on a staggered grid" begin + tomlpath = joinpath(@__DIR__, "sbm_river-floodplain-staggered-scheme_config.toml") + config = Wflow.Config(tomlpath) + config.model.river_routing = "manning_staggered" + config.dir_output = mktempdir() + model = Wflow.Model(config) + (; river_flow) = model.routing + + Wflow.run_timestep!(model) + Wflow.run_timestep!(model) + + (; q_av, h) = river_flow.variables + @test sum(q_av) ≈ 2198.5455132587495 + @test q_av[1622] ≈ 0.0002189225364991537 + @test q_av[43] ≈ 10.2692066407329 + @test q_av[501] ≈ 0.021410239377431573 + @test q_av[5808] ≈ 0.004923137077613924 + @test h[1622] ≈ 0.0017960475101775342 + @test h[43] ≈ 1.3029109034019006 + @test h[501] ≈ 0.005400545537332966 + @test h[5808] ≈ 0.00589431731806414 + (; q_av, h) = river_flow.floodplain.variables + @test maximum(q_av) ≈ 1.1824324225913527 + @test maximum(h) ≈ 1.1422278106700228 +end + @testitem "run wflow sbm" begin tomlpath = joinpath(@__DIR__, "sbm_config.toml") config = Wflow.Config(tomlpath) @@ -785,7 +811,7 @@ end end @testitem "water balance river local inertial routing with floodplain" begin - tomlpath = joinpath(@__DIR__, "sbm_river-floodplain-local-inertial_config.toml") + tomlpath = joinpath(@__DIR__, "sbm_river-floodplain-staggered-scheme_config.toml") config = Wflow.Config(tomlpath) config.dir_output = mktempdir() config.model.water_mass_balance__flag = true diff --git a/Wflow/test/sbm_river-floodplain-local-inertial_config.toml b/Wflow/test/sbm_river-floodplain-local-inertial_config.toml deleted file mode 100644 index 13d2f5920..000000000 --- a/Wflow/test/sbm_river-floodplain-local-inertial_config.toml +++ /dev/null @@ -1,229 +0,0 @@ -# This is a TOML configuration file for Wflow. -# Relative file paths are interpreted as being relative to this TOML file. -# Wflow documentation https://deltares.github.io/Wflow.jl/dev/ -# TOML documentation: https://github.com/toml-lang/toml - -dir_input = "data/input" -dir_output = "data/output" - -[logging] -loglevel = "info" - -[time] -calendar = "proleptic_gregorian" -endtime = 2000-02-01T00:00:00 -starttime = 2000-01-01T00:00:00 -time_units = "days since 1900-01-01 00:00:00" -timestepsecs = 86400 - -[state] -path_input = "instates-moselle.nc" -path_output = "outstates-moselle.nc" - -# if listed, the variable must be present in the NetCDF or error -# if not listed, the variable can get a default value if it has one - -[state.variables] -vegetation_canopy_water__depth = "canopystorage" - -soil_water_saturated_zone__depth = "satwaterdepth" -soil_surface__temperature = "tsoil" -soil_layer_water_unsaturated_zone__depth = "ustorelayerdepth" - -snowpack_dry_snow__leq_depth = "snow" -snowpack_liquid_water__depth = "snowwater" - -river_water__depth = "h_river" -river_water__instantaneous_volume_flow_rate = "q_river" - -floodplain_water__instantaneous_volume_flow_rate = "q_floodplain" -floodplain_water__depth = "h_floodplain" - -reservoir_water_surface__elevation = "waterlevel_reservoir" - -subsurface_water__instantaneous_volume_flow_rate = "ssf" - -land_surface_water__instantaneous_volume_flow_rate = "q_land" -land_surface_water__depth = "h_land" - -[input] -path_forcing = "forcing-moselle.nc" -path_static = "staticmaps-moselle.nc" - -# these are not directly part of the model -basin__local_drain_direction = "wflow_ldd" -river_location__mask = "wflow_river" -reservoir_area__count = "wflow_reservoirareas" -reservoir_location__count = "wflow_reservoirlocs" -subbasin_location__count = "wflow_subcatch" -river_gauge__count = "wflow_gauges_grdc" - -[input.forcing] -atmosphere_water__precipitation_volume_flux = "precip" -land_surface_water__potential_evaporation_volume_flux = "pet" -atmosphere_air__temperature = "temp" - -[input.static] -atmosphere_air__snowfall_temperature_threshold = "TT" -atmosphere_air__snowfall_temperature_interval = "TTI" - -land_water_covered__area_fraction = "WaterFrac" - -snowpack__melting_temperature_threshold = "TTM" -snowpack__degree_day_coefficient = "Cfmax" -snowpack__liquid_water_holding_capacity = "WHC" - -soil_layer_water__brooks_corey_exponent = "c" -soil_surface_water__infiltration_reduction_parameter = "cf_soil" -soil_water__vertical_saturated_hydraulic_conductivity_scale_parameter = "f" -compacted_soil_surface_water__infiltration_capacity = "InfiltCapPath" -soil_water__residual_volume_fraction = "thetaR" -soil_water__saturated_volume_fraction = "thetaS" -soil_water_saturated_zone_bottom__max_leakage_volume_flux = "MaxLeakage" -compacted_soil__area_fraction = "PathFrac" -soil_wet_root__sigmoid_function_shape_parameter = "rootdistpar" -soil__thickness = "SoilThickness" - -vegetation_canopy_water__mean_evaporation_to_mean_precipitation_ratio = "EoverR" -vegetation_canopy__light_extinction_coefficient = "Kext" -vegetation__specific_leaf_storage = "Sl" -vegetation_wood_water__storage_capacity = "Swood" -vegetation_root__depth = "RootingDepth" - -river__length = "wflow_riverlength" -river_water_flow__manning_n_parameter = "N_River" -river__slope = "RiverSlope" -river__width = "wflow_riverwidth" -river_bank_water__depth = "RiverDepth" -river_bank_water__elevation = "RiverZ" -floodplain_water__sum_of_volume_per_depth = "floodplain_volume" - -land_surface_water_flow__manning_n_parameter = "N" -land_surface__slope = "Slope" -land_surface__elevation = "wflow_dem" - -reservoir_surface__area = "reservoir_area" -reservoir_water_demand__required_downstream_volume_flow_rate = "ResDemand" -reservoir_water_release_below_spillway__max_volume_flow_rate = "ResMaxRelease" -reservoir_water__max_volume = "ResMaxVolume" -reservoir_water__target_full_volume_fraction = "ResTargetFullFrac" -reservoir_water__target_min_volume_fraction = "ResTargetMinFrac" -reservoir_water_surface__initial_elevation = "waterlevel_reservoir" -reservoir_water__rating_curve_type_count = "outflowfunc" -reservoir_water__storage_curve_type_count = "storfunc" - -subsurface_water__horizontal_to_vertical_saturated_hydraulic_conductivity_ratio = "KsatHorFrac" - -[input.cyclic] -vegetation__leaf_area_index = "LAI" - -[input.static.soil_surface_water__vertical_saturated_hydraulic_conductivity] -netcdf_variable_name = "KsatVer" -scale = 1.0 -offset = 0.0 - -[model] -kinematic_wave__adaptive_time_step_flag = true -river_routing = "local_inertial" -floodplain_1d__flag = true -snow_gravitational_transport__flag = true -cold_start__flag = true -reservoir__flag = true -snow__flag = true -soil_layer__thickness = [100, 300, 800] -type = "sbm" -river_streamorder__min_count = 6 -land_streamorder__min_count = 5 - -[output.netcdf_grid] -path = "output_moselle.nc" - -[output.netcdf_grid.variables] -soil_water_saturated_zone__depth = "satwaterdepth" -soil_surface__temperature = "tsoil" -soil_layer_water_unsaturated_zone__depth = "ustorelayerdepth" -snowpack_dry_snow__leq_depth = "snow" -snowpack_liquid_water__depth = "snowwater" -river_water__depth = "h_river" -river_water__volume_flow_rate = "q_av_river" -reservoir_water__volume = "storage_reservoir" -subsurface_water__volume_flow_rate = "ssf_av" -land_surface_water__volume_flow_rate = "q_av_land" -land_surface_water__depth = "h_land" -land.interception.variables.canopy_storage = "canopystorage" - -[output.netcdf_scalar] -path = "output_scalar_moselle.nc" - -[[output.netcdf_scalar.variable]] -name = "Q" -map = "river_gauge__count" -parameter = "routing.river_flow.variables.q" - -[[output.netcdf_scalar.variable]] -coordinate.x = 6.255 -coordinate.y = 50.012 -name = "temp_coord" -location = "temp_bycoord" -parameter = "atmosphere_air__temperature" - -[[output.netcdf_scalar.variable]] -location = "temp_byindex" -name = "temp_index" -index.x = 100 -index.y = 264 -parameter = "atmosphere_air__temperature" - -[output.csv] -path = "output_moselle.csv" - -[[output.csv.column]] -header = "Q" -parameter = "river_water__volume_flow_rate" -reducer = "maximum" - -[[output.csv.column]] -header = "storage" -index = 1 -parameter = "reservoir_water__volume" - -[[output.csv.column]] -coordinate.x = 6.255 -coordinate.y = 50.012 -header = "temp_bycoord" -parameter = "atmosphere_air__temperature" - -[[output.csv.column]] -coordinate.x = 6.255 -coordinate.y = 50.012 -header = "vwc_layer2_bycoord" -parameter = "land.soil.variables.vwc" -layer = 2 - -[[output.csv.column]] -header = "temp_byindex" -index.x = 100 -index.y = 264 -parameter = "atmosphere_air__temperature" - -[[output.csv.column]] -header = "Q" -map = "river_gauge__count" -parameter = "river_water__volume_flow_rate" - -[[output.csv.column]] -header = "recharge" -map = "subbasin_location__count" -parameter = "soil_water_saturated_zone_top__net_recharge_volume_flux" -reducer = "mean" - -[API] -variables = [ - "river_water__volume_flow_rate", - "soil_water_unsaturated_zone__depth", - "soil_water__transpiration_volume_flux", - "soil_layer_1_water_unsaturated_zone__depth", - "soil_layer_2_water_unsaturated_zone__depth", - "soil_layer_3_water_unsaturated_zone__depth", - "soil_layer_4_water_unsaturated_zone__depth", -] diff --git a/Wflow/test/utils.jl b/Wflow/test/utils.jl index 66ad83ca7..84d2c0b9a 100644 --- a/Wflow/test/utils.jl +++ b/Wflow/test/utils.jl @@ -75,7 +75,7 @@ end for file_name in [ "sbm_config.toml", "sbm_gwf_config.toml", - "sbm_river-floodplain-local-inertial_config.toml", + "sbm_river-floodplain-staggered-scheme_config.toml", "sbm_river-land-local-inertial_config.toml", "sbm_gwf_piave_demand_config.toml", ] From 0e0abb9bf8684e601b1c9cc97d1213d6e559e6c6 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 1 Apr 2026 14:34:15 +0200 Subject: [PATCH 25/90] Add missing toml file --- ...er-floodplain-staggered-scheme_config.toml | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 Wflow/test/sbm_river-floodplain-staggered-scheme_config.toml diff --git a/Wflow/test/sbm_river-floodplain-staggered-scheme_config.toml b/Wflow/test/sbm_river-floodplain-staggered-scheme_config.toml new file mode 100644 index 000000000..13d2f5920 --- /dev/null +++ b/Wflow/test/sbm_river-floodplain-staggered-scheme_config.toml @@ -0,0 +1,229 @@ +# This is a TOML configuration file for Wflow. +# Relative file paths are interpreted as being relative to this TOML file. +# Wflow documentation https://deltares.github.io/Wflow.jl/dev/ +# TOML documentation: https://github.com/toml-lang/toml + +dir_input = "data/input" +dir_output = "data/output" + +[logging] +loglevel = "info" + +[time] +calendar = "proleptic_gregorian" +endtime = 2000-02-01T00:00:00 +starttime = 2000-01-01T00:00:00 +time_units = "days since 1900-01-01 00:00:00" +timestepsecs = 86400 + +[state] +path_input = "instates-moselle.nc" +path_output = "outstates-moselle.nc" + +# if listed, the variable must be present in the NetCDF or error +# if not listed, the variable can get a default value if it has one + +[state.variables] +vegetation_canopy_water__depth = "canopystorage" + +soil_water_saturated_zone__depth = "satwaterdepth" +soil_surface__temperature = "tsoil" +soil_layer_water_unsaturated_zone__depth = "ustorelayerdepth" + +snowpack_dry_snow__leq_depth = "snow" +snowpack_liquid_water__depth = "snowwater" + +river_water__depth = "h_river" +river_water__instantaneous_volume_flow_rate = "q_river" + +floodplain_water__instantaneous_volume_flow_rate = "q_floodplain" +floodplain_water__depth = "h_floodplain" + +reservoir_water_surface__elevation = "waterlevel_reservoir" + +subsurface_water__instantaneous_volume_flow_rate = "ssf" + +land_surface_water__instantaneous_volume_flow_rate = "q_land" +land_surface_water__depth = "h_land" + +[input] +path_forcing = "forcing-moselle.nc" +path_static = "staticmaps-moselle.nc" + +# these are not directly part of the model +basin__local_drain_direction = "wflow_ldd" +river_location__mask = "wflow_river" +reservoir_area__count = "wflow_reservoirareas" +reservoir_location__count = "wflow_reservoirlocs" +subbasin_location__count = "wflow_subcatch" +river_gauge__count = "wflow_gauges_grdc" + +[input.forcing] +atmosphere_water__precipitation_volume_flux = "precip" +land_surface_water__potential_evaporation_volume_flux = "pet" +atmosphere_air__temperature = "temp" + +[input.static] +atmosphere_air__snowfall_temperature_threshold = "TT" +atmosphere_air__snowfall_temperature_interval = "TTI" + +land_water_covered__area_fraction = "WaterFrac" + +snowpack__melting_temperature_threshold = "TTM" +snowpack__degree_day_coefficient = "Cfmax" +snowpack__liquid_water_holding_capacity = "WHC" + +soil_layer_water__brooks_corey_exponent = "c" +soil_surface_water__infiltration_reduction_parameter = "cf_soil" +soil_water__vertical_saturated_hydraulic_conductivity_scale_parameter = "f" +compacted_soil_surface_water__infiltration_capacity = "InfiltCapPath" +soil_water__residual_volume_fraction = "thetaR" +soil_water__saturated_volume_fraction = "thetaS" +soil_water_saturated_zone_bottom__max_leakage_volume_flux = "MaxLeakage" +compacted_soil__area_fraction = "PathFrac" +soil_wet_root__sigmoid_function_shape_parameter = "rootdistpar" +soil__thickness = "SoilThickness" + +vegetation_canopy_water__mean_evaporation_to_mean_precipitation_ratio = "EoverR" +vegetation_canopy__light_extinction_coefficient = "Kext" +vegetation__specific_leaf_storage = "Sl" +vegetation_wood_water__storage_capacity = "Swood" +vegetation_root__depth = "RootingDepth" + +river__length = "wflow_riverlength" +river_water_flow__manning_n_parameter = "N_River" +river__slope = "RiverSlope" +river__width = "wflow_riverwidth" +river_bank_water__depth = "RiverDepth" +river_bank_water__elevation = "RiverZ" +floodplain_water__sum_of_volume_per_depth = "floodplain_volume" + +land_surface_water_flow__manning_n_parameter = "N" +land_surface__slope = "Slope" +land_surface__elevation = "wflow_dem" + +reservoir_surface__area = "reservoir_area" +reservoir_water_demand__required_downstream_volume_flow_rate = "ResDemand" +reservoir_water_release_below_spillway__max_volume_flow_rate = "ResMaxRelease" +reservoir_water__max_volume = "ResMaxVolume" +reservoir_water__target_full_volume_fraction = "ResTargetFullFrac" +reservoir_water__target_min_volume_fraction = "ResTargetMinFrac" +reservoir_water_surface__initial_elevation = "waterlevel_reservoir" +reservoir_water__rating_curve_type_count = "outflowfunc" +reservoir_water__storage_curve_type_count = "storfunc" + +subsurface_water__horizontal_to_vertical_saturated_hydraulic_conductivity_ratio = "KsatHorFrac" + +[input.cyclic] +vegetation__leaf_area_index = "LAI" + +[input.static.soil_surface_water__vertical_saturated_hydraulic_conductivity] +netcdf_variable_name = "KsatVer" +scale = 1.0 +offset = 0.0 + +[model] +kinematic_wave__adaptive_time_step_flag = true +river_routing = "local_inertial" +floodplain_1d__flag = true +snow_gravitational_transport__flag = true +cold_start__flag = true +reservoir__flag = true +snow__flag = true +soil_layer__thickness = [100, 300, 800] +type = "sbm" +river_streamorder__min_count = 6 +land_streamorder__min_count = 5 + +[output.netcdf_grid] +path = "output_moselle.nc" + +[output.netcdf_grid.variables] +soil_water_saturated_zone__depth = "satwaterdepth" +soil_surface__temperature = "tsoil" +soil_layer_water_unsaturated_zone__depth = "ustorelayerdepth" +snowpack_dry_snow__leq_depth = "snow" +snowpack_liquid_water__depth = "snowwater" +river_water__depth = "h_river" +river_water__volume_flow_rate = "q_av_river" +reservoir_water__volume = "storage_reservoir" +subsurface_water__volume_flow_rate = "ssf_av" +land_surface_water__volume_flow_rate = "q_av_land" +land_surface_water__depth = "h_land" +land.interception.variables.canopy_storage = "canopystorage" + +[output.netcdf_scalar] +path = "output_scalar_moselle.nc" + +[[output.netcdf_scalar.variable]] +name = "Q" +map = "river_gauge__count" +parameter = "routing.river_flow.variables.q" + +[[output.netcdf_scalar.variable]] +coordinate.x = 6.255 +coordinate.y = 50.012 +name = "temp_coord" +location = "temp_bycoord" +parameter = "atmosphere_air__temperature" + +[[output.netcdf_scalar.variable]] +location = "temp_byindex" +name = "temp_index" +index.x = 100 +index.y = 264 +parameter = "atmosphere_air__temperature" + +[output.csv] +path = "output_moselle.csv" + +[[output.csv.column]] +header = "Q" +parameter = "river_water__volume_flow_rate" +reducer = "maximum" + +[[output.csv.column]] +header = "storage" +index = 1 +parameter = "reservoir_water__volume" + +[[output.csv.column]] +coordinate.x = 6.255 +coordinate.y = 50.012 +header = "temp_bycoord" +parameter = "atmosphere_air__temperature" + +[[output.csv.column]] +coordinate.x = 6.255 +coordinate.y = 50.012 +header = "vwc_layer2_bycoord" +parameter = "land.soil.variables.vwc" +layer = 2 + +[[output.csv.column]] +header = "temp_byindex" +index.x = 100 +index.y = 264 +parameter = "atmosphere_air__temperature" + +[[output.csv.column]] +header = "Q" +map = "river_gauge__count" +parameter = "river_water__volume_flow_rate" + +[[output.csv.column]] +header = "recharge" +map = "subbasin_location__count" +parameter = "soil_water_saturated_zone_top__net_recharge_volume_flux" +reducer = "mean" + +[API] +variables = [ + "river_water__volume_flow_rate", + "soil_water_unsaturated_zone__depth", + "soil_water__transpiration_volume_flux", + "soil_layer_1_water_unsaturated_zone__depth", + "soil_layer_2_water_unsaturated_zone__depth", + "soil_layer_3_water_unsaturated_zone__depth", + "soil_layer_4_water_unsaturated_zone__depth", +] From a6715ebdd779d02ca589be791d54e7b1e479ffc7 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 1 Apr 2026 16:06:27 +0200 Subject: [PATCH 26/90] Some ordering and renaming Edge/node variables and parameters. --- Wflow/src/routing/surface/floodplain.jl | 33 ++++++----- .../surface/surface_staggered_scheme.jl | 56 ++++++++++--------- Wflow/test/routing_process.jl | 19 +++---- 3 files changed, 56 insertions(+), 52 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 4c84e26cd..abb67ca7e 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -113,11 +113,12 @@ end "Struct to store local inertial floodplain flow model parameters" @with_kw struct FloodPlainParameters{P} - profile::P # floodplain profile - mannings_n_at_edge::Vector{Float64} = Float64[] # manning's roughness at edge [s m-1/3] - mannings_n_sq::Vector{Float64} = Float64[] # manning's roughness squared at edge [(s m-1/3)2] - zb_max::Vector{Float64} = Float64[] # maximum bankfull elevation at edge [m] - slope_at_edge::Vector{Float64} = Float64[] # slope at edge [-] + profile::P # floodplain profile + # edge parameters + mannings_n::Vector{Float64} = Float64[] # manning's roughness at edge [s m-1/3] + mannings_n_sq::Vector{Float64} = Float64[] # manning's roughness squared at edge [(s m-1/3)2] + zb_max::Vector{Float64} = Float64[] # maximum bankfull elevation at edge [m] + slope::Vector{Float64} = Float64[] # slope at edge [-] end "Initialize floodplain flow model parameters" @@ -145,13 +146,13 @@ function FloodPlainParameters( append!(mannings_n, mannings_n[index_pit]) # copy to ghost nodes mannings_n_at_edge = compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) - zb_max = compute_value_at_edge(zb_floodplain, nodes_at_edge, n_edges, maximum) + zb_max_edge = compute_value_at_edge(zb_floodplain, nodes_at_edge, n_edges, maximum) if river_routing == RoutingType.local_inertial - mannings_n_sq = mannings_n_at_edge .* mannings_n_at_edge + mannings_n_sq_at_edge = mannings_n_at_edge .* mannings_n_at_edge slope_at_edge = [] elseif river_routing == RoutingType.manning_staggered - mannings_n_sq = [] + mannings_n_sq_at_edge = [] flow_length_at_edge = compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) slope_at_edge = compute_slope_at_edge( @@ -164,10 +165,10 @@ function FloodPlainParameters( parameters = FloodPlainParameters(; profile, - mannings_n_at_edge, - mannings_n_sq, - zb_max, - slope_at_edge, + mannings_n = mannings_n_at_edge, + mannings_n_sq = mannings_n_sq_at_edge, + zb_max = zb_max_edge, + slope = slope_at_edge, ) return parameters end @@ -176,9 +177,7 @@ end @with_kw struct FloodPlainVariables n::Int n_edges::Int - storage::Vector{Float64} = zeros(n) # storage [m³] - h::Vector{Float64} # water depth [m] - error::Vector{Float64} = zeros(n) # error storage [m³] + # edge variables a::Vector{Float64} = zeros(n_edges) # flow area at edge [m²] r::Vector{Float64} = zeros(n_edges) # hydraulic radius at edge [m] hf::Vector{Float64} = zeros(n_edges) # water depth at edge [m] @@ -186,6 +185,10 @@ end q::Vector{Float64} = zeros(n_edges) # discharge at edge [m³ s⁻¹] q_av::Vector{Float64} = zeros(n_edges) # average river discharge at edge [m³ s⁻¹] for model timestep Δt hf_index::Vector{Int} = zeros(Int, n_edges) # edge index with `hf` [-] above depth threshold + # node variables + storage::Vector{Float64} = zeros(n) # storage [m³] + h::Vector{Float64} # water depth [m] + error::Vector{Float64} = zeros(n) # error storage [m³] end "Floodplain flow model" diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 8287c705a..cdf5a1aff 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -6,16 +6,17 @@ active_e::Vector{Int} # active edges [-] froude_limit::Bool = false # if true a check is performed if froude number > 1.0 (algorithm is modified) [-] h_thresh::Float64 = 0.0 # depth threshold for calculating flow [m] + # node parameters zb::Vector{Float64} = Float64[] # river bed elevation [m] - zb_max::Vector{Float64} = Float64[] # maximum channel bed elevation [m] bankfull_storage::Vector{Float64} = Float64[] # bankfull storage [m³] bankfull_depth::Vector{Float64} = Float64[] # bankfull depth [m] + # edge parameters + zb_max::Vector{Float64} = Float64[] # maximum channel bed elevation at edge [m] + mannings_n::Vector{Float64} = Float64[] # Manning's roughness at edge [s m-1/3] mannings_n_sq::Vector{Float64} = Float64[] # Manning's roughness squared at edge [(s m-1/3)2] - mannings_n::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] - mannings_n_at_edge::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] at edge - slope_at_edge::Vector{Float64} = Float64[] # slope at edge [-] - flow_length_at_edge::Vector{Float64} = Float64[] # flow (river) length at edge [m] - flow_width_at_edge::Vector{Float64} = Float64[] # flow (river) width at edge [m] + slope::Vector{Float64} = Float64[] # slope at edge [-] + flow_length::Vector{Float64} = Float64[] # flow (river) length at edge [m] + flow_width::Vector{Float64} = Float64[] # flow (river) width at edge [m] end function get_river_parameters( @@ -114,16 +115,16 @@ function RiverFlowStaggeredParameters( bankfull_storage = bankfull_depth .* flow_width .* flow_length # determine parameters at edges - zb_max = compute_value_at_edge(zb, nodes_at_edge, n_edges, maximum) + zb_max_at_edge = compute_value_at_edge(zb, nodes_at_edge, n_edges, maximum) flow_width_at_edge = compute_value_at_edge(flow_width, nodes_at_edge, n_edges, minimum) flow_length_at_edge = compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) mannings_n_at_edge = compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) if river_routing == RoutingType.local_inertial - mannings_n_sq = mannings_n_at_edge .* mannings_n_at_edge + mannings_n_sq_at_edge = mannings_n_at_edge .* mannings_n_at_edge slope_at_edge = [] elseif river_routing == RoutingType.manning_staggered - mannings_n_sq = [] + mannings_n_sq_at_edge = [] slope_at_edge = compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) end @@ -136,15 +137,14 @@ function RiverFlowStaggeredParameters( froude_limit, h_thresh = waterdepth_threshold, zb, - zb_max, bankfull_storage, bankfull_depth, - mannings_n, - mannings_n_at_edge, - mannings_n_sq, - slope_at_edge, - flow_length_at_edge, - flow_width_at_edge, + zb_max = zb_max_at_edge, + mannings_n = mannings_n_at_edge, + mannings_n_sq = mannings_n_sq_at_edge, + slope = slope_at_edge, + flow_length = flow_length_at_edge, + flow_width = flow_width_at_edge, ) return parameters end @@ -153,17 +153,19 @@ end @with_kw struct RiverFlowStaggeredVariables <: AbstractRiverFlowVariables n::Int n_edges::Int + # edge variables q::Vector{Float64} = zeros(n_edges) # river discharge at edge (subgrid channel) [m³ s⁻¹] q0::Vector{Float64} = Float64[] # river discharge at edge (subgrid channel) at previous time step [m³ s⁻¹] q_av::Vector{Float64} # average river channel (+ floodplain) discharge at edge [m³ s⁻¹] (model timestep Δt) q_channel_av::Vector{Float64} # average river channel discharge at edge [m³ s⁻¹] (for model timestep Δt) - h::Vector{Float64} # water depth [m] zs_max::Vector{Float64} = zeros(n_edges) # maximum water elevation at edge [m] zs_src::Vector{Float64} = zeros(n_edges) # water elevation of source node of edge [m] zs_dst::Vector{Float64} = zeros(n_edges) # water elevation of downstream node of edge [m] hf::Vector{Float64} = zeros(n_edges) # water depth at edge [m] a::Vector{Float64} = zeros(n_edges) # flow area at edge [m²] r::Vector{Float64} = zeros(n_edges) # wetted perimeter at edge [m] + # node variables + h::Vector{Float64} # water depth [m] storage::Vector{Float64} = zeros(n) # river storage [m³] error::Vector{Float64} = zeros(n) # error storage [m³] end @@ -322,8 +324,8 @@ function update_river_channel_flow!( river_v.zs_max[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) river_v.hf[i] = (river_v.zs_max[i] - river_p.zb_max[i]) - river_v.a[i] = river_p.flow_width_at_edge[i] * river_v.hf[i] # flow area (rectangular channel) - river_v.r[i] = river_v.a[i] / (river_p.flow_width_at_edge[i] + 2.0 * river_v.hf[i]) # hydraulic radius (rectangular channel) + river_v.a[i] = river_p.flow_width[i] * river_v.hf[i] # flow area (rectangular channel) + river_v.r[i] = river_v.a[i] / (river_p.flow_width[i] + 2.0 * river_v.hf[i]) # hydraulic radius (rectangular channel) river_v.q[i] = ifelse( river_v.hf[i] > river_p.h_thresh, @@ -334,7 +336,7 @@ function update_river_channel_flow!( river_v.hf[i], river_v.a[i], river_v.r[i], - river_p.flow_length_at_edge[i], + river_p.flow_length[i], river_p.mannings_n_sq[i], river_p.froude_limit, dt, @@ -373,15 +375,15 @@ function update_river_channel_flow!( river_v.zs_max[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) river_v.hf[i] = (river_v.zs_max[i] - river_p.zb_max[i]) - river_v.a[i] = river_p.flow_width_at_edge[i] * river_v.hf[i] # flow area (rectangular channel) - river_v.r[i] = river_v.a[i] / (river_p.flow_width_at_edge[i] + 2.0 * river_v.hf[i]) # hydraulic radius (rectangular channel) + river_v.a[i] = river_p.flow_width[i] * river_v.hf[i] # flow area (rectangular channel) + river_v.r[i] = river_v.a[i] / (river_p.flow_width[i] + 2.0 * river_v.hf[i]) # hydraulic radius (rectangular channel) river_v.q[i] = ifelse( river_v.hf[i] > river_p.h_thresh, manning_flow( - river_p.mannings_n_at_edge[i], + river_p.mannings_n[i], river_v.r[i], - river_p.slope_at_edge[i], + river_p.slope[i], river_v.a[i], ), 0.0, @@ -472,7 +474,7 @@ function update_floodplain_flow!( floodplain_v.hf[i], floodplain_v.a[i], floodplain_v.r[i], - river_p.flow_length_at_edge[i], + river_p.flow_length[i], floodplain_p.mannings_n_sq[i], river_p.froude_limit, dt, @@ -571,9 +573,9 @@ function update_floodplain_flow!( floodplain_v.q[i] = if floodplain_v.a[i] > 1.0e-05 manning_flow( - floodplain_p.mannings_n_at_edge[i], + floodplain_p.mannings_n[i], floodplain_v.r[i], - floodplain_p.slope_at_edge[i], + floodplain_p.slope[i], floodplain_v.a[i], ) else diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index e6dab58de..5e4c834c7 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -304,20 +304,20 @@ end n_edges = ne(graph) # determine z, width, length and manning's n at edges - zb_max = fill(0.0, n_edges) + zb_max_at_edge = fill(0.0, n_edges) width_at_edge = fill(0.0, n_edges) length_at_edge = fill(0.0, n_edges) - mannings_n_sq = fill(0.0, n_edges) + mannings_n_sq_at_edge = fill(0.0, n_edges) for i in 1:n_edges - zb_max[i] = max(zb[nodes_at_edge.src[i]], zb[nodes_at_edge.dst[i]]) + zb_max_at_edge[i] = max(zb[nodes_at_edge.src[i]], zb[nodes_at_edge.dst[i]]) width_at_edge[i] = min(width[nodes_at_edge.dst[i]], width[nodes_at_edge.src[i]]) length_at_edge[i] = 0.5 * (dl[nodes_at_edge.dst[i]] + dl[nodes_at_edge.src[i]]) - mannings_n = + mannings_n_at_edge = ( n_river[nodes_at_edge.dst[i]] * dl[nodes_at_edge.dst[i]] + n_river[nodes_at_edge.src[i]] * dl[nodes_at_edge.src[i]] ) / (dl[nodes_at_edge.dst[i]] + dl[nodes_at_edge.src[i]]) - mannings_n_sq[i] = mannings_n * mannings_n + mannings_n_sq_at_edge[i] = mannings_n_at_edge * mannings_n_at_edge end river_network = Wflow.NetworkRiver(; @@ -347,11 +347,10 @@ end active_n = collect(1:(n - 1)), active_e = collect(1:n_edges), h_thresh, - zb_max, - mannings_n_sq, - mannings_n = n_river, - flow_width_at_edge = width_at_edge, - flow_length_at_edge = length_at_edge, + zb_max = zb_max_at_edge, + mannings_n_sq = mannings_n_sq_at_edge, + flow_width = width_at_edge, + flow_length = length_at_edge, bankfull_storage = fill(Wflow.MISSING_VALUE, n), bankfull_depth = fill(Wflow.MISSING_VALUE, n), zb, From 5a05a012e9295d7ec06497118dd8098bb560e004 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 7 Apr 2026 10:40:07 +0200 Subject: [PATCH 27/90] Add floodplain model for kinematic wave --- Wflow/src/routing/surface/floodplain.jl | 70 ++++++++++++++++---- Wflow/src/routing/surface/surface_kinwave.jl | 6 +- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index abb67ca7e..a3d5d5064 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -1,4 +1,6 @@ abstract type AbstractFloodPlainModel end +abstract type AbstractFloodPlainParameters end +abstract type AbstractFloodPlainVariables end """ FloodPlainProfile @@ -20,7 +22,7 @@ function FloodPlainProfile( dataset::NCDataset, config::Config, domain::DomainRiver; - index_pit::Vector{Int} = [], + index_pit::Vector{Int} = Int[], ) (; indices) = domain.network (; flow_width, flow_length) = domain.parameters @@ -111,8 +113,8 @@ function FloodPlainProfile( return profile end -"Struct to store local inertial floodplain flow model parameters" -@with_kw struct FloodPlainParameters{P} +"Struct to store floodplain flow model parameters on a staggered grid" +@with_kw struct FloodPlainStaggeredParameters{P} <: AbstractFloodPlainParameters profile::P # floodplain profile # edge parameters mannings_n::Vector{Float64} = Float64[] # manning's roughness at edge [s m-1/3] @@ -121,8 +123,8 @@ end slope::Vector{Float64} = Float64[] # slope at edge [-] end -"Initialize floodplain flow model parameters" -function FloodPlainParameters( +"Initialize floodplain flow model parameters on a staggered grid" +function FloodPlainStaggeredParameters( dataset::NCDataset, config::Config, domain::DomainRiver, @@ -163,7 +165,7 @@ function FloodPlainParameters( ) end - parameters = FloodPlainParameters(; + parameters = FloodPlainStaggeredParameters(; profile, mannings_n = mannings_n_at_edge, mannings_n_sq = mannings_n_sq_at_edge, @@ -173,8 +175,8 @@ function FloodPlainParameters( return parameters end -"Struct to store floodplain flow model variables" -@with_kw struct FloodPlainVariables +"Struct to store floodplain flow model variables on a staggered grid" +@with_kw struct FloodPlainStaggeredVariables <: AbstractFloodPlainVariables n::Int n_edges::Int # edge variables @@ -192,10 +194,29 @@ end end "Floodplain flow model" -@with_kw struct FloodPlainModel{T, P} <: AbstractFloodPlainModel +@with_kw struct FloodPlainModel{ + T <: AbstractRoutingMethod, + P <: AbstractFloodPlainParameters, + V <: AbstractFloodPlainVariables, +} <: AbstractFloodPlainModel routing_method::T - parameters::FloodPlainParameters{P} - variables::FloodPlainVariables + parameters::P + variables::V +end + +"Struct to store floodplain parameters" +@with_kw struct FloodPlainParameters{P} <: AbstractFloodPlainParameters + profile::P # floodplain profile + mannings_n::Vector{Float64} = Float64[] # manning's roughness[s m-1/3] +end + +"Struct to store floodplain variables" +@with_kw struct FloodPlainVariables <: AbstractFloodPlainVariables + n::Int + q::Vector{Float64} = zeros(n) # discharge [m³ s⁻¹] + q_av::Vector{Float64} = zeros(n) # average river discharge [m³ s⁻¹] for model timestep Δt + storage::Vector{Float64} = zeros(n) # storage [m³] + h::Vector{Float64} = zeros(n) # water depth [m] end "Determine the initial floodplain storage" @@ -271,7 +292,7 @@ function flood_depth( return flood_depth end -"Initialize floodplain geometry and model variables and parameters" +"Initialize floodplain geometry, model variables and parameters on staggered grid" function FloodPlainModel( dataset::NCDataset, config::Config, @@ -283,7 +304,8 @@ function FloodPlainModel( n = length(indices) index_pit = findall(x -> x == 5, local_drain_direction) n_edges = ne(graph) - parameters = FloodPlainParameters(dataset, config, domain, zb_floodplain, index_pit) + parameters = + FloodPlainStaggeredParameters(dataset, config, domain, zb_floodplain, index_pit) if river_routing == RoutingType.local_inertial q0 = zeros(n_edges) routing_method = LocalInertial() @@ -291,8 +313,28 @@ function FloodPlainModel( q0 = [] routing_method = ManningStaggered() end - variables = FloodPlainVariables(; n, n_edges, q0, h = zeros(n + length(index_pit))) + variables = + FloodPlainStaggeredVariables(; n, n_edges, q0, h = zeros(n + length(index_pit))) floodplain_model = FloodPlainModel(; routing_method, parameters, variables) return floodplain_model end + +function FloodPlainModel(dataset::NCDataset, config::Config, domain::DomainRiver) + (; indices) = domain.network + n = length(indices) + profile = FloodPlainProfile(dataset, config, domain) + mannings_n = ncread( + dataset, + config, + "floodplain_water_flow__manning_n_parameter"; + sel = indices, + defaults = 0.072, + type = Float64, + ) + parameters = FloodPlainParameters(; profile, mannings_n) + variables = FloodPlainVariables(; n) + floodplain_model = + FloodPlainModel(; routing_method = KinematicWave(), parameters, variables) + return floodplain_model +end diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index ebd0720bb..cfec46d7f 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -109,7 +109,11 @@ function init_kinematic_wave_river_flow( variables = FlowVariables(; n) parameters = RiverFlowParameters(dataset, config, domain) boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir) - floodplain = nothing + if config.model.floodplain_1d__flag + floodplain = FloodPlainModel(dataset, config, domain) + else + floodplain = nothing + end routing_method = KinematicWave() river_flow = RiverFlowModel(; From 2f6ad3831924a817dc0b7bbeb1751cef1e556681 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 7 Apr 2026 13:14:10 +0200 Subject: [PATCH 28/90] Simplify dispatching on floodplain model --- Wflow/src/routing/surface/surface_flow.jl | 2 +- .../src/routing/surface/surface_staggered_scheme.jl | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Wflow/src/routing/surface/surface_flow.jl b/Wflow/src/routing/surface/surface_flow.jl index 8d59598a1..e366e2122 100644 --- a/Wflow/src/routing/surface/surface_flow.jl +++ b/Wflow/src/routing/surface/surface_flow.jl @@ -28,9 +28,9 @@ end @with_kw struct RiverFlowModel{ T <: AbstractRoutingMethod, + F <: Union{AbstractFloodPlainModel, Nothing}, P <: AbstractRiverFlowParameters, V <: AbstractRiverFlowVariables, - F <: Union{AbstractFloodPlainModel, Nothing}, A <: AbstractAllocationModel, } <: AbstractRiverFlowModel routing_method::T diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index cdf5a1aff..46f9c42cb 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -401,10 +401,10 @@ end Update floodplain flow for the local inertial river flow model. """ function update_floodplain_flow!( - river_flow_model::RiverFlowModel{T, P, V, F}, + river_flow_model::RiverFlowModel{T, F}, domain::DomainRiver, dt::Float64, -) where {T <: LocalInertial, P, V, F <: FloodPlainModel{<:LocalInertial}} +) where {T <: LocalInertial, F <: FloodPlainModel{<:LocalInertial}} (; nodes_at_edge) = domain.network (; flow_width) = domain.parameters @@ -506,10 +506,10 @@ end Update floodplain flow for the manning river flow model on a staggered grid. """ function update_floodplain_flow!( - river_flow_model::RiverFlowModel{T, P, V, F}, + river_flow_model::RiverFlowModel{T, F}, domain::DomainRiver, dt::Float64, -) where {T <: ManningStaggered, P, V, F <: FloodPlainModel{<:ManningStaggered}} +) where {T <: ManningStaggered, F <: FloodPlainModel{<:ManningStaggered}} (; nodes_at_edge) = domain.network (; flow_width) = domain.parameters @@ -592,10 +592,10 @@ function update_floodplain_flow!( end update_floodplain_flow!( - model::RiverFlowModel{T, P, V, F}, + model::RiverFlowModel{T, F}, domain::DomainRiver, dt::Float64, -) where {T <: AbstractStaggeredRoutingMethod, P, V, F <: Nothing} = nothing +) where {T <: AbstractStaggeredRoutingMethod, F <: Nothing} = nothing """ Update reservoir boundary conditions for a river flow model on a staggered grid. From 7f2316dbbe843221459c02995ec3ffab9ee941bb Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 7 Apr 2026 15:38:16 +0200 Subject: [PATCH 29/90] Init manning flow parameters Remove update of `alpha` at each model timestep as dynamic input of manning roughness is not allowed. --- Wflow/src/routing/surface/surface_flow.jl | 11 ++--- Wflow/src/routing/surface/surface_kinwave.jl | 43 +++++++++----------- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/Wflow/src/routing/surface/surface_flow.jl b/Wflow/src/routing/surface/surface_flow.jl index e366e2122..10fb40917 100644 --- a/Wflow/src/routing/surface/surface_flow.jl +++ b/Wflow/src/routing/surface/surface_flow.jl @@ -17,13 +17,10 @@ end "Struct for storing Manning flow parameters" @with_kw struct ManningFlowParameters - n::Int - beta::Float64 # constant in Manning's equation [-] - slope::Vector{Float64} # Slope [m m⁻¹] - mannings_n::Vector{Float64} # Manning's roughness [s m⁻⅓] - alpha_pow::Float64 # Used in the power part of alpha [-] - alpha_term::Vector{Float64} = fill(MISSING_VALUE, n) # Term used in computation of alpha [-] - alpha::Vector{Float64} = fill(MISSING_VALUE, n) # Constant in momentum equation A = alpha*Q^beta, based on Manning's equation [s3/5 m1/5] + beta::Float64 # constant in Manning's equation [-] + slope::Vector{Float64} # Slope [m m⁻¹] + mannings_n::Vector{Float64} # Manning's roughness [s m⁻⅓] + alpha::Vector{Float64} # Constant in momentum equation A = alpha*Q^beta, based on Manning's equation [s3/5 m1/5] end @with_kw struct RiverFlowModel{ diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index cfec46d7f..549c419be 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -11,15 +11,18 @@ end "Initialize Manning flow parameters" -function ManningFlowParameters(mannings_n::Vector{Float64}, slope::Vector{Float64}) - n = length(slope) - parameters = ManningFlowParameters(; - n, - beta = Float64(0.6), - slope, - mannings_n, - alpha_pow = Float64((2.0 / 3.0) * 0.6), - ) +function ManningFlowParameters( + mannings_n::Vector{Float64}, + slope::Vector{Float64}, + wetted_perimeter::Vector{Float64}, +) + beta = Float64(0.6) + hydraulic_radius_pow = Float64(2.0 / 3.0) + alpha_term = @. pow(mannings_n / sqrt(slope), beta) + alpha_pow = hydraulic_radius_pow * beta + alpha = @. alpha_term * pow(wetted_perimeter, alpha_pow) + + parameters = ManningFlowParameters(; beta, slope, mannings_n, alpha) return parameters end @@ -64,7 +67,9 @@ function RiverFlowParameters(dataset::NCDataset, config::Config, domain::DomainR type = Float64, ) - flow_params = ManningFlowParameters(mannings_n, slope) + # use fixed wetted perimeter based on 0.5 * bankfull_depth + wetted_perimeter = flow_width + bankfull_depth + flow_params = ManningFlowParameters(mannings_n, slope, wetted_perimeter) bankfull_storage = bankfull_depth .* flow_length .* flow_width parameters = RiverFlowParameters(; flow = flow_params, bankfull_depth, bankfull_storage) return parameters @@ -160,7 +165,7 @@ function init_kinematic_wave_overland_flow( domain::DomainLand, ) (; indices) = domain.network - (; slope) = domain.parameters + (; slope, surface_flow_width) = domain.parameters mannings_n = ncread( dataset, config, @@ -174,7 +179,7 @@ function init_kinematic_wave_overland_flow( timestepping = init_kinematic_wave_timestepping(config, n; domain = "land") variables = OverLandFlowVariables(; n) - parameters = ManningFlowParameters(mannings_n, slope) + parameters = ManningFlowParameters(mannings_n, slope, surface_flow_width) boundary_conditions = LandFlowBC(; n) routing_method = KinematicWave() @@ -314,14 +319,10 @@ function update_overland_flow_model!( dt::Float64, ) (; inwater) = overland_flow_model.boundary_conditions - (; alpha_term, mannings_n, beta, alpha_pow, alpha) = overland_flow_model.parameters - (; surface_flow_width, flow_length, slope) = domain.parameters + (; flow_length) = domain.parameters (; q_av, qlat, qin_av, to_river) = overland_flow_model.variables (; adaptive) = overland_flow_model.timestepping - @. alpha_term = pow(mannings_n / sqrt(slope), beta) - # use fixed alpha value based flow width - @. alpha = alpha_term * pow(surface_flow_width, alpha_pow) @. qlat = inwater / flow_length q_av .= 0.0 @@ -461,17 +462,11 @@ function update_river_flow_model!( clock::Clock, ) (; reservoir, inwater) = river_flow_model.boundary_conditions - (; alpha_term, mannings_n, beta, alpha_pow, alpha, bankfull_depth) = - river_flow_model.parameters - (; slope, flow_width, flow_length) = domain.river.parameters + (; flow_length) = domain.river.parameters (; qlat, qin_av) = river_flow_model.variables (; adaptive) = river_flow_model.timestepping - @. alpha_term = pow(mannings_n / sqrt(slope), beta) - # use fixed alpha value based on 0.5 * bankfull_depth - @. alpha = alpha_term * pow(flow_width + bankfull_depth, alpha_pow) @. qlat = inwater / flow_length - set_flow_vars!(river_flow_model) qin_av .= 0.0 set_reservoir_vars!(reservoir) From a1b1d4d9946395aa873ca7c3f4f8cced091333bf Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 8 Apr 2026 08:48:38 +0200 Subject: [PATCH 30/90] Use separate structs river and overland flow variables For kinematic wave routing. --- Wflow/src/routing/surface/surface_kinwave.jl | 45 ++++++++++---------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index 549c419be..b222743e6 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -1,13 +1,14 @@ -"Struct for storing (shared) variables for river and overland flow models" -@with_kw struct FlowVariables <: AbstractRiverFlowVariables +"Struct for storing variables for river flow model" +@with_kw struct RiverFlowVariables <: AbstractRiverFlowVariables n::Int - q::Vector{Float64} = zeros(n) # Discharge [m³ s⁻¹] + q::Vector{Float64} = zeros(n) # River discharge [m³ s⁻¹] qlat::Vector{Float64} = zeros(n) # Lateral inflow per unit length [m² s⁻¹] - qin::Vector{Float64} = zeros(n) # Inflow from upstream cells [m³ s⁻¹] - qin_av::Vector{Float64} = zeros(n) # Average inflow from upstream cells [m³ s⁻¹] for model timestep Δt - q_av::Vector{Float64} = zeros(n) # Average discharge [m³ s⁻¹] for model timestep Δt - storage::Vector{Float64} = zeros(n) # Kinematic wave storage [m³] (based on water depth h) - h::Vector{Float64} = zeros(n) # Water depth [m] + qin::Vector{Float64} = zeros(n) # River inflow from upstream cells [m³ s⁻¹] + qin_av::Vector{Float64} = zeros(n) # Average river inflow from upstream cells [m³ s⁻¹] for model timestep Δt + q_av::Vector{Float64} # Average river channel (+ floodplain) discharge discharge [m³ s⁻¹] for model timestep Δt + q_channel_av::Vector{Float64} # Average river channel discharge [m³ s⁻¹] (for model timestep Δt) + storage::Vector{Float64} = zeros(n) # River kinematic wave storage [m³] (based on water depth h) + h::Vector{Float64} = zeros(n) # River water depth [m] end "Initialize Manning flow parameters" @@ -111,7 +112,12 @@ function init_kinematic_wave_river_flow( allocation = do_water_demand(config) ? AllocationRiverModel(; n) : NoAllocationRiverModel(n) - variables = FlowVariables(; n) + q_av = zeros(n) + variables = RiverFlowVariables(; + n, + q_av, + q_channel_av = config.model.floodplain_1d__flag ? zeros(n) : q_av, + ) parameters = RiverFlowParameters(dataset, config, domain) boundary_conditions = RiverFlowBC(dataset, config, domain.network, reservoir) if config.model.floodplain_1d__flag @@ -137,19 +143,14 @@ end "Struct for storing overland flow model variables" @with_kw struct OverLandFlowVariables <: AbstractOverlandFlowVariables n::Int - flow::FlowVariables = FlowVariables(; n) - to_river::Vector{Float64} = zeros(n) # Part of overland flow [m³ s⁻¹] that flows to the river -end - -"Overload `getproperty` for overland flow model variables" -function Base.getproperty(v::OverLandFlowVariables, s::Symbol) - if s === :to_river - getfield(v, s) - elseif s === :flow - getfield(v, :flow) - else - getfield(getfield(v, :flow), s) - end + q::Vector{Float64} = zeros(n) # Overland discharge [m³ s⁻¹] + qlat::Vector{Float64} = zeros(n) # Lateral inflow per unit length [m² s⁻¹] + qin::Vector{Float64} = zeros(n) # Overland inflow from upstream cells [m³ s⁻¹] + qin_av::Vector{Float64} = zeros(n) # Average overland inflow from upstream cells [m³ s⁻¹] for model timestep Δt + q_av::Vector{Float64} = zeros(n) # Average overland discharge [m³ s⁻¹] for model timestep Δt + storage::Vector{Float64} = zeros(n) # Overland kinematic wave storage [m³] (based on water depth h) + h::Vector{Float64} = zeros(n) # Overland water depth [m] + to_river::Vector{Float64} = zeros(n) # Part of overland flow [m³ s⁻¹] that flows to the river end "Struct for storing overland flow model boundary conditions" From e193cdce04d08816256408079c49a17482580998 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 13 Apr 2026 10:39:18 +0200 Subject: [PATCH 31/90] Add kinematic wave flood routing For routing type `KinematicWave` using a compound channel (not separate routing for floodplain) with a compound `alpha` value. --- Wflow/src/routing/surface/floodplain.jl | 14 +++ Wflow/src/routing/surface/surface_flow.jl | 2 + Wflow/src/routing/surface/surface_kinwave.jl | 111 +++++++++++++++++-- 3 files changed, 120 insertions(+), 7 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index a3d5d5064..7be0fa11c 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -292,6 +292,20 @@ function flood_depth( return flood_depth end +function flood_depth_storage( + profile::FloodPlainProfile, + flood_area::Float64, + flow_length::Float64, + i::Int, +) + i1, i2 = interpolation_indices(flood_area, @view profile.a[:, i]) + ΔA = (flood_area - profile.a[i1, i]) + dh = ΔA / profile.width[i2, i] + flood_depth = profile.depth[i1] + dh + flood_storage = profile.storage[i1] + ΔA * flow_length + return flood_depth, flood_storage +end + "Initialize floodplain geometry, model variables and parameters on staggered grid" function FloodPlainModel( dataset::NCDataset, diff --git a/Wflow/src/routing/surface/surface_flow.jl b/Wflow/src/routing/surface/surface_flow.jl index 10fb40917..603e046f4 100644 --- a/Wflow/src/routing/surface/surface_flow.jl +++ b/Wflow/src/routing/surface/surface_flow.jl @@ -20,6 +20,8 @@ end beta::Float64 # constant in Manning's equation [-] slope::Vector{Float64} # Slope [m m⁻¹] mannings_n::Vector{Float64} # Manning's roughness [s m⁻⅓] + alpha_pow::Float64 # Used in the power part of alpha [-] + alpha_term::Vector{Float64} # Term used in computation of alpha [-] alpha::Vector{Float64} # Constant in momentum equation A = alpha*Q^beta, based on Manning's equation [s3/5 m1/5] end diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index b222743e6..ca572aecd 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -23,7 +23,8 @@ function ManningFlowParameters( alpha_pow = hydraulic_radius_pow * beta alpha = @. alpha_term * pow(wetted_perimeter, alpha_pow) - parameters = ManningFlowParameters(; beta, slope, mannings_n, alpha) + parameters = + ManningFlowParameters(; beta, slope, mannings_n, alpha_pow, alpha_term, alpha) return parameters end @@ -32,6 +33,7 @@ end flow::ManningFlowParameters bankfull_depth::Vector{Float64} # Bankfull water level [m] bankfull_storage::Vector{Float64} # Bankfull storage [m³] + bankfull_flow::Vector{Float64} # Bankfull discharge [m³ s⁻¹] end "Overload `getproperty` for river flow model parameters" @@ -40,6 +42,8 @@ function Base.getproperty(v::RiverFlowParameters, s::Symbol) getfield(v, s) elseif s === :bankfull_storage getfield(v, s) + elseif s === :bankfull_flow + getfield(v, s) elseif s === :flow getfield(v, :flow) else @@ -72,7 +76,19 @@ function RiverFlowParameters(dataset::NCDataset, config::Config, domain::DomainR wetted_perimeter = flow_width + bankfull_depth flow_params = ManningFlowParameters(mannings_n, slope, wetted_perimeter) bankfull_storage = bankfull_depth .* flow_length .* flow_width - parameters = RiverFlowParameters(; flow = flow_params, bankfull_depth, bankfull_storage) + if config.model.floodplain_1d__flag + wetted_perimeter = @. flow_width + 2.0 * bankfull_depth + alpha = @. flow_params.alpha_term * pow(wetted_perimeter, flow_params.alpha_pow) + bankfull_flow = @. pow(bankfull_depth * flow_width / alpha, 1.0 / flow_params.beta) + else + bankfull_flow = Float64[] + end + parameters = RiverFlowParameters(; + flow = flow_params, + bankfull_depth, + bankfull_storage, + bankfull_flow, + ) return parameters end @@ -364,9 +380,10 @@ function kinwave_river_update!( (; reservoir, external_inflow, actual_external_abstraction_av, abstraction) = river_flow_model.boundary_conditions - (; beta, alpha) = river_flow_model.parameters + (; beta, alpha, bankfull_depth, bankfull_flow) = river_flow_model.parameters (; flow_width, flow_length) = domain.parameters - (; h, q, q_av, storage, qin, qin_av, qlat) = river_flow_model.variables + (; h, q, q_av, q_channel_av, storage, qin, qin_av, qlat) = river_flow_model.variables + (; floodplain) = river_flow_model if !isnothing(reservoir) res_bc = reservoir.boundary_conditions @@ -442,9 +459,34 @@ function kinwave_river_update!( end # update h and storage crossarea = alpha[v] * pow(q[v], beta) - h[v] = crossarea / flow_width[v] - storage[v] = flow_length[v] * flow_width[v] * h[v] - + if isnothing(floodplain) + h[v] = crossarea / flow_width[v] + storage[v] = flow_length[v] * flow_width[v] * h[v] + else + bankfull_area = flow_width[v] * bankfull_depth[v] + if crossarea > bankfull_area + floodarea = crossarea - bankfull_area + flood_depth, flood_storage = flood_depth_storage( + floodplain.parameters.profile, + floodarea, + flow_length[v], + v, + ) + h[v] = bankfull_depth[v] + flood_depth + storage[v] = bankfull_area * flow_length[v] + flood_storage + floodplain_flow = q[v] - bankfull_flow[v] + else + h[v] = crossarea / flow_width[v] + storage[v] = flow_length[v] * flow_width[v] * h[v] + flood_storage = 0.0 + flood_depth = 0.0 + floodplain_flow = 0.0 + end + floodplain.variables.storage[v] = flood_storage + floodplain.variables.h[v] = flood_depth + floodplain.variables.q[v] = floodplain_flow + floodplain.variables.q_av[v] += floodplain.variables.q[v] * dt + end # average variables (here accumulated for model timestep Δt) q_av[v] += q[v] * dt qin_av[v] += qin[v] * dt @@ -453,6 +495,51 @@ function kinwave_river_update!( end end +function update_alpha_parameter!( + river_flow_model::RiverFlowModel{T, F}, + parameters::RiverParameters, +) where {T <: KinematicWave, F <: FloodPlainModel} + (; h) = river_flow_model.variables + (; bankfull_depth, alpha, mannings_n, beta, alpha_term, alpha_pow) = + river_flow_model.parameters + (; flow_width, slope) = parameters + + floodplain_p = river_flow_model.floodplain.parameters + floodplain_v = river_flow_model.floodplain.variables + + for i in eachindex(h) + if h[i] > bankfull_depth[i] + wp_channel = 2.0 * bankfull_depth[i] + flow_width[i] + i0 = 0 + for k in eachindex(floodplain_p.profile.depth) + i0 += 1 * (floodplain_p.profile.depth[k] <= floodplain_v.h[i]) + end + i1 = max(i0, 1) + i2 = ifelse(i1 == length(floodplain_p.profile.depth), i1, i1 + 1) + wp_floodplain = wetted_perimeter( + floodplain_p.profile.p[i1, i], + floodplain_p.profile.depth[i1], + floodplain_v.h[i], + ) + wp_combined = wp_channel + wp_floodplain + mannings_n_combined = pow( + wp_floodplain/wp_combined * pow(floodplain_p.mannings_n[i], 1.5) + + wp_channel/wp_combined * pow(mannings_n[i], 1.5), + 2.0/3.0, + ) + alpha[i] = alpha_term[i] * pow(wp_combined, alpha_pow) + else + wp_channel = 2.0 * h[i] + flow_width[i] + alpha[i] = alpha_term[i] * pow(wp_channel, alpha_pow) + end + end +end + +update_alpha_parameter!( + river_flow_model::RiverFlowModel{T, F}, + parameters::RiverParameters, +) where {T <: KinematicWave, F <: Nothing} = nothing + """ Update river flow model `RiverFlowModel{<:KinematicWave}` for a single timestep `dt`. Timestepping within `dt` is either with a fixed timestep `dt_fixed` or adaptive. @@ -462,6 +549,7 @@ function update_river_flow_model!( domain::Domain, clock::Clock, ) + (; floodplain) = river_flow_model (; reservoir, inwater) = river_flow_model.boundary_conditions (; flow_length) = domain.river.parameters (; qlat, qin_av) = river_flow_model.variables @@ -472,10 +560,14 @@ function update_river_flow_model!( qin_av .= 0.0 set_reservoir_vars!(reservoir) update_index_hq!(reservoir, clock) + if !isnothing(floodplain) + floodplain.variables.q_av .= 0.0 + end dt = tosecond(clock.dt) t = 0.0 while t < dt + update_alpha_parameter!(river_flow_model, domain.river.parameters) dt_s = adaptive ? stable_timestep(river_flow_model, flow_length, 0.05) : river_flow_model.timestepping.dt_fixed @@ -487,6 +579,11 @@ function update_river_flow_model!( average_reservoir_vars!(reservoir, dt) average_flow_vars!(river_flow_model, dt) qin_av ./= dt + if !isnothing(floodplain) + floodplain.variables.q_av ./= dt + river_flow_model.variables.q_channel_av .= + river_flow_model.variables.q_av .- floodplain.variables.q_av + end return nothing end From 33ab15457d7ba5390506f6713a8b7fa5049cc2ba Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 13 Apr 2026 12:26:40 +0200 Subject: [PATCH 32/90] Use function for wetted perimeter channel --- Wflow/src/routing/surface/surface_flow.jl | 5 +++++ Wflow/src/routing/surface/surface_kinwave.jl | 6 +++--- Wflow/src/routing/surface/surface_staggered_scheme.jl | 9 ++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Wflow/src/routing/surface/surface_flow.jl b/Wflow/src/routing/surface/surface_flow.jl index 603e046f4..b02f7e06f 100644 --- a/Wflow/src/routing/surface/surface_flow.jl +++ b/Wflow/src/routing/surface/surface_flow.jl @@ -53,3 +53,8 @@ end parameters::P variables::V end + +function wetted_perimeter_channel(h::Float64, flow_width::Float64) + channel_perimeter = 2.0 * h + flow_width + return channel_perimeter +end diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index ca572aecd..6a52f2326 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -77,7 +77,7 @@ function RiverFlowParameters(dataset::NCDataset, config::Config, domain::DomainR flow_params = ManningFlowParameters(mannings_n, slope, wetted_perimeter) bankfull_storage = bankfull_depth .* flow_length .* flow_width if config.model.floodplain_1d__flag - wetted_perimeter = @. flow_width + 2.0 * bankfull_depth + wetted_perimeter = @. wetted_perimeter_channel(bankfull_depth, flow_width) alpha = @. flow_params.alpha_term * pow(wetted_perimeter, flow_params.alpha_pow) bankfull_flow = @. pow(bankfull_depth * flow_width / alpha, 1.0 / flow_params.beta) else @@ -509,7 +509,7 @@ function update_alpha_parameter!( for i in eachindex(h) if h[i] > bankfull_depth[i] - wp_channel = 2.0 * bankfull_depth[i] + flow_width[i] + wp_channel = wetted_perimeter_channel(bankfull_depth[i], flow_width[i]) i0 = 0 for k in eachindex(floodplain_p.profile.depth) i0 += 1 * (floodplain_p.profile.depth[k] <= floodplain_v.h[i]) @@ -529,7 +529,7 @@ function update_alpha_parameter!( ) alpha[i] = alpha_term[i] * pow(wp_combined, alpha_pow) else - wp_channel = 2.0 * h[i] + flow_width[i] + wp_channel = wetted_perimeter_channel(h[i], flow_width[i]) alpha[i] = alpha_term[i] * pow(wp_channel, alpha_pow) end end diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 46f9c42cb..d8d5e7126 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -325,7 +325,8 @@ function update_river_channel_flow!( river_v.hf[i] = (river_v.zs_max[i] - river_p.zb_max[i]) river_v.a[i] = river_p.flow_width[i] * river_v.hf[i] # flow area (rectangular channel) - river_v.r[i] = river_v.a[i] / (river_p.flow_width[i] + 2.0 * river_v.hf[i]) # hydraulic radius (rectangular channel) + river_v.r[i] = + river_v.a[i] / wetted_perimeter_channel(river_v.hf[i], river_p.flow_width[i]) # hydraulic radius (rectangular channel) river_v.q[i] = ifelse( river_v.hf[i] > river_p.h_thresh, @@ -376,7 +377,8 @@ function update_river_channel_flow!( river_v.hf[i] = (river_v.zs_max[i] - river_p.zb_max[i]) river_v.a[i] = river_p.flow_width[i] * river_v.hf[i] # flow area (rectangular channel) - river_v.r[i] = river_v.a[i] / (river_p.flow_width[i] + 2.0 * river_v.hf[i]) # hydraulic radius (rectangular channel) + river_v.r[i] = + river_v.a[i] / wetted_perimeter_channel(river_v.hf[i], river_p.flow_width[i]) # hydraulic radius (rectangular channel) river_v.q[i] = ifelse( river_v.hf[i] > river_p.h_thresh, @@ -1012,7 +1014,8 @@ function stable_timestep( beta = 5.0/3.0 @batch per = thread reduction = ((min, dt_min),) for i in 1:(n) - @fastmath @inbounds h_r = (flow_width[i] * h[i]) / (flow_width[i] + 2.0 * h[i]) + @fastmath @inbounds h_r = + (flow_width[i] * h[i]) / wetted_perimeter_channel(h[i], flow_width[i]) celerity = beta * 1.0/mannings_n[i] * pow(h_r, 2.0/3.0) * sqrt(slope[i]) dt = alpha_coefficient * flow_length[i] / celerity dt_min = min(dt, dt_min) From db4afdde7f93be1ef96e1fcadcec2d80c9d63b3b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 14 Apr 2026 13:11:28 +0200 Subject: [PATCH 33/90] Refactor computation of floodplain variables --- Wflow/src/routing/surface/floodplain.jl | 61 ++++++---- Wflow/src/routing/surface/surface_kinwave.jl | 2 +- .../surface/surface_staggered_scheme.jl | 70 ++--------- Wflow/test/run_sbm.jl | 114 ++++++------------ 4 files changed, 90 insertions(+), 157 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 7be0fa11c..acd47c64b 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -226,13 +226,7 @@ function initialize_storage!(river, domain::Domain, nriv::Int) (; profile) = floodplain.parameters for i in 1:nriv i1, i2 = interpolation_indices(floodplain.variables.h[i], profile.depth) - a = flow_area( - profile.width[i2, i], - profile.a[i1, i], - profile.depth[i1], - floodplain.variables.h[i], - ) - a = max(a - (flow_width[i] * floodplain.variables.h[i]), 0.0) + a = compute_floodplain_flow_area(profile, floodplain.variables.h[i], i, i1, i2) floodplain.variables.storage[i] = flow_length[i] * a end return nothing @@ -255,31 +249,35 @@ function interpolation_indices(x, v::AbstractVector) end """ - flow_area(width, area, depth, h) - -Compute floodplain flow area based on flow depth `h` and floodplain `depth`, `area` and -`width` of a floodplain profile. +Compute flood flow area (including area above channel) based on flow depth `h` and +floodplain `depth`, `area` and `width` of a floodplain profile. """ -function flow_area(width, area, depth, h) - dh = h - depth # depth at i1 - area = area + (width * dh) # area at i1, width at i2 +function compute_flood_flow_area( + profile::FloodPlainProfile, + h::Float64, + idx::Int, + i1::Int, + i2::Int, +) + (; a, width, depth) = profile + dh = h - depth[i1] # depth at i1 + area = a[i1, idx] + (width[i2, idx] * dh) # area at i1, width at i2 return area end """ - function wetted_perimeter(p, depth, h) - Compute floodplain wetted perimeter based on flow depth `h` and floodplain `depth` and wetted perimeter `p` of a floodplain profile. """ -function wetted_perimeter(p, depth, h) - dh = h - depth # depth at i1 - p += 2.0 * dh # p at i1 +function compute_wetted_perimeter(profile::FloodPlainProfile, h::Float64, idx::Int, i1::Int) + (; p, depth) = profile + dh = h - depth[i1] # depth at i1 + p = p[i1, idx] + 2.0 * dh # p at i1 return p end "Compute flood depth by interpolating flood storage `flood_storage` using flood depth intervals." -function flood_depth( +function compute_flood_depth( profile::FloodPlainProfile, flood_storage::Float64, flow_length::Float64, @@ -292,15 +290,28 @@ function flood_depth( return flood_depth end -function flood_depth_storage( +function compute_floodplain_flow_area( + profile::FloodPlainProfile, + h::Float64, + idx::Int, + i1::Int, + i2::Int, +) + channel_area = profile.width[1, idx] * h + floodplain_flow_area = compute_flood_flow_area(profile, h, idx, i1, i2) + floodplain_flow_area = max(floodplain_flow_area - channel_area, 0.0) + return floodplain_flow_area +end + +function compute_flood_depth_storage( profile::FloodPlainProfile, flood_area::Float64, flow_length::Float64, - i::Int, + idx::Int, ) - i1, i2 = interpolation_indices(flood_area, @view profile.a[:, i]) - ΔA = (flood_area - profile.a[i1, i]) - dh = ΔA / profile.width[i2, i] + i1, i2 = interpolation_indices(flood_area, @view profile.a[:, idx]) + ΔA = (flood_area - profile.a[i1, idx]) + dh = ΔA / profile.width[i2, idx] flood_depth = profile.depth[i1] + dh flood_storage = profile.storage[i1] + ΔA * flow_length return flood_depth, flood_storage diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index 6a52f2326..5b1fe5fac 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -466,7 +466,7 @@ function kinwave_river_update!( bankfull_area = flow_width[v] * bankfull_depth[v] if crossarea > bankfull_area floodarea = crossarea - bankfull_area - flood_depth, flood_storage = flood_depth_storage( + flood_depth, flood_storage = compute_flood_depth_storage( floodplain.parameters.profile, floodarea, flow_length[v], diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index d8d5e7126..b80e92120 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -413,6 +413,7 @@ function update_floodplain_flow!( river_v = river_flow_model.variables river_p = river_flow_model.parameters floodplain_p = river_flow_model.floodplain.parameters + (; profile) = floodplain_p floodplain_v = river_flow_model.floodplain.variables @batch per = thread minbatch = 1000 for i in 1:length(floodplain_v.hf) @@ -429,43 +430,20 @@ function update_floodplain_flow!( end end - get_area(i, i1, i2, idx) = flow_area( - floodplain_p.profile.width[i2, idx], - floodplain_p.profile.a[i1, idx], - floodplain_p.profile.depth[i1], - floodplain_v.hf[i], - ) - - get_wetted_perimeter(i, i1, idx) = wetted_perimeter( - floodplain_p.profile.p[i1, idx], - floodplain_p.profile.depth[i1], - floodplain_v.hf[i], - ) - @batch per = thread minbatch = 1000 for j in 1:n i = floodplain_v.hf_index[j] i_src = nodes_at_edge.src[i] i_dst = nodes_at_edge.dst[i] - i0 = 0 - for k in eachindex(floodplain_p.profile.depth) - i0 += 1 * (floodplain_p.profile.depth[k] <= floodplain_v.hf[i]) - end - i1 = max(i0, 1) - i2 = ifelse(i1 == length(floodplain_p.profile.depth), i1, i1 + 1) - - a_src = get_area(i, i1, i2, i_src) - a_src = max(a_src - (floodplain_v.hf[i] * flow_width[i_src]), 0.0) - - a_dst = get_area(i, i1, i2, i_dst) - a_dst = max(a_dst - (floodplain_v.hf[i] * flow_width[i_dst]), 0.0) - + i1, i2 = interpolation_indices(floodplain_v.hf[i], @view profile.depth[:]) + a_src = compute_floodplain_flow_area(profile, floodplain_v.hf[i], i_src, i1, i2) + a_dst = compute_floodplain_flow_area(profile, floodplain_v.hf[i], i_dst, i1, i2) floodplain_v.a[i] = min(a_src, a_dst) floodplain_v.r[i] = if a_src < a_dst - a_src / get_wetted_perimeter(i, i1, i_src) + a_src / compute_wetted_perimeter(profile, floodplain_v.hf[i], i_src, i1) else - a_dst / get_wetted_perimeter(i, i1, i_dst) + a_dst / compute_wetted_perimeter(profile, floodplain_v.hf[i], i_dst, i1) end floodplain_v.q[i] = if floodplain_v.a[i] > 1.0e-05 @@ -518,6 +496,7 @@ function update_floodplain_flow!( river_v = river_flow_model.variables river_p = river_flow_model.parameters floodplain_p = river_flow_model.floodplain.parameters + (; profile) = floodplain_p floodplain_v = river_flow_model.floodplain.variables @batch per = thread minbatch = 1000 for i in 1:length(floodplain_v.hf) @@ -534,43 +513,20 @@ function update_floodplain_flow!( end end - get_area(i, i1, i2, idx) = flow_area( - floodplain_p.profile.width[i2, idx], - floodplain_p.profile.a[i1, idx], - floodplain_p.profile.depth[i1], - floodplain_v.hf[i], - ) - - get_wetted_perimeter(i, i1, idx) = wetted_perimeter( - floodplain_p.profile.p[i1, idx], - floodplain_p.profile.depth[i1], - floodplain_v.hf[i], - ) - @batch per = thread minbatch = 1000 for j in 1:n i = floodplain_v.hf_index[j] i_src = nodes_at_edge.src[i] i_dst = nodes_at_edge.dst[i] - i0 = 0 - for k in eachindex(floodplain_p.profile.depth) - i0 += 1 * (floodplain_p.profile.depth[k] <= floodplain_v.hf[i]) - end - i1 = max(i0, 1) - i2 = ifelse(i1 == length(floodplain_p.profile.depth), i1, i1 + 1) - - a_src = get_area(i, i1, i2, i_src) - a_src = max(a_src - (floodplain_v.hf[i] * flow_width[i_src]), 0.0) - - a_dst = get_area(i, i1, i2, i_dst) - a_dst = max(a_dst - (floodplain_v.hf[i] * flow_width[i_dst]), 0.0) - + i1, i2 = interpolation_indices(floodplain_v.hf[i], @view profile.depth[:]) + a_src = compute_floodplain_flow_area(profile, floodplain_v.hf[i], i_src, i1, i2) + a_dst = compute_floodplain_flow_area(profile, floodplain_v.hf[i], i_dst, i1, i2) floodplain_v.a[i] = min(a_src, a_dst) floodplain_v.r[i] = if a_src < a_dst - a_src / get_wetted_perimeter(i, i1, i_src) + a_src / compute_wetted_perimeter(profile, floodplain_v.hf[i], i_src, i1) else - a_dst / get_wetted_perimeter(i, i1, i_dst) + a_dst / compute_wetted_perimeter(profile, floodplain_v.hf[i], i_dst, i1) end floodplain_v.q[i] = if floodplain_v.a[i] > 1.0e-05 @@ -674,7 +630,7 @@ function update_water_depth_and_storage!( storage_total = river_v.storage[i] + floodplain_v.storage[i] if storage_total > river_p.bankfull_storage[i] flood_storage = storage_total - river_p.bankfull_storage[i] - h = flood_depth(floodplain_p.profile, flood_storage, flow_length[i], i) + h = compute_flood_depth(floodplain_p.profile, flood_storage, flow_length[i], i) river_v.h[i] = river_p.bankfull_depth[i] + h river_v.storage[i] = river_v.h[i] * flow_width[i] * flow_length[i] floodplain_v.storage[i] = max(storage_total - river_v.storage[i], 0.0) diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index 7012f265f..f8fca32c5 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -456,8 +456,9 @@ end @testset "river flow (local inertial) floodplain schematization" begin # floodplain geometry checks (index 3) - @test profile.storage[:, 3] ≈ [0.0, 8641.0, 19011.0, 31685.0, 51848.0, 80653.0] - @test profile.width[:, 3] ≈ [ + idx = 3 + @test profile.storage[:, idx] ≈ [0.0, 8641.0, 19011.0, 31685.0, 51848.0, 80653.0] + @test profile.width[:, idx] ≈ [ 30.0, 99.28617594254938, 119.15260323159785, @@ -465,7 +466,7 @@ end 231.6754039497307, 330.9730700179533, ] - @test profile.p[:, 3] ≈ [ + @test profile.p[:, idx] ≈ [ 69.28617594254938, 70.28617594254938, 91.15260323159785, @@ -473,7 +474,7 @@ end 205.6754039497307, 305.9730700179533, ] - @test profile.a[:, 3] ≈ [ + @test profile.a[:, idx] ≈ [ 0.0, 49.64308797127469, 109.21938958707361, @@ -481,98 +482,63 @@ end 297.8700179533214, 463.35655296229805, ] - @test dh .* profile.width[2:end, 3] * flow_length[3] ≈ Δv - @test profile.a[:, 3] * flow_length[3] ≈ profile.storage[:, 3] + @test dh .* profile.width[2:end, idx] * flow_length[idx] ≈ Δv + @test profile.a[:, 3] * flow_length[idx] ≈ profile.storage[:, idx] # flood depth from flood storage (8000.0) flood_vol = 8000.0 - river_flow.variables.storage[3] = - flood_vol + river_flow.parameters.bankfull_storage[3] - i1, i2 = Wflow.interpolation_indices(flood_vol, profile.storage[:, 3]) + river_flow.variables.storage[idx] = + flood_vol + river_flow.parameters.bankfull_storage[idx] + i1, i2 = Wflow.interpolation_indices(flood_vol, profile.storage[:, idx]) @test (i1, i2) == (1, 2) - flood_depth = Wflow.flood_depth(profile, flood_vol, flow_length[3], 3) + flood_depth = Wflow.compute_flood_depth(profile, flood_vol, flow_length[idx], idx) @test flood_depth ≈ 0.46290938548779076 - @test (flood_depth - profile.depth[i1]) * profile.width[i2, 3] * flow_length[3] + - profile.storage[i1, 3] ≈ flood_vol + @test (flood_depth - profile.depth[i1]) * + profile.width[i2, idx] * + flow_length[idx] + profile.storage[i1, idx] ≈ flood_vol # flood depth from flood storage (12000.0) flood_vol = 12000.0 - river_flow.variables.storage[3] = - flood_vol + river_flow.parameters.bankfull_storage[3] - i1, i2 = Wflow.interpolation_indices(flood_vol, profile.storage[:, 3]) - @test (i1, i2) == (2, 3) - flood_depth = Wflow.flood_depth(profile, flood_vol, flow_length[3], 3) + river_flow.variables.storage[idx] = + flood_vol + river_flow.parameters.bankfull_storage[idx] + i1, i2 = Wflow.interpolation_indices(flood_vol, profile.storage[:, idx]) + @test (i1, i2) == (2, idx) + flood_depth = Wflow.compute_flood_depth(profile, flood_vol, flow_length[idx], idx) @test flood_depth ≈ 0.6619575699132112 - @test (flood_depth - profile.depth[i1]) * profile.width[i2, 3] * flow_length[3] + - profile.storage[i1, 3] ≈ flood_vol + @test (flood_depth - profile.depth[i1]) * + profile.width[i2, idx] * + flow_length[idx] + profile.storage[i1, idx] ≈ flood_vol # test extrapolation of segment flood_vol = 95000.0 - river_flow.variables.storage[3] = - flood_vol + river_flow.parameters.bankfull_storage[3] - i1, i2 = Wflow.interpolation_indices(flood_vol, profile.storage[:, 3]) + river_flow.variables.storage[idx] = + flood_vol + river_flow.parameters.bankfull_storage[idx] + i1, i2 = Wflow.interpolation_indices(flood_vol, profile.storage[:, idx]) @test (i1, i2) == (6, 6) - flood_depth = Wflow.flood_depth(profile, flood_vol, flow_length[3], 3) + flood_depth = Wflow.compute_flood_depth(profile, flood_vol, flow_length[idx], idx) @test flood_depth ≈ 2.749036625585836 - @test (flood_depth - profile.depth[i1]) * profile.width[i2, 3] * flow_length[3] + - profile.storage[i1, 3] ≈ flood_vol - river_flow.variables.storage[3] = 0.0 # reset storage + @test (flood_depth - profile.depth[i1]) * + profile.width[i2, idx] * + flow_length[idx] + profile.storage[i1, idx] ≈ flood_vol + river_flow.variables.storage[idx] = 0.0 # reset storage # flow area and wetted perimeter based on hf h = 0.5 i1, i2 = Wflow.interpolation_indices(h, profile.depth) - @test Wflow.flow_area( - profile.width[i2, 3], - profile.a[i1, 3], - profile.depth[i1], - h, - ) ≈ 49.64308797127469 - @test Wflow.wetted_perimeter(profile.p[i1, 3], profile.depth[i1], h) ≈ - 70.28617594254938 + @test Wflow.compute_flood_flow_area(profile, h, idx, i1, i2) ≈ 49.64308797127469 + @test Wflow.compute_wetted_perimeter(profile, h, idx, i1) ≈ 70.28617594254938 h = 1.5 i1, i2 = Wflow.interpolation_indices(h, profile.depth) - @test Wflow.flow_area( - profile.width[i2, 3], - profile.a[i1, 3], - profile.depth[i1], - h, - ) ≈ 182.032315978456 - @test Wflow.wetted_perimeter(profile.p[i1, 3], profile.depth[i1], h) ≈ - 118.62585278276481 + @test Wflow.compute_flood_flow_area(profile, h, idx, i1, i2) ≈ 182.032315978456 + @test Wflow.compute_wetted_perimeter(profile, h, idx, i1) ≈ 118.62585278276481 h = 1.7 i1, i2 = Wflow.interpolation_indices(h, profile.depth) - @test Wflow.flow_area( - profile.width[i2, 3], - profile.a[i1, 3], - profile.depth[i1], - h, - ) ≈ 228.36739676840216 - @test Wflow.wetted_perimeter(profile.p[i1, 3], profile.depth[i1], h) ≈ - 119.02585278276482 + @test Wflow.compute_flood_flow_area(profile, h, idx, i1, i2) ≈ 228.36739676840216 + @test Wflow.compute_wetted_perimeter(profile, h, idx, i1) ≈ 119.02585278276482 h = 3.2 i1, i2 = Wflow.interpolation_indices(h, profile.depth) - @test Wflow.flow_area( - profile.width[i2, 3], - profile.a[i1, 3], - profile.depth[i1], - h, - ) ≈ 695.0377019748654 - @test Wflow.wetted_perimeter(profile.p[i1, 3], profile.depth[i1], h) ≈ - 307.3730700179533 + @test Wflow.compute_flood_flow_area(profile, h, idx, i1, i2) ≈ 695.0377019748654 + @test Wflow.compute_wetted_perimeter(profile, h, idx, i1) ≈ 307.3730700179533 h = 4.0 i1, i2 = Wflow.interpolation_indices(h, profile.depth) - @test Wflow.flow_area( - profile.width[i2, 3], - profile.a[i1, 3], - profile.depth[i1], - h, - ) ≈ 959.816157989228 - @test Wflow.wetted_perimeter(profile.p[i1, 3], profile.depth[i1], h) ≈ - 308.9730700179533 - @test Wflow.flow_area( - profile.width[i2, 4], - profile.a[i1, 4], - profile.depth[i1], - h, - ) ≈ 407.6395313908081 - @test Wflow.wetted_perimeter(profile.p[i1, 4], profile.depth[i1], h) ≈ - 90.11775307900271 + @test Wflow.compute_flood_flow_area(profile, h, idx, i1, i2) ≈ 959.816157989228 + @test Wflow.compute_wetted_perimeter(profile, h, idx, i1) ≈ 308.9730700179533 end Wflow.run_timestep!(model) From d2c5e45cede5f6d071b45654bf6b6eea383e9530 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 14 Apr 2026 13:45:58 +0200 Subject: [PATCH 34/90] Use function for active floodplain cells --- Wflow/src/routing/surface/floodplain.jl | 17 ++++++++++++++++ .../surface/surface_staggered_scheme.jl | 20 ++----------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index acd47c64b..726aa5d9f 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -317,6 +317,23 @@ function compute_flood_depth_storage( return flood_depth, flood_storage end +function active_floodplain_cells(river_flow_model) + (; floodplain) = river_flow_model + (; hf) = river_flow_model.variables + (; active_e, h_thresh) = river_flow_model.parameters + + n = 0 + @inbounds for i in active_e + @inbounds if hf[i] > h_thresh + n += 1 + floodplain.variables.hf_index[n] = i + else + floodplain.variables.q[i] = 0.0 + end + end + return n +end + "Initialize floodplain geometry, model variables and parameters on staggered grid" function FloodPlainModel( dataset::NCDataset, diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index b80e92120..c3dc8569e 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -420,15 +420,7 @@ function update_floodplain_flow!( floodplain_v.hf[i] = max(river_v.zs_max[i] - floodplain_p.zb_max[i], 0.0) end - n = 0 - @inbounds for i in river_p.active_e - @inbounds if river_v.hf[i] > river_p.h_thresh - n += 1 - floodplain_v.hf_index[n] = i - else - floodplain_v.q[i] = 0.0 - end - end + n = active_floodplain_cells(river_flow_model) @batch per = thread minbatch = 1000 for j in 1:n i = floodplain_v.hf_index[j] @@ -503,15 +495,7 @@ function update_floodplain_flow!( floodplain_v.hf[i] = max(river_v.zs_max[i] - floodplain_p.zb_max[i], 0.0) end - n = 0 - @inbounds for i in river_p.active_e - @inbounds if river_v.hf[i] > river_p.h_thresh - n += 1 - floodplain_v.hf_index[n] = i - else - floodplain_v.q[i] = 0.0 - end - end + n = active_floodplain_cells(river_flow_model) @batch per = thread minbatch = 1000 for j in 1:n i = floodplain_v.hf_index[j] From 7ff7e8cc8172c93e7944a343fb388f0752451b5b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 15 Apr 2026 11:03:36 +0200 Subject: [PATCH 35/90] Move update kinematic wave floodplain to function --- Wflow/src/routing/surface/surface_kinwave.jl | 85 +++++++++++--------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index 5b1fe5fac..a5fdf85e5 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -361,6 +361,45 @@ function update_overland_flow_model!( return nothing end +function update_floodplain_model!( + river_flow_model::RiverFlowModel{T, F}, + domain::DomainRiver, + v::Int, + dt::Float64, +) where {T <: KinematicWave, F <: FloodPlainModel} + (; floodplain) = river_flow_model + (; beta, alpha, bankfull_depth, bankfull_flow) = river_flow_model.parameters + (; flow_length, flow_width) = domain.parameters + (; q, h, storage) = river_flow_model.variables + + flow_area = alpha[v] * pow(q[v], beta) + if q[v] > bankfull_flow[v] + bankfull_area = flow_width[v] * bankfull_depth[v] + flood_area = flow_area - bankfull_area + flood_depth, flood_storage = compute_flood_depth_storage( + floodplain.parameters.profile, + flood_area, + flow_length[v], + v, + ) + floodplain_flow = q[v] - bankfull_flow[v] + # update total river and floodplain depth and storage + h[v] = bankfull_depth[v] + flood_depth + storage[v] = bankfull_area * flow_length[v] + flood_storage + else + flood_storage = 0.0 + flood_depth = 0.0 + floodplain_flow = 0.0 + # update river depth and storage + h[v] = flow_area / flow_width[v] + storage[v] = flow_length[v] * flow_width[v] * h[v] + end + floodplain.variables.storage[v] = flood_storage + floodplain.variables.h[v] = flood_depth + floodplain.variables.q[v] = floodplain_flow + floodplain.variables.q_av[v] += floodplain_flow * dt +end + "Update river flow model `RiverFlowModel{<:KinematicWave}` for a single timestep" function kinwave_river_update!( river_flow_model::RiverFlowModel{<:KinematicWave}, @@ -380,9 +419,9 @@ function kinwave_river_update!( (; reservoir, external_inflow, actual_external_abstraction_av, abstraction) = river_flow_model.boundary_conditions - (; beta, alpha, bankfull_depth, bankfull_flow) = river_flow_model.parameters + (; beta, alpha) = river_flow_model.parameters (; flow_width, flow_length) = domain.parameters - (; h, q, q_av, q_channel_av, storage, qin, qin_av, qlat) = river_flow_model.variables + (; h, q, q_av, storage, qin, qin_av, qlat) = river_flow_model.variables (; floodplain) = river_flow_model if !isnothing(reservoir) @@ -458,34 +497,12 @@ function kinwave_river_update!( end end # update h and storage - crossarea = alpha[v] * pow(q[v], beta) + flow_area = alpha[v] * pow(q[v], beta) if isnothing(floodplain) - h[v] = crossarea / flow_width[v] + h[v] = flow_area / flow_width[v] storage[v] = flow_length[v] * flow_width[v] * h[v] else - bankfull_area = flow_width[v] * bankfull_depth[v] - if crossarea > bankfull_area - floodarea = crossarea - bankfull_area - flood_depth, flood_storage = compute_flood_depth_storage( - floodplain.parameters.profile, - floodarea, - flow_length[v], - v, - ) - h[v] = bankfull_depth[v] + flood_depth - storage[v] = bankfull_area * flow_length[v] + flood_storage - floodplain_flow = q[v] - bankfull_flow[v] - else - h[v] = crossarea / flow_width[v] - storage[v] = flow_length[v] * flow_width[v] * h[v] - flood_storage = 0.0 - flood_depth = 0.0 - floodplain_flow = 0.0 - end - floodplain.variables.storage[v] = flood_storage - floodplain.variables.h[v] = flood_depth - floodplain.variables.q[v] = floodplain_flow - floodplain.variables.q_av[v] += floodplain.variables.q[v] * dt + update_floodplain_model!(river_flow_model, domain, v, dt) end # average variables (here accumulated for model timestep Δt) q_av[v] += q[v] * dt @@ -510,17 +527,9 @@ function update_alpha_parameter!( for i in eachindex(h) if h[i] > bankfull_depth[i] wp_channel = wetted_perimeter_channel(bankfull_depth[i], flow_width[i]) - i0 = 0 - for k in eachindex(floodplain_p.profile.depth) - i0 += 1 * (floodplain_p.profile.depth[k] <= floodplain_v.h[i]) - end - i1 = max(i0, 1) - i2 = ifelse(i1 == length(floodplain_p.profile.depth), i1, i1 + 1) - wp_floodplain = wetted_perimeter( - floodplain_p.profile.p[i1, i], - floodplain_p.profile.depth[i1], - floodplain_v.h[i], - ) + i1, i2 = interpolation_indices(floodplain_v.h[i], floodplain_p.profile.depth) + wp_floodplain = + compute_wetted_perimeter(floodplain_p.profile, floodplain_v.h[i], i, i1) wp_combined = wp_channel + wp_floodplain mannings_n_combined = pow( wp_floodplain/wp_combined * pow(floodplain_p.mannings_n[i], 1.5) + From 0f70c6ecf38fd194acc1736c16e346becec5452b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 5 May 2026 16:37:38 +0200 Subject: [PATCH 36/90] Separate routing for floodplain Making use of a simple routing scheme ( Manning flow and `accucapacityflux`) as kinematic wave routing gets quite slow for very small flows. --- Wflow/src/mass_balance.jl | 24 ++- Wflow/src/routing/surface/floodplain.jl | 25 +-- Wflow/src/routing/surface/surface_flow.jl | 1 + Wflow/src/routing/surface/surface_kinwave.jl | 175 +++++++++---------- Wflow/test/run_sbm.jl | 50 +++++- 5 files changed, 153 insertions(+), 122 deletions(-) diff --git a/Wflow/src/mass_balance.jl b/Wflow/src/mass_balance.jl index 1dcb19287..11bd80eb1 100644 --- a/Wflow/src/mass_balance.jl +++ b/Wflow/src/mass_balance.jl @@ -125,13 +125,12 @@ function compute_total_storage!( end """ - get_storage(river_flow_model::RiverFlowModel{<:LocalInertial}, i) - get_storage(river_flow_model::RiverFlowModel{<:KinematicWave}, i) + get_storage(river_flow_model::RiverFlowModel, i) -Return storage of a river flow model at index `i`. For `RiverFlowModel{<:LocalInertial}` -floodplain storage is added to river storage if an optional floodplain is included. +Return storage of a river flow model at index `i`, floodplain storage is added to river +storage if an optional floodplain is included. """ -function get_storage(river_flow_model::RiverFlowModel{<:LocalInertial}, i) +function get_storage(river_flow_model::AbstractRiverFlowModel, i) (; storage) = river_flow_model.variables if isnothing(river_flow_model.floodplain) return storage[i] @@ -140,8 +139,6 @@ function get_storage(river_flow_model::RiverFlowModel{<:LocalInertial}, i) return total_storage end end -get_storage(river_flow_model::RiverFlowModel{<:KinematicWave}, i) = - river_flow_model.variables.storage[i] """ Save river (+ floodplain) storage at previous time step as `storage_prev` of river @@ -296,12 +293,16 @@ function compute_flow_balance!( (; storage_prev, error, relative_error) = water_balance (; inwater, external_inflow, actual_external_abstraction_av, abstraction) = river_flow_model.boundary_conditions - (; qin_av, q_av, storage) = river_flow_model.variables + (; qin_av, q_av) = river_flow_model.variables for i in eachindex(storage_prev) total_in = inwater[i] + qin_av[i] + max(0.0, external_inflow[i]) total_out = q_av[i] + actual_external_abstraction_av[i] + abstraction[i] - storage_rate = (storage[i] - storage_prev[i]) / dt + storage = river_flow_model.variables.storage[i] + if !isnothing(river_flow_model.floodplain) + storage += river_flow_model.floodplain.variables.storage[i] + end + storage_rate = (storage - storage_prev[i]) / dt error[i], relative_error[i] = compute_mass_balance_error(total_in, total_out, storage_rate) end @@ -504,10 +505,7 @@ routing. function compute_flow_routing_balance!( model::Model{R}, ) where { - R <: Routing{ - <:OverlandFlowModel{<:LocalInertial}, - <:RiverFlowModel{<:LocalInertial}, - }, + R <: Routing{<:OverlandFlowModel{<:LocalInertial}, <:RiverFlowModel{<:LocalInertial}}, } (; river_flow, overland_flow, subsurface_flow) = model.routing (; reservoir) = river_flow.boundary_conditions diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 726aa5d9f..b4a9a7b88 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -213,10 +213,13 @@ end "Struct to store floodplain variables" @with_kw struct FloodPlainVariables <: AbstractFloodPlainVariables n::Int - q::Vector{Float64} = zeros(n) # discharge [m³ s⁻¹] - q_av::Vector{Float64} = zeros(n) # average river discharge [m³ s⁻¹] for model timestep Δt - storage::Vector{Float64} = zeros(n) # storage [m³] - h::Vector{Float64} = zeros(n) # water depth [m] + q::Vector{Float64} = zeros(n) # discharge [m³ s⁻¹] + q_av::Vector{Float64} = zeros(n) # average floodplain discharge [m³ s⁻¹] for model timestep Δt + qin::Vector{Float64} = zeros(n) # floodplain inflow from upstream cells [m³ s⁻¹] + qin_av::Vector{Float64} = zeros(n) # Average floodplain inflow from upstream cells [m³ s⁻¹] for model timestep Δt + storage::Vector{Float64} = zeros(n) # storage [m³] + h::Vector{Float64} = zeros(n) # water depth [m] + flow_capacity::Vector{Float64} = zeros(n) # flow capacity [m³ dt⁻¹] end "Determine the initial floodplain storage" @@ -303,20 +306,6 @@ function compute_floodplain_flow_area( return floodplain_flow_area end -function compute_flood_depth_storage( - profile::FloodPlainProfile, - flood_area::Float64, - flow_length::Float64, - idx::Int, -) - i1, i2 = interpolation_indices(flood_area, @view profile.a[:, idx]) - ΔA = (flood_area - profile.a[i1, idx]) - dh = ΔA / profile.width[i2, idx] - flood_depth = profile.depth[i1] + dh - flood_storage = profile.storage[i1] + ΔA * flow_length - return flood_depth, flood_storage -end - function active_floodplain_cells(river_flow_model) (; floodplain) = river_flow_model (; hf) = river_flow_model.variables diff --git a/Wflow/src/routing/surface/surface_flow.jl b/Wflow/src/routing/surface/surface_flow.jl index b02f7e06f..0eb72eef9 100644 --- a/Wflow/src/routing/surface/surface_flow.jl +++ b/Wflow/src/routing/surface/surface_flow.jl @@ -12,6 +12,7 @@ abstract type AbstractOverlandFlowVariables end external_inflow::Vector{Float64} = zeros(n) # External inflow (abstraction/supply/demand) [m³ s⁻¹] actual_external_abstraction_av::Vector{Float64} = zeros(n) # Actual abstraction from external negative inflow [m³ s⁻¹] abstraction::Vector{Float64} = zeros(n) # Abstraction (computed as part of water demand and allocation) [m³ s⁻¹] + floodplain_water_exchange::Vector{Float64} = zeros(n) # Exchange with floodplain [m³ s⁻¹] reservoir::R # Reservoir model struct of arrays end diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index a5fdf85e5..f713f61a7 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -33,7 +33,6 @@ end flow::ManningFlowParameters bankfull_depth::Vector{Float64} # Bankfull water level [m] bankfull_storage::Vector{Float64} # Bankfull storage [m³] - bankfull_flow::Vector{Float64} # Bankfull discharge [m³ s⁻¹] end "Overload `getproperty` for river flow model parameters" @@ -42,8 +41,6 @@ function Base.getproperty(v::RiverFlowParameters, s::Symbol) getfield(v, s) elseif s === :bankfull_storage getfield(v, s) - elseif s === :bankfull_flow - getfield(v, s) elseif s === :flow getfield(v, :flow) else @@ -75,20 +72,8 @@ function RiverFlowParameters(dataset::NCDataset, config::Config, domain::DomainR # use fixed wetted perimeter based on 0.5 * bankfull_depth wetted_perimeter = flow_width + bankfull_depth flow_params = ManningFlowParameters(mannings_n, slope, wetted_perimeter) - bankfull_storage = bankfull_depth .* flow_length .* flow_width - if config.model.floodplain_1d__flag - wetted_perimeter = @. wetted_perimeter_channel(bankfull_depth, flow_width) - alpha = @. flow_params.alpha_term * pow(wetted_perimeter, flow_params.alpha_pow) - bankfull_flow = @. pow(bankfull_depth * flow_width / alpha, 1.0 / flow_params.beta) - else - bankfull_flow = Float64[] - end - parameters = RiverFlowParameters(; - flow = flow_params, - bankfull_depth, - bankfull_storage, - bankfull_flow, - ) + bankfull_storage = bankfull_depth .* flow_width .* flow_length + parameters = RiverFlowParameters(; flow = flow_params, bankfull_depth, bankfull_storage) return parameters end @@ -364,42 +349,41 @@ end function update_floodplain_model!( river_flow_model::RiverFlowModel{T, F}, domain::DomainRiver, - v::Int, dt::Float64, -) where {T <: KinematicWave, F <: FloodPlainModel} +) where {T <: KinematicWave, F <: FloodPlainModel{<:KinematicWave}} (; floodplain) = river_flow_model - (; beta, alpha, bankfull_depth, bankfull_flow) = river_flow_model.parameters - (; flow_length, flow_width) = domain.parameters - (; q, h, storage) = river_flow_model.variables - - flow_area = alpha[v] * pow(q[v], beta) - if q[v] > bankfull_flow[v] - bankfull_area = flow_width[v] * bankfull_depth[v] - flood_area = flow_area - bankfull_area - flood_depth, flood_storage = compute_flood_depth_storage( - floodplain.parameters.profile, - flood_area, - flow_length[v], - v, - ) - floodplain_flow = q[v] - bankfull_flow[v] - # update total river and floodplain depth and storage - h[v] = bankfull_depth[v] + flood_depth - storage[v] = bankfull_area * flow_length[v] + flood_storage - else - flood_storage = 0.0 - flood_depth = 0.0 - floodplain_flow = 0.0 - # update river depth and storage - h[v] = flow_area / flow_width[v] - storage[v] = flow_length[v] * flow_width[v] * h[v] + (; slope, flow_length) = domain.parameters + (; profile, mannings_n) = floodplain.parameters + (; flow_capacity, h, q, q_av, qin, qin_av, storage) = floodplain.variables + + for i in eachindex(flow_capacity) + if h[i] > 0.0 + i1, i2 = interpolation_indices(h[i], @view profile.depth[:]) + flow_area = compute_floodplain_flow_area(profile, h[i], i, i1, i2) + wetted_perimeter = compute_wetted_perimeter(profile, h[i], i, i1) + hydraulic_radius = flow_area/wetted_perimeter + flow_capacity[i] = + manning_flow(mannings_n[i], hydraulic_radius, slope[i], flow_area) * dt + else + flow_capacity[i] = 0.0 + end + end + q .= accucapacityflux(storage, domain.network, flow_capacity) # q/dt + q_av .+= q + q ./= dt # m3/s + flux_in!(qin, q, domain.network) + @. qin_av = qin_av + qin * dt + for i in eachindex(storage) + h[i] = compute_flood_depth(profile, storage[i], flow_length[i], i) end - floodplain.variables.storage[v] = flood_storage - floodplain.variables.h[v] = flood_depth - floodplain.variables.q[v] = floodplain_flow - floodplain.variables.q_av[v] += floodplain_flow * dt end +update_floodplain_model!( + river_flow_model::RiverFlowModel{T, F}, + domain::DomainRiver, + dt::Float64, +) where {T <: KinematicWave, F <: Nothing} = nothing + "Update river flow model `RiverFlowModel{<:KinematicWave}` for a single timestep" function kinwave_river_update!( river_flow_model::RiverFlowModel{<:KinematicWave}, @@ -416,8 +400,13 @@ function kinwave_river_update!( reservoir_indices, ) = domain.network - (; reservoir, external_inflow, actual_external_abstraction_av, abstraction) = - river_flow_model.boundary_conditions + (; + reservoir, + external_inflow, + actual_external_abstraction_av, + abstraction, + floodplain_water_exchange, + ) = river_flow_model.boundary_conditions (; beta, alpha) = river_flow_model.parameters (; flow_width, flow_length) = domain.parameters @@ -441,18 +430,22 @@ function kinwave_river_update!( if external_inflow[v] < 0.0 _abstraction = min(-external_inflow[v], (storage[v] / dt) * 0.80) actual_external_abstraction_av[v] += _abstraction * dt - _inflow = -_abstraction / flow_length[v] + _inflow_lat = -_abstraction / flow_length[v] else - _inflow = external_inflow[v] / flow_length[v] + _inflow_lat = external_inflow[v] / flow_length[v] end # internal abstraction (water demand) is limited by river storage and # negative external inflow as part of water allocation computations. - _inflow -= abstraction[v] / flow_length[v] + _inflow_lat -= abstraction[v] / flow_length[v] + + if !isnothing(floodplain) + _inflow_lat += floodplain_water_exchange[v] / flow_length[v] + end q[v] = kinematic_wave( qin[v], q[v], - qlat[v] + _inflow, + qlat[v] + _inflow_lat, alpha[v], beta, dt, @@ -470,15 +463,15 @@ function kinwave_river_update!( (reservoir.variables.storage[i] / dt) * 0.98, ) res_bc.actual_external_abstraction_av[i] += _abstraction * dt - _inflow = -_abstraction + res_inflow = -_abstraction else - _inflow = res_bc.external_inflow[i] + res_inflow = res_bc.external_inflow[i] end net_inflow = q[v] + res_bc.inflow_overland[i] + res_bc.inflow_subsurface[i] + - _inflow + res_inflow update_reservoir_model!(reservoir, i, net_inflow, dt, dt_forcing) downstream_nodes = outneighbors(graph, v) @@ -498,12 +491,8 @@ function kinwave_river_update!( end # update h and storage flow_area = alpha[v] * pow(q[v], beta) - if isnothing(floodplain) - h[v] = flow_area / flow_width[v] - storage[v] = flow_length[v] * flow_width[v] * h[v] - else - update_floodplain_model!(river_flow_model, domain, v, dt) - end + h[v] = flow_area / flow_width[v] + storage[v] = flow_length[v] * flow_area # average variables (here accumulated for model timestep Δt) q_av[v] += q[v] * dt qin_av[v] += qin[v] * dt @@ -512,41 +501,41 @@ function kinwave_river_update!( end end -function update_alpha_parameter!( +function river_floodplain_exchange!( river_flow_model::RiverFlowModel{T, F}, parameters::RiverParameters, -) where {T <: KinematicWave, F <: FloodPlainModel} - (; h) = river_flow_model.variables - (; bankfull_depth, alpha, mannings_n, beta, alpha_term, alpha_pow) = - river_flow_model.parameters - (; flow_width, slope) = parameters - - floodplain_p = river_flow_model.floodplain.parameters + dt::Float64, +) where {T <: KinematicWave, F <: FloodPlainModel{<:KinematicWave}} + river_v = river_flow_model.variables + river_p = river_flow_model.parameters floodplain_v = river_flow_model.floodplain.variables + floodplain_p = river_flow_model.floodplain.parameters - for i in eachindex(h) - if h[i] > bankfull_depth[i] - wp_channel = wetted_perimeter_channel(bankfull_depth[i], flow_width[i]) - i1, i2 = interpolation_indices(floodplain_v.h[i], floodplain_p.profile.depth) - wp_floodplain = - compute_wetted_perimeter(floodplain_p.profile, floodplain_v.h[i], i, i1) - wp_combined = wp_channel + wp_floodplain - mannings_n_combined = pow( - wp_floodplain/wp_combined * pow(floodplain_p.mannings_n[i], 1.5) + - wp_channel/wp_combined * pow(mannings_n[i], 1.5), - 2.0/3.0, - ) - alpha[i] = alpha_term[i] * pow(wp_combined, alpha_pow) + (; flow_length, flow_width) = parameters + (; floodplain_water_exchange) = river_flow_model.boundary_conditions + + for i in eachindex(river_v.qlat) + storage_total = river_v.storage[i] + floodplain_v.storage[i] + if storage_total > river_p.bankfull_storage[i] + flood_storage = storage_total - river_p.bankfull_storage[i] + h = compute_flood_depth(floodplain_p.profile, flood_storage, flow_length[i], i) + river_storage = (river_p.bankfull_depth[i] + h) * flow_width[i] * flow_length[i] + delta_river_storage = river_storage - river_v.storage[i] + floodplain_v.storage[i] = max(storage_total - river_storage, 0.0) + floodplain_v.h[i] = floodplain_v.storage[i] > 0.0 ? h : 0.0 else - wp_channel = wetted_perimeter_channel(h[i], flow_width[i]) - alpha[i] = alpha_term[i] * pow(wp_channel, alpha_pow) + delta_river_storage = max(storage_total - river_v.storage[i], 0.0) + floodplain_v.h[i] = 0.0 + floodplain_v.storage[i] = 0.0 end + floodplain_water_exchange[i] = delta_river_storage/dt end end -update_alpha_parameter!( +river_floodplain_exchange!( river_flow_model::RiverFlowModel{T, F}, parameters::RiverParameters, + dt::Float64, ) where {T <: KinematicWave, F <: Nothing} = nothing """ @@ -571,17 +560,19 @@ function update_river_flow_model!( update_index_hq!(reservoir, clock) if !isnothing(floodplain) floodplain.variables.q_av .= 0.0 + floodplain.variables.qin_av .= 0.0 end dt = tosecond(clock.dt) t = 0.0 while t < dt - update_alpha_parameter!(river_flow_model, domain.river.parameters) dt_s = adaptive ? stable_timestep(river_flow_model, flow_length, 0.05) : river_flow_model.timestepping.dt_fixed dt_s = check_timestepsize(dt_s, t, dt) + river_floodplain_exchange!(river_flow_model, domain.river.parameters, dt_s) kinwave_river_update!(river_flow_model, domain.river, dt_s, dt) + update_floodplain_model!(river_flow_model, domain.river, dt) t += dt_s end @@ -590,8 +581,12 @@ function update_river_flow_model!( qin_av ./= dt if !isnothing(floodplain) floodplain.variables.q_av ./= dt - river_flow_model.variables.q_channel_av .= - river_flow_model.variables.q_av .- floodplain.variables.q_av + river_flow_model.variables.q_channel_av .= river_flow_model.variables.q_av + river_flow_model.variables.q_av .= + river_flow_model.variables.q_channel_av .+ floodplain.variables.q_av + floodplain.variables.qin_av ./= dt + river_flow_model.variables.qin_av .= + river_flow_model.variables.qin_av .+ floodplain.variables.qin_av end return nothing end diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index f8fca32c5..cb67056c9 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -203,7 +203,7 @@ end (; q_av) = model.routing.river_flow.variables @test q_av[4009] ≈ 8.426694842173548 # pit/ outlet, CartesianIndex(141, 228) @test q_av[4020] ≈ 0.006370691658310787 # downstream of pit 4009, CartesianIndex(141, 229) - @test q_av[2508] ≈ 131.40631419288573 # pit/ outlet + @test q_av[2508] ≈ 131.40631419288536 # pit/ outlet @test q_av[5808] ≈ 0.11941498848157915 # pit/ outlet end end @@ -709,6 +709,31 @@ end @test maximum(h) ≈ 1.1422278106700228 end +@testitem "Kinematic river flow including 1D floodplain schematization" begin + tomlpath = joinpath(@__DIR__, "sbm_river-floodplain-kw_config.toml") + config = Wflow.Config(tomlpath) + config.dir_output = mktempdir() + model = Wflow.Model(config) + (; river_flow) = model.routing + + Wflow.run_timestep!(model) + Wflow.run_timestep!(model) + + (; q_av, h) = river_flow.variables + @test sum(q_av) ≈ 3727.418193238896 + @test q_av[1622] ≈ 0.000750286757828644 + @test q_av[43] ≈ 11.458528971675058 + @test q_av[501] ≈ 0.33680653896370893 + @test q_av[5808] ≈ 0.04397636693917755 + @test h[1622] ≈ 0.0009790323262245453 + @test h[43] ≈ 0.16099844437124217 + @test h[501] ≈ 0.21391813674805524 + @test h[5808] ≈ 0.005801973682481159 + (; q_av, h) = river_flow.floodplain.variables + @test maximum(q_av) ≈ 6.079511650002338 + @test maximum(h) ≈ 0.12123075887001009 +end + @testitem "run wflow sbm" begin tomlpath = joinpath(@__DIR__, "sbm_config.toml") config = Wflow.Config(tomlpath) @@ -796,6 +821,29 @@ end Wflow.close_files(model; delete_output = false) end +@testitem "water balance river kinematic wave routing with floodplain" begin + tomlpath = joinpath(@__DIR__, "sbm_river-floodplain-kw_config.toml") + config = Wflow.Config(tomlpath) + config.dir_output = mktempdir() + config.model.water_mass_balance__flag = true + model = Wflow.Model(config) + (; river_water_balance) = model.mass_balance.routing + Wflow.run_timestep!(model) + @testset "water balance first timestep" begin + @test all(e -> abs(e) < 1e-9, river_water_balance.error) + @test all(re -> abs(re) < 1e-9, river_water_balance.relative_error) + end + Wflow.run_timestep!(model) + @testset "water balance second timestep" begin + @test all(e -> abs(e) < 3e-5, river_water_balance.error) + @test all(re -> abs(re) < 12.2, river_water_balance.relative_error) + inds = findall(x -> x > 1e-3, model.routing.river_flow.variables.q_av) + @test all(re -> abs(re) < 1e-9, river_water_balance.error[inds]) + @test all(re -> abs(re) < 1e-9, river_water_balance.relative_error[inds]) + end + Wflow.close_files(model; delete_output = false) +end + @testitem "water balance river and land local inertial routing" begin tomlpath = joinpath(@__DIR__, "sbm_river-land-local-inertial_config.toml") config = Wflow.Config(tomlpath) From f8f5069faf62f434ba2010d212d127b7306ed214 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 11 May 2026 10:27:14 +0200 Subject: [PATCH 37/90] Support mass balance computation `ManningStaggered` --- Wflow/src/mass_balance.jl | 2 +- .../surface/surface_staggered_scheme.jl | 3 +- Wflow/test/run_sbm.jl | 42 +++++++++++++++---- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Wflow/src/mass_balance.jl b/Wflow/src/mass_balance.jl index 11bd80eb1..1effb0861 100644 --- a/Wflow/src/mass_balance.jl +++ b/Wflow/src/mass_balance.jl @@ -314,7 +314,7 @@ Compute water mass balance error and relative error for river (and floodplain) l inertial routing. """ function compute_flow_balance!( - river_flow_model::RiverFlowModel{<:LocalInertial}, + river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, water_balance::MassBalance, network::NetworkRiver, dt::Float64, diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index c6e4e6e88..673545685 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -342,6 +342,7 @@ function update_river_channel_flow!( (; nodes_at_edge) = domain.network river_v = river_flow_model.variables river_p = river_flow_model.parameters + (; inwater, abstraction) = river_flow_model.boundary_conditions @batch per = thread minbatch = 1000 for j in eachindex(river_p.active_e) i = river_p.active_e[j] @@ -935,7 +936,7 @@ function stable_timestep( dt = alpha_coefficient * flow_length[i] / celerity dt_min = min(dt, dt_min) end - dt_min = isinf(dt_min) ? 600.0 : dt_min + dt_min = isinf(dt_min) ? 60.0 : dt_min return dt_min end diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index 5395eec0e..4a20228df 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -699,18 +699,18 @@ end Wflow.run_timestep!(model) (; q_av, h) = river_flow.variables - @test sum(q_av) ≈ 2198.5455132587495 - @test q_av[1622] ≈ 0.0002189225364991537 + @test sum(q_av) ≈ 2198.5452801537313 + @test q_av[1622] ≈ 0.0002189256643005604 @test q_av[43] ≈ 10.2692066407329 - @test q_av[501] ≈ 0.021410239377431573 - @test q_av[5808] ≈ 0.004923137077613924 - @test h[1622] ≈ 0.0017960475101775342 + @test q_av[501] ≈ 0.02141022323325671 + @test q_av[5808] ≈ 0.004923073187279234 + @test h[1622] ≈ 0.0017958855481125012 @test h[43] ≈ 1.3029109034019006 - @test h[501] ≈ 0.005400545537332966 - @test h[5808] ≈ 0.00589431731806414 + @test h[501] ≈ 0.005400545672072305 + @test h[5808] ≈ 0.0058942944042401375 (; q_av, h) = river_flow.floodplain.variables - @test maximum(q_av) ≈ 1.1824324225913527 - @test maximum(h) ≈ 1.1422278106700228 + @test maximum(q_av) ≈ 1.1824323411611732 + @test maximum(h) ≈ 1.1422278469202736 end @testitem "Kinematic river flow including 1D floodplain schematization" begin @@ -825,6 +825,30 @@ end Wflow.close_files(model; delete_output = false) end +@testitem "water balance river flow with floodplain using Manning's equation on a staggered grid" begin + tomlpath = joinpath(@__DIR__, "sbm_river-floodplain-staggered-scheme_config.toml") + config = Wflow.Config(tomlpath) + config.model.river_routing = "manning_staggered" + config.model.water_mass_balance__flag = true + config.dir_output = mktempdir() + model = Wflow.Model(config) + (; river_water_balance) = model.mass_balance.routing + Wflow.run_timestep!(model) + @testset "water balance first timestep" begin + @test all(e -> abs(e) < 1e-9, river_water_balance.error) + @test all(re -> abs(re) < 1e-9, river_water_balance.relative_error) + end + Wflow.run_timestep!(model) + @testset "water balance second timestep" begin + @test all(e -> abs(e) < 0.00012, river_water_balance.error) + @test all(re -> abs(re) < 0.33, river_water_balance.relative_error) + inds = findall(x -> x > 2.0e-3, model.routing.river_flow.variables.q_av) + @test all(e -> abs(e) < 1e-9, river_water_balance.error[inds]) + @test all(re -> abs(re) < 1e-9, river_water_balance.relative_error[inds]) + end + Wflow.close_files(model; delete_output = false) +end + @testitem "water balance river kinematic wave routing with floodplain" begin tomlpath = joinpath(@__DIR__, "sbm_river-floodplain-kw_config.toml") config = Wflow.Config(tomlpath) From 43651d8c90068b294daf231ba5e06ca091e4802c Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 3 Jun 2026 20:59:05 +0200 Subject: [PATCH 38/90] Consistent use of `at_edge` For parameter and variable names stored at edges that normally are stored at cell centres/nodes. --- Wflow/src/routing/surface/floodplain.jl | 27 ++-- .../surface/surface_staggered_scheme.jl | 138 +++++++++--------- Wflow/test/routing_process.jl | 94 +++++++----- 3 files changed, 142 insertions(+), 117 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 933fa389a..5a0eb6085 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -126,12 +126,14 @@ end @with_kw struct FloodPlainStaggeredParameters <: AbstractFloodPlainParameters # floodplain profile profile::FloodPlainProfile - # manning's roughness at edge [s m-1/3] + # manning's roughness [s m-1/3] mannings_n::Vector{Float64} = Float64[] + # manning's roughness at edge [s m-1/3] + mannings_n_at_edge::Vector{Float64} = Float64[] # manning's roughness squared at edge [(s m-1/3)2] - mannings_n_sq::Vector{Float64} = Float64[] + mannings_n_sq_at_edge::Vector{Float64} = Float64[] # maximum bankfull elevation at edge [m] - zb_max::Vector{Float64} = Float64[] + zb_max_at_edge::Vector{Float64} = Float64[] # slope at edge [-] slope_at_edge::Vector{Float64} = Float64[] end @@ -160,7 +162,7 @@ function FloodPlainStaggeredParameters( append!(mannings_n, mannings_n[index_pit]) # copy to ghost nodes mannings_n_at_edge = compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) - zb_max_edge = compute_value_at_edge(zb_floodplain, nodes_at_edge, n_edges, maximum) + zb_max_at_edge = compute_value_at_edge(zb_floodplain, nodes_at_edge, n_edges, maximum) if river_routing == RoutingType.local_inertial mannings_n_sq_at_edge = mannings_n_at_edge .* mannings_n_at_edge @@ -179,9 +181,10 @@ function FloodPlainStaggeredParameters( parameters = FloodPlainStaggeredParameters(; profile, - mannings_n = mannings_n_at_edge, - mannings_n_sq = mannings_n_sq_at_edge, - zb_max = zb_max_edge, + mannings_n, + mannings_n_at_edge, + mannings_n_sq_at_edge, + zb_max_at_edge, slope_at_edge, ) return parameters @@ -192,9 +195,9 @@ end n::Int n_edges::Int # flow area at edge [m²] - flow_area::Vector{Float64} = zeros(n_edges) + flow_area_at_edge::Vector{Float64} = zeros(n_edges) # hydraulic radius at edge [m] - hydraulic_radius::Vector{Float64} = zeros(n_edges) + hydraulic_radius_at_edge::Vector{Float64} = zeros(n_edges) # water depth at edge [m] water_depth_at_edge::Vector{Float64} = zeros(n_edges) # discharge at edge at previous time step @@ -228,8 +231,10 @@ end "Struct to store floodplain parameters" @with_kw struct FloodPlainParameters <: AbstractFloodPlainParameters - profile::FloodPlainProfile # floodplain profile - mannings_n::Vector{Float64} = Float64[] # manning's roughness[s m-1/3] + # floodplain profile + profile::FloodPlainProfile + # manning's roughness[s m-1/3] + mannings_n::Vector{Float64} = Float64[] end "Struct to store floodplain variables" diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index c47ef2800..25d578c10 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -19,11 +19,13 @@ # bankfull depth [m] bankfull_depth::Vector{Float64} = Float64[] # maximum channel bed elevation at edge [m] - zb_max::Vector{Float64} = Float64[] - # Manning's roughness at edge [s m-1/3] + zb_max_at_edge::Vector{Float64} = Float64[] + # Manning's roughness [s m-1/3] mannings_n::Vector{Float64} = Float64[] + # Manning's roughness at edge [s m-1/3] + mannings_n_at_edge::Vector{Float64} = Float64[] # Manning's roughness squared at edge [(s m-1/3)2] - mannings_n_sq::Vector{Float64} = Float64[] + mannings_n_sq_at_edge::Vector{Float64} = Float64[] # slope at edge [-] slope_at_edge::Vector{Float64} = Float64[] # flow (river) length at edge [m] @@ -138,9 +140,10 @@ function RiverFlowStaggeredParameters( zb, bankfull_storage, bankfull_depth, - zb_max = zb_max_at_edge, - mannings_n = mannings_n_at_edge, - mannings_n_sq = mannings_n_sq_at_edge, + zb_max_at_edge, + mannings_n, + mannings_n_at_edge, + mannings_n_sq_at_edge, slope_at_edge, flow_length_at_edge, flow_width_at_edge, @@ -167,7 +170,7 @@ end # water depth [m] h::Vector{Float64} # maximum water elevation at edge [m] - zs_max::Vector{Float64} = zeros(n_edges) + zs_max_at_edge::Vector{Float64} = zeros(n_edges) # water elevation of source node of edge [m] zs_src::Vector{Float64} = zeros(n_edges) # water elevation of downstream node of edge [m] @@ -175,9 +178,9 @@ end # water depth at edge [m] water_depth_at_edge::Vector{Float64} = zeros(n_edges) # flow area at edge [m²] - flow_area::Vector{Float64} = zeros(n_edges) + flow_area_at_edge::Vector{Float64} = zeros(n_edges) # wetted perimeter at edge [m] - hydraulic_radius::Vector{Float64} = zeros(n_edges) + hydraulic_radius_at_edge::Vector{Float64} = zeros(n_edges) # river storage [m³] storage::Vector{Float64} = zeros(n_cells) # error storage [m³] @@ -335,13 +338,14 @@ function update_river_channel_flow!( river_v.zs_src[i] = river_p.zb[i_src] + river_v.h[i_src] river_v.zs_dst[i] = river_p.zb[i_dst] + river_v.h[i_dst] - river_v.zs_max[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) - river_v.water_depth_at_edge[i] = (river_v.zs_max[i] - river_p.zb_max[i]) + river_v.zs_max_at_edge[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) + river_v.water_depth_at_edge[i] = + (river_v.zs_max_at_edge[i] - river_p.zb_max_at_edge[i]) - river_v.flow_area[i] = + river_v.flow_area_at_edge[i] = river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] # flow area (rectangular channel) - river_v.hydraulic_radius[i] = - river_v.flow_area[i] / wetted_perimeter_channel( + river_v.hydraulic_radius_at_edge[i] = + river_v.flow_area_at_edge[i] / wetted_perimeter_channel( river_v.water_depth_at_edge[i], river_p.flow_width_at_edge[i], ) # hydraulic radius (rectangular channel) @@ -353,10 +357,10 @@ function update_river_channel_flow!( river_v.zs_src[i], river_v.zs_dst[i], river_v.water_depth_at_edge[i], - river_v.flow_area[i], - river_v.hydraulic_radius[i], + river_v.flow_area_at_edge[i], + river_v.hydraulic_radius_at_edge[i], river_p.flow_length_at_edge[i], - river_p.mannings_n_sq[i], + river_p.mannings_n_sq_at_edge[i], river_p.froude_limit, dt, ), @@ -392,13 +396,14 @@ function update_river_channel_flow!( river_v.zs_src[i] = river_p.zb[i_src] + river_v.h[i_src] river_v.zs_dst[i] = river_p.zb[i_dst] + river_v.h[i_dst] - river_v.zs_max[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) - river_v.water_depth_at_edge[i] = (river_v.zs_max[i] - river_p.zb_max[i]) + river_v.zs_max_at_edge[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) + river_v.water_depth_at_edge[i] = + (river_v.zs_max_at_edge[i] - river_p.zb_max_at_edge[i]) - river_v.flow_area[i] = + river_v.flow_area_at_edge[i] = river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] # flow area (rectangular channel) - river_v.hydraulic_radius[i] = - river_v.flow_area[i] / wetted_perimeter_channel( + river_v.hydraulic_radius_at_edge[i] = + river_v.flow_area_at_edge[i] / wetted_perimeter_channel( river_v.water_depth_at_edge[i], river_p.flow_width_at_edge[i], ) # hydraulic radius (rectangular channel) @@ -406,10 +411,10 @@ function update_river_channel_flow!( river_v.q[i] = ifelse( river_v.water_depth_at_edge[i] > river_p.h_thresh, manning_flow( - river_p.mannings_n[i], - river_v.hydraulic_radius[i], + river_p.mannings_n_at_edge[i], + river_v.hydraulic_radius_at_edge[i], river_p.slope_at_edge[i], - river_v.flow_area[i], + river_v.flow_area_at_edge[i], ), 0.0, ) @@ -441,7 +446,7 @@ function update_floodplain_flow!( @batch per = thread minbatch = 1000 for i in 1:length(floodplain_v.water_depth_at_edge) floodplain_v.water_depth_at_edge[i] = - max(river_v.zs_max[i] - floodplain_p.zb_max[i], 0.0) + max(river_v.zs_max_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) end n = active_floodplain_cells(river_flow_model) @@ -469,9 +474,9 @@ function update_floodplain_flow!( i1, i2, ) - floodplain_v.flow_area[i] = min(a_src, a_dst) + floodplain_v.flow_area_at_edge[i] = min(a_src, a_dst) - floodplain_v.hydraulic_radius[i] = if a_src < a_dst + floodplain_v.hydraulic_radius_at_edge[i] = if a_src < a_dst a_src / compute_wetted_perimeter( profile, floodplain_v.water_depth_at_edge[i], @@ -487,16 +492,16 @@ function update_floodplain_flow!( ) end - floodplain_v.q[i] = if floodplain_v.flow_area[i] > 1.0e-05 + floodplain_v.q[i] = if floodplain_v.flow_area_at_edge[i] > 1.0e-05 local_inertial_flow( floodplain_v.q_previous[i], river_v.zs_src[i], river_v.zs_dst[i], floodplain_v.water_depth_at_edge[i], - floodplain_v.flow_area[i], - floodplain_v.hydraulic_radius[i], + floodplain_v.flow_area_at_edge[i], + floodplain_v.hydraulic_radius_at_edge[i], river_p.flow_length_at_edge[i], - floodplain_p.mannings_n_sq[i], + floodplain_p.mannings_n_sq_at_edge[i], river_p.froude_limit, dt, ) @@ -543,7 +548,7 @@ function update_floodplain_flow!( @batch per = thread minbatch = 1000 for i in 1:length(floodplain_v.water_depth_at_edge) floodplain_v.water_depth_at_edge[i] = - max(river_v.zs_max[i] - floodplain_p.zb_max[i], 0.0) + max(river_v.zs_max_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) end n = active_floodplain_cells(river_flow_model) @@ -571,9 +576,9 @@ function update_floodplain_flow!( i1, i2, ) - floodplain_v.flow_area[i] = min(a_src, a_dst) + floodplain_v.flow_area_at_edge[i] = min(a_src, a_dst) - floodplain_v.hydraulic_radius[i] = if a_src < a_dst + floodplain_v.hydraulic_radius_at_edge[i] = if a_src < a_dst a_src / compute_wetted_perimeter( profile, floodplain_v.water_depth_at_edge[i], @@ -589,12 +594,12 @@ function update_floodplain_flow!( ) end - floodplain_v.q[i] = if floodplain_v.flow_area[i] > 1.0e-05 + floodplain_v.q[i] = if floodplain_v.flow_area_at_edge[i] > 1.0e-05 manning_flow( - floodplain_p.mannings_n[i], - floodplain_v.hydraulic_radius[i], + floodplain_p.mannings_n_at_edge[i], + floodplain_v.hydraulic_radius_at_edge[i], floodplain_p.slope_at_edge[i], - floodplain_v.flow_area[i], + floodplain_v.flow_area_at_edge[i], ) else 0.0 @@ -862,9 +867,9 @@ end # number of cells [-] n::Int # effective flow width x direction at edge (floodplain) [m] - xwidth::Vector{Float64} + xwidth_at_edge::Vector{Float64} # effective flow width y direction at edge (floodplain) [m] - ywidth::Vector{Float64} + ywidth_at_edge::Vector{Float64} # acceleration due to gravity [m s⁻²] g::Float64 = 9.80665 # weighting factor (de Almeida et al., 2012) [-] @@ -872,11 +877,11 @@ end # depth threshold for calculating flow [m] h_thresh::Float64 # maximum cell elevation at edge [m] (x direction) - zx_max::Vector{Float64} + zx_max_at_edge::Vector{Float64} # maximum cell elevation at edge [m] (y direction) - zy_max::Vector{Float64} + zy_max_at_edge::Vector{Float64} # Manning's roughness squared at edge [(s m-1/3)2] - mannings_n_sq::Vector{Float64} + mannings_n_sq_at_edge::Vector{Float64} # elevation [m] of cell z::Vector{Float64} # if true a check is performed if froude number > 1.0 (algorithm is modified) [-] @@ -915,16 +920,16 @@ function LocalInertialOverlandFlowParameters( elevation = elevation_2d[indices] n = length(domain.land.network.indices) - zx_max = zeros(n) - zy_max = zeros(n) + zx_max_at_edge = zeros(n) + zy_max_at_edge = zeros(n) for i in 1:n idx_right = edge_indices.idx_right[i] if idx_right <= n - zx_max[i] = max(elevation[i], elevation[idx_right]) + zx_max_at_edge[i] = max(elevation[i], elevation[idx_right]) end idx_up = edge_indices.idx_up[i] if idx_up <= n - zy_max[i] = max(elevation[i], elevation[idx_up]) + zy_max_at_edge[i] = max(elevation[i], elevation[idx_up]) end end @@ -936,13 +941,13 @@ function LocalInertialOverlandFlowParameters( set_effective_flowwidth!(we_x, we_y, domain) parameters = LocalInertialOverlandFlowParameters(; n, - xwidth = we_x, - ywidth = we_y, + xwidth_at_edge = we_x, + ywidth_at_edge = we_y, theta, h_thresh = waterdepth_threshold, - zx_max, - zy_max, - mannings_n_sq = mannings_n .* mannings_n, + zx_max_at_edge, + zy_max_at_edge, + mannings_n_sq_at_edge = mannings_n .* mannings_n, z = elevation, froude_limit, ) @@ -1036,7 +1041,8 @@ function stable_timestep( ) dt_min = Inf (; alpha_coefficient) = river_flow_model.timestepping - (; n, mannings_n) = river_flow_model.parameters + #TODO: apply this on edges? + (; n, mannings_n_at_edge) = river_flow_model.parameters (; h) = river_flow_model.variables (; flow_length, flow_width, slope) = parameters @@ -1044,7 +1050,7 @@ function stable_timestep( @batch per = thread reduction = ((min, dt_min),) for i in 1:(n) @fastmath @inbounds h_r = (flow_width[i] * h[i]) / wetted_perimeter_channel(h[i], flow_width[i]) - celerity = beta * 1.0/mannings_n[i] * pow(h_r, 2.0/3.0) * sqrt(slope[i]) + celerity = beta * 1.0/mannings_n_at_edge[i] * pow(h_r, 2.0/3.0) * sqrt(slope[i]) dt = alpha_coefficient * flow_length[i] / celerity dt_min = min(dt, dt_min) end @@ -1191,8 +1197,8 @@ Update flow for flow_area single direction in the local inertial overland flow m if is_x_direction upstream_idx = indices.idx_right[i] downstream_idx = indices.idx_left[i] - width = land_p.ywidth[i] - z_max = land_p.zx_max[i] + width_at_edge = land_p.ywidth_at_edge[i] + z_max_at_edge = land_p.zx_max_at_edge[i] length_vec = x_length q_current = land_v.qx q_prev = land_v.qx0 @@ -1200,8 +1206,8 @@ Update flow for flow_area single direction in the local inertial overland flow m else upstream_idx = indices.idx_up[i] downstream_idx = indices.idx_down[i] - width = land_p.xwidth[i] - z_max = land_p.zy_max[i] + width_at_edge = land_p.xwidth_at_edge[i] + z_max_at_edge = land_p.zy_max_at_edge[i] length_vec = y_length q_current = land_v.qy q_prev = land_v.qy0 @@ -1210,14 +1216,14 @@ Update flow for flow_area single direction in the local inertial overland flow m # the effective flow width is zero when the river width exceeds the cell width and # floodplain flow is not calculated. - if upstream_idx <= land_p.n && width != 0.0 + if upstream_idx <= land_p.n && width_at_edge != 0.0 zs_current = land_p.z[i] + land_v.h[i] zs_upstream = land_p.z[upstream_idx] + land_v.h[upstream_idx] - zs_max = max(zs_current, zs_upstream) - water_depth_at_edge = (zs_max - z_max) + zs_max_at_edge = max(zs_current, zs_upstream) + water_depth_at_edge = (zs_max_at_edge - z_max_at_edge) if water_depth_at_edge > land_p.h_thresh - length = 0.5 * (length_vec[i] + length_vec[upstream_idx]) # can be precalculated + length_at_edge = 0.5 * (length_vec[i] + length_vec[upstream_idx]) # can be precalculated q_current[i] = local_inertial_flow( land_p.theta, q_prev[i], @@ -1226,9 +1232,9 @@ Update flow for flow_area single direction in the local inertial overland flow m zs_current, zs_upstream, water_depth_at_edge, - width, - length, - land_p.mannings_n_sq[i], + width_at_edge, + length_at_edge, + land_p.mannings_n_sq_at_edge[i], land_p.froude_limit, dt, ) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 8a2c5ec33..95bb55462 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -457,11 +457,15 @@ end froude_limit = true, h_thresh = 0.001, zb = [315.1000061035156, 314.3999938964844, 278.8000183105469], - zb_max = [315.1000061035156, 314.3999938964844], + zb_max_at_edge = [315.1000061035156, 314.3999938964844], bankfull_storage = [57155.32165002823, 100397.03622722626, 90180.89622545242], bankfull_depth = [1.0, 1.0, 1.0], - mannings_n_sq = [0.0008999999597668652, 0.0008999999597668652], - mannings_n = [0.029999999329447746, 0.029999999329447746, 0.029999999329447746], + mannings_n_sq_at_edge = [0.0008999999597668652, 0.0008999999597668652], + mannings_n_at_edge = [ + 0.029999999329447746, + 0.029999999329447746, + 0.029999999329447746, + ], flow_length_at_edge = [831.578125, 1005.890625], flow_width_at_edge = [94.73094177246094, 94.73094177246094], ), @@ -501,10 +505,10 @@ end @test dt ≈ 909.829412320351 @test river_flow_model.variables.zs_src ≈ [315.14484852086804, 0.0] @test river_flow_model.variables.zs_dst ≈ [314.3999938964844, 0.0] - @test river_flow_model.variables.zs_max ≈ [315.14484852086804, 0.0] + @test river_flow_model.variables.zs_max_at_edge ≈ [315.14484852086804, 0.0] @test river_flow_model.variables.water_depth_at_edge ≈ [0.04484241735241312, 0.0] - @test river_flow_model.variables.flow_area ≈ [4.247964427147839, 0.0] - @test river_flow_model.variables.hydraulic_radius ≈ [0.04480000374545946, 0.0] + @test river_flow_model.variables.flow_area_at_edge ≈ [4.247964427147839, 0.0] + @test river_flow_model.variables.hydraulic_radius_at_edge ≈ [0.04480000374545946, 0.0] @test river_flow_model.variables.q ≈ [0.534558444239785, 0.0] @test river_flow_model.variables.q_cumulative ≈ [486.35699517356477, 0.0] @@ -574,11 +578,15 @@ end froude_limit = true, h_thresh = 0.001, zb = [165.10784077644348, 165.10784077644348, 165.10784077644348], - zb_max = [165.10784077644348, 165.10784077644348], + zb_max_at_edge = [165.10784077644348, 165.10784077644348], bankfull_storage = [107921.00118967971, 200292.17449130033, 69688.46493603183], bankfull_depth = [1.592156171798706, 1.592156171798706, 1.592156171798706], - mannings_n_sq = [0.0008999999597668652, 0.0008999999597668652], - mannings_n = [0.029999999329447746, 0.029999999329447746, 0.029999999329447746], + mannings_n_sq_at_edge = [0.0008999999597668652, 0.0008999999597668652], + mannings_n_at_edge = [ + 0.029999999329447746, + 0.029999999329447746, + 0.029999999329447746, + ], flow_length_at_edge = [648.828125, 568.34375], flow_width_at_edge = [149.17837524414062, 149.17837524414062], ), @@ -627,9 +635,9 @@ end 828.5732065990505 981.3369851412588 1304.3660916852448; ], ), - mannings_n = [0.072, 0.072, 0.072], - mannings_n_sq = [0.005184, 0.005184], - zb_max = [166.6999969482422, 166.6999969482422], + mannings_n_at_edge = [0.072, 0.072, 0.072], + mannings_n_sq_at_edge = [0.005184, 0.005184], + zb_max_at_edge = [166.6999969482422, 166.6999969482422], ), variables = Wflow.FloodPlainStaggeredVariables(; n, @@ -664,11 +672,13 @@ end @test dt ≈ 49.40931052556788 @test river_flow_model.variables.zs_src ≈ [166.98963199894177, 166.92754760786679] @test river_flow_model.variables.zs_dst ≈ [166.92754760786679, 166.86980277988906] - @test river_flow_model.variables.zs_max ≈ [166.98963199894177, 166.92754760786679] + @test river_flow_model.variables.zs_max_at_edge ≈ + [166.98963199894177, 166.92754760786679] @test river_flow_model.variables.water_depth_at_edge ≈ [1.8817912224982933, 1.8197068314233036] - @test river_flow_model.variables.flow_area ≈ [280.7225571209805, 271.4609085323917] - @test river_flow_model.variables.hydraulic_radius ≈ + @test river_flow_model.variables.flow_area_at_edge ≈ + [280.7225571209805, 271.4609085323917] + @test river_flow_model.variables.hydraulic_radius_at_edge ≈ [1.8354842671202385, 1.7763698223484754] @test river_flow_model.variables.q ≈ [137.1827776559179, 133.7538757670657] @test river_flow_model.variables.q_cumulative ≈ [6778.106459961183, 6608.686781773178] @@ -678,9 +688,9 @@ end @test river_flow_model.floodplain.variables.water_depth_at_edge ≈ [0.2896350506995873, 0.22755065962459753] @test river_flow_model.floodplain.variables.hf_index == [1, 2] - @test river_flow_model.floodplain.variables.flow_area ≈ + @test river_flow_model.floodplain.variables.flow_area_at_edge ≈ [55.72535396228429, 117.78030190165774] - @test river_flow_model.floodplain.variables.hydraulic_radius ≈ + @test river_flow_model.floodplain.variables.hydraulic_radius_at_edge ≈ [0.28876564013944644, 0.22735076093732] @test river_flow_model.floodplain.variables.q ≈ [3.3074672215578524, 6.1421232455587536] @@ -730,13 +740,17 @@ end ), parameters = Wflow.LocalInertialOverlandFlowParameters(; n, - ywidth = [926.6857061478484, 869.7426481339323, 812.7995901200163], - xwidth = [], - zx_max = [257.3280029296875, 232.67100524902344, 232.67100524902344], + ywidth_at_edge = [926.6857061478484, 869.7426481339323, 812.7995901200163], + xwidth_at_edge = [], + zx_max_at_edge = [257.3280029296875, 232.67100524902344, 232.67100524902344], theta = 1.0, h_thresh = 1e-3, - zy_max = [], - mannings_n_sq = [0.24167056670421605, 0.2883451232664811, 0.3928782408368683], + zy_max_at_edge = [], + mannings_n_sq_at_edge = [ + 0.24167056670421605, + 0.2883451232664811, + 0.3928782408368683, + ], z = [257.3280029296875, 227.5050048828125, 232.67100524902344], froude_limit = true, ), @@ -795,13 +809,13 @@ end ), parameters = Wflow.LocalInertialOverlandFlowParameters(; n = n_land, - xwidth = [], - ywidth = [], + xwidth_at_edge = [], + ywidth_at_edge = [], theta = 1.0, h_thresh = 1e-3, - zx_max = [], - zy_max = [], - mannings_n_sq = [], + zx_max_at_edge = [], + zy_max_at_edge = [], + mannings_n_sq_at_edge = [], z = [], froude_limit = true, ), @@ -819,11 +833,11 @@ end froude_limit = true, h_thresh = 1e-3, zb = [], - zb_max = [], + zb_max_at_edge = [], bankfull_storage = [171137.5821314017], bankfull_depth = [1.3683528900146484], - mannings_n_sq = [], - mannings_n = [], + mannings_n_sq_at_edge = [], + mannings_n_at_edge = [], flow_length_at_edge = [], flow_width_at_edge = [], ), @@ -1001,8 +1015,8 @@ end active_n = collect(1:(n - 1)), active_e = collect(1:n_edges), h_thresh, - zb_max = zb_max_at_edge, - mannings_n_sq = mannings_n_sq_at_edge, + zb_max_at_edge, + mannings_n_sq_at_edge, flow_width_at_edge, flow_length_at_edge, bankfull_storage = fill(Wflow.MISSING_VALUE, n), @@ -1052,7 +1066,7 @@ end A = 0.04970535373017763 R = 0.0011733219820725962 length = 533.453125 - mannings_n_sq = 0.0008999999597668652 + mannings_n_sq_at_edge = 0.0008999999597668652 froude_limit = true dt = 89.29563868855615 @@ -1064,7 +1078,7 @@ end A, R, length, - mannings_n_sq, + mannings_n_sq_at_edge, froude_limit, dt, ) ≈ 0.005331926324969742 @@ -1079,7 +1093,7 @@ end water_depth_at_edge = 0.00310727852479431 width = 620.6649135473787 length = 926.602742473319 - mannings_n_sq = 0.1773345894316103 + mannings_n_sq_at_edge = 0.1773345894316103 froude_limit = true dt = 49.774905820268735 @@ -1093,7 +1107,7 @@ end water_depth_at_edge, width, length, - mannings_n_sq, + mannings_n_sq_at_edge, froude_limit, dt, ) ≈ 0.00017992597962222483 @@ -1108,7 +1122,7 @@ end A = 0.04970535373017763 R = 0.0011733219820725962 length = 533.453125 - mannings_n_sq = 0.0008999999597668652 + mannings_n_sq_at_edge = 0.0008999999597668652 froude_limit = true dt = 89.29563868855615 @@ -1120,7 +1134,7 @@ end A, R, length, - mannings_n_sq, + mannings_n_sq_at_edge, froude_limit, dt, ) ≈ 0.005331926324969742 @@ -1135,7 +1149,7 @@ end water_depth_at_edge = 0.00310727852479431 width = 620.6649135473787 length = 926.602742473319 - mannings_n_sq = 0.1773345894316103 + mannings_n_sq_at_edge = 0.1773345894316103 froude_limit = true dt = 49.774905820268735 @@ -1149,7 +1163,7 @@ end water_depth_at_edge, width, length, - mannings_n_sq, + mannings_n_sq_at_edge, froude_limit, dt, ) ≈ 0.00017992597962222483 From 9c2c9f5ab618048c117cf957a3c92bf7acc6e3e7 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 4 Jun 2026 13:20:40 +0200 Subject: [PATCH 39/90] Explain `FloodPlainModel` `domain` --- Wflow/src/routing/surface/floodplain.jl | 27 +++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 5a0eb6085..8b7d9da6f 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -2,6 +2,22 @@ abstract type AbstractFloodPlainModel end abstract type AbstractFloodPlainParameters end abstract type AbstractFloodPlainVariables end +"""" + FloodPlainModel + +Floodplain flow model for each river cell (as part of a `RiverFlowModel`). The +`FloodPlainModel` is defined on an unstructured staggered or unstaggered grid. +""" +@with_kw struct FloodPlainModel{ + T <: AbstractRoutingMethod, + P <: AbstractFloodPlainParameters, + V <: AbstractFloodPlainVariables, +} <: AbstractFloodPlainModel + routing_method::T + parameters::P + variables::V +end + """ FloodPlainProfile @@ -218,17 +234,6 @@ end error::Vector{Float64} = zeros(n) end -"Floodplain flow model" -@with_kw struct FloodPlainModel{ - T <: AbstractRoutingMethod, - P <: AbstractFloodPlainParameters, - V <: AbstractFloodPlainVariables, -} <: AbstractFloodPlainModel - routing_method::T - parameters::P - variables::V -end - "Struct to store floodplain parameters" @with_kw struct FloodPlainParameters <: AbstractFloodPlainParameters # floodplain profile From 680fb280b5e1de1fb2405b59bc71c888d4fb8c75 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 4 Jun 2026 14:22:18 +0200 Subject: [PATCH 40/90] Address review comment Make use of const `LDD_PIT`. --- Wflow/src/Wflow.jl | 2 ++ Wflow/src/network.jl | 6 +++--- Wflow/src/routing/surface/floodplain.jl | 2 +- Wflow/src/routing/surface/surface_staggered_scheme.jl | 2 +- Wflow/src/routing/utils.jl | 2 +- Wflow/test/routing_process.jl | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Wflow/src/Wflow.jl b/Wflow/src/Wflow.jl index 668511613..54445ead5 100644 --- a/Wflow/src/Wflow.jl +++ b/Wflow/src/Wflow.jl @@ -75,6 +75,8 @@ const VERSION = VersionNumber(TOML.parsefile(joinpath(@__DIR__, "..", "Project.toml"))["version"]) const GRAVITATIONAL_ACCELERATION = 9.80665 # m s⁻² +# local drain direction pit [-] +const LDD_PIT = 5 mutable struct Clock{T} time::T diff --git a/Wflow/src/network.jl b/Wflow/src/network.jl index e9f916da7..34d4f788d 100644 --- a/Wflow/src/network.jl +++ b/Wflow/src/network.jl @@ -109,7 +109,7 @@ nthreads > 1 to run the kinematic wave parallel, otherwise it is equal to the co domain. """ function network_subdomains(config::Config, network::NetworkLand) - pit_inds = findall(x -> x == 5, network.local_drain_direction) + pit_inds = findall(x -> x == LDD_PIT, network.local_drain_direction) order_of_subdomains, subdomain_inds, toposort_subdomain = kinwave_set_subdomains( network.graph, network.order, @@ -238,7 +238,7 @@ nthreads > 1 to run the kinematic wave parallel, otherwise it is equal to the co domain. """ function network_subdomains(config::Config, network::NetworkRiver) - pit_inds = findall(x -> x == 5, network.local_drain_direction) + pit_inds = findall(x -> x == LDD_PIT, network.local_drain_direction) order_of_subdomains, subdomain_inds, toposort_subdomain = kinwave_set_subdomains( network.graph, network.order, @@ -254,7 +254,7 @@ end "Initialize `NodesAtEdge`" function NodesAtEdge(network::NetworkRiver) - index_pit = findall(x -> x == 5, network.local_drain_direction) + index_pit = findall(x -> x == LDD_PIT, network.local_drain_direction) add_vertex_edge_graph!(network.graph, index_pit) nodes_at_edge = NodesAtEdge(; adjacent_nodes_at_edge(network.graph)...) return nodes_at_edge, index_pit diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 8b7d9da6f..01c80537a 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -374,7 +374,7 @@ function FloodPlainModel( (; indices, local_drain_direction, graph) = domain.network (; river_routing) = config.model n = length(indices) - index_pit = findall(x -> x == 5, local_drain_direction) + index_pit = findall(x -> x == LDD_PIT, local_drain_direction) n_edges = ne(graph) parameters = FloodPlainStaggeredParameters(dataset, config, domain, zb_floodplain, index_pit) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 25d578c10..38a160a63 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -108,7 +108,7 @@ function RiverFlowStaggeredParameters( n = length(indices) n_edges = ne(graph) active_index = findall(x -> x == 0, reservoir_outlet) - index_pit = findall(x -> x == 5, local_drain_direction) + index_pit = findall(x -> x == LDD_PIT, local_drain_direction) mannings_n, bankfull_depth, bankfull_elevation, flow_length, flow_width = get_river_parameters(dataset, config, domain, index_pit) diff --git a/Wflow/src/routing/utils.jl b/Wflow/src/routing/utils.jl index 007352dba..236159d61 100644 --- a/Wflow/src/routing/utils.jl +++ b/Wflow/src/routing/utils.jl @@ -11,7 +11,7 @@ function flowgraph(ldd::AbstractVector, indices::AbstractVector, PCR_DIR::Abstra for (from_node, from_index) in enumerate(indices) ldd_val = ldd[from_node] # skip pits to prevent cycles - ldd_val == 5 && continue + ldd_val == LDD_PIT && continue to_index = from_index + PCR_DIR[ldd_val] # find the node id of the downstream cell to_node = searchsortedfirst(indices, to_index) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 95bb55462..87aa2b139 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -78,7 +78,7 @@ end # a topological sort is used for visiting nodes in order from upstream to downstream toposort = topological_sort_by_dfs(graph) sink = toposort[end] - @test ldd[sink] == 5 # the most downstream node must be a sink + @test ldd[sink] == Wflow.LDD_PIT # the most downstream node must be a sink # calculate parameters of kinematic wave q = 0.000001 From 953eacd512500653342f41aa40e1c532133a1e8c Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 4 Jun 2026 15:32:56 +0200 Subject: [PATCH 41/90] Address review comment --- Wflow/src/routing/surface/surface_process.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wflow/src/routing/surface/surface_process.jl b/Wflow/src/routing/surface/surface_process.jl index 8810a04ea..a70ba7717 100644 --- a/Wflow/src/routing/surface/surface_process.jl +++ b/Wflow/src/routing/surface/surface_process.jl @@ -172,6 +172,6 @@ function local_inertial_flow( end function manning_flow(mannings_n, hydraulic_radius, slope, area) - q = 1.0 / mannings_n * pow(hydraulic_radius, 2.0 / 3.0) * sqrt(slope) * area + q = cbrt(hydraulic_radius^2) * sqrt(slope) * area / mannings_n return q end From e7d3047e9047565e0d6a537c106cf276fd645acc Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 4 Jun 2026 16:07:36 +0200 Subject: [PATCH 42/90] Remove `Float64` `hydraulic_radius_pow` --- Wflow/src/routing/surface/surface_kinwave.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index 54f9b94d2..418b7ddbc 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -34,7 +34,7 @@ function ManningFlowParameters( slope::Vector{Float64}, wetted_perimeter::Vector{Float64}, ) - hydraulic_radius_pow = Float64(2.0 / 3.0) + hydraulic_radius_pow = 2.0 / 3.0 alpha_term = @. pow(mannings_n / sqrt(slope), BETA_KINWAVE) alpha_pow = hydraulic_radius_pow * BETA_KINWAVE alpha = @. alpha_term * pow(wetted_perimeter, alpha_pow) From a19a519ba07679af210ce7b7054757136292c970 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 4 Jun 2026 16:29:54 +0200 Subject: [PATCH 43/90] Address review comment Remove `pow` from `celerity` computation. --- Wflow/src/routing/surface/surface_staggered_scheme.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 38a160a63..cbee14a9e 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -1050,7 +1050,7 @@ function stable_timestep( @batch per = thread reduction = ((min, dt_min),) for i in 1:(n) @fastmath @inbounds h_r = (flow_width[i] * h[i]) / wetted_perimeter_channel(h[i], flow_width[i]) - celerity = beta * 1.0/mannings_n_at_edge[i] * pow(h_r, 2.0/3.0) * sqrt(slope[i]) + celerity = beta * cbrt(h_r^2) * sqrt(slope[i])/mannings_n_at_edge[i] dt = alpha_coefficient * flow_length[i] / celerity dt_min = min(dt, dt_min) end From 3bfa352b97b80804e9d6fe502658fce1a4b6f5e9 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 4 Jun 2026 16:51:30 +0200 Subject: [PATCH 44/90] Use constant `BETA_KINWAVE` for `celerity` computation --- Wflow/src/routing/surface/surface_staggered_scheme.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index cbee14a9e..4d142343e 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -1046,11 +1046,10 @@ function stable_timestep( (; h) = river_flow_model.variables (; flow_length, flow_width, slope) = parameters - beta = 5.0/3.0 @batch per = thread reduction = ((min, dt_min),) for i in 1:(n) @fastmath @inbounds h_r = (flow_width[i] * h[i]) / wetted_perimeter_channel(h[i], flow_width[i]) - celerity = beta * cbrt(h_r^2) * sqrt(slope[i])/mannings_n_at_edge[i] + celerity = cbrt(h_r^2) * sqrt(slope[i]) / mannings_n_at_edge[i] / BETA_KINWAVE dt = alpha_coefficient * flow_length[i] / celerity dt_min = min(dt, dt_min) end From a951f2ed022e6f30dc25e08533814d4058e6c65e Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 4 Jun 2026 17:28:26 +0200 Subject: [PATCH 45/90] Compute stable timestep using variables at edges For `RiverFlowModel{<:ManningStaggered}`. --- .../surface/surface_staggered_scheme.jl | 22 ++++++++++++------- Wflow/test/run_sbm.jl | 22 +++++++++---------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 4d142343e..08ccc4275 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -1041,16 +1041,22 @@ function stable_timestep( ) dt_min = Inf (; alpha_coefficient) = river_flow_model.timestepping - #TODO: apply this on edges? - (; n, mannings_n_at_edge) = river_flow_model.parameters - (; h) = river_flow_model.variables - (; flow_length, flow_width, slope) = parameters + (; + active_e, + mannings_n_at_edge, + flow_length_at_edge, + flow_width_at_edge, + slope_at_edge, + ) = river_flow_model.parameters + (; water_depth_at_edge) = river_flow_model.variables - @batch per = thread reduction = ((min, dt_min),) for i in 1:(n) + @batch per = thread reduction = ((min, dt_min),) for i in active_e @fastmath @inbounds h_r = - (flow_width[i] * h[i]) / wetted_perimeter_channel(h[i], flow_width[i]) - celerity = cbrt(h_r^2) * sqrt(slope[i]) / mannings_n_at_edge[i] / BETA_KINWAVE - dt = alpha_coefficient * flow_length[i] / celerity + (flow_width_at_edge[i] * water_depth_at_edge[i]) / + wetted_perimeter_channel(water_depth_at_edge[i], flow_width_at_edge[i]) + celerity = + cbrt(h_r^2) * sqrt(slope_at_edge[i]) / mannings_n_at_edge[i] / BETA_KINWAVE + dt = alpha_coefficient * flow_length_at_edge[i] / celerity dt_min = min(dt, dt_min) end dt_min = isinf(dt_min) ? 60.0 : dt_min diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index 53d7dda0e..552f4bea6 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -1163,18 +1163,18 @@ end Wflow.run_timestep!(model) (; q_average, h) = river_flow.variables - @test sum(q_average) ≈ 2184.8957510389937 - @test q_average[1622] ≈ 0.00021891756973974456 - @test q_average[43] ≈ 10.353169686665648 - @test q_average[501] ≈ 0.021410389047065236 - @test q_average[5808] ≈ 0.004923123282944491 - @test h[1622] ≈ 0.0017956698949444315 - @test h[43] ≈ 1.3192079305303674 - @test h[501] ≈ 0.005400538309955543 - @test h[5808] ≈ 0.005894305999311178 + @test sum(q_average) ≈ 2184.304753007843 + @test q_average[1622] ≈ 0.0002190265220138185 + @test q_average[43] ≈ 10.354333905435187 + @test q_average[501] ≈ 0.02141198454056767 + @test q_average[5808] ≈ 0.004920531981445287 + @test h[1622] ≈ 0.0017967526822043184 + @test h[43] ≈ 1.3192174478288576 + @test h[501] ≈ 0.005400600297111819 + @test h[5808] ≈ 0.005892191643178794 (; q_average, h) = river_flow.floodplain.variables - @test maximum(q_average) ≈ 1.0232013166337701 - @test maximum(h) ≈ 1.1458011481426886 + @test maximum(q_average) ≈ 1.0162290359867245 + @test maximum(h) ≈ 1.149051908326585 end @testitem "Kinematic river flow including 1D floodplain schematization" begin From bc618140d20322993d07fa869325297c9a175ff3 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 4 Jun 2026 20:27:54 +0200 Subject: [PATCH 46/90] Remove magic number In `stable_timestep` functions. --- Wflow/src/routing/surface/surface_kinwave.jl | 3 ++- Wflow/src/routing/surface/surface_staggered_scheme.jl | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index 418b7ddbc..393ddb141 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -665,6 +665,7 @@ function stable_timestep( n = length(q) stable_timesteps .= Inf + dt_min_default = 600.0 k = 0 for i in 1:n if q[i] > KIN_WAVE_MIN_FLOW @@ -679,7 +680,7 @@ function stable_timestep( elseif k > 0 quantile!(@view(stable_timesteps[1:k]), p) else - 600.0 + dt_min_default end return dt_min diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 08ccc4275..f38467cd7 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -1000,6 +1000,7 @@ function stable_timestep( parameters::RiverParameters, ) dt_min = Inf + dt_min_default = 60.0 (; alpha_coefficient) = river_flow_model.timestepping (; n) = river_flow_model.parameters (; h) = river_flow_model.variables @@ -1009,7 +1010,7 @@ function stable_timestep( alpha_coefficient * flow_length[i] / sqrt(GRAVITATIONAL_ACCELERATION * h[i]) dt_min = min(dt, dt_min) end - dt_min = isinf(dt_min) ? 60.0 : dt_min + dt_min = isinf(dt_min) ? dt_min_default : dt_min return dt_min end @@ -1018,6 +1019,7 @@ function stable_timestep( parameters::LandParameters, ) dt_min = Inf + dt_min_default = 60.0 (; alpha_coefficient) = overland_flow_model.timestepping (; n) = overland_flow_model.parameters (; x_length, y_length, river_location) = parameters @@ -1031,7 +1033,7 @@ function stable_timestep( end dt_min = min(dt, dt_min) end - dt_min = isinf(dt_min) ? 60.0 : dt_min + dt_min = isinf(dt_min) ? dt_min_default : dt_min return dt_min end @@ -1040,6 +1042,7 @@ function stable_timestep( parameters::RiverParameters, ) dt_min = Inf + dt_min_default = 60.0 (; alpha_coefficient) = river_flow_model.timestepping (; active_e, @@ -1059,7 +1062,7 @@ function stable_timestep( dt = alpha_coefficient * flow_length_at_edge[i] / celerity dt_min = min(dt, dt_min) end - dt_min = isinf(dt_min) ? 60.0 : dt_min + dt_min = isinf(dt_min) ? dt_min_default : dt_min return dt_min end From 156c4c7b65ac749cfbc90e5dd1c21cffc3c79bec Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 4 Jun 2026 21:38:13 +0200 Subject: [PATCH 47/90] Address review comment --- Wflow/src/routing/utils.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Wflow/src/routing/utils.jl b/Wflow/src/routing/utils.jl index 236159d61..e56186535 100644 --- a/Wflow/src/routing/utils.jl +++ b/Wflow/src/routing/utils.jl @@ -162,7 +162,7 @@ function flux_in!(flux_in, flux, network) end function compute_value_at_edge(v, nodes_at_edge, n_edges, func::Function) - x = fill(Float64(0), n_edges) + x = zeros(n_edges) for i in 1:n_edges src_node = nodes_at_edge.src[i] dst_node = nodes_at_edge.dst[i] @@ -172,7 +172,7 @@ function compute_value_at_edge(v, nodes_at_edge, n_edges, func::Function) end function compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) - mannings_n_at_edge = fill(Float64(0), n_edges) + mannings_n_at_edge = zeros(n_edges) for i in 1:n_edges src_node = nodes_at_edge.src[i] dst_node = nodes_at_edge.dst[i] @@ -186,7 +186,7 @@ function compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_ed end function compute_slope_at_edge(elev, length_at_edge, nodes_at_edge, n_edges) - slope = fill(Float64(0), n_edges) + slope = zeros(n_edges) for i in 1:n_edges src_node = nodes_at_edge.src[i] dst_node = nodes_at_edge.dst[i] From 0a85945064250860721ddf43c869ebd4095e2fbb Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 5 Jun 2026 14:44:46 +0200 Subject: [PATCH 48/90] Improve docstring for `FloodPlainModel` --- Wflow/src/routing/surface/floodplain.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 01c80537a..ce75e130a 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -6,7 +6,15 @@ abstract type AbstractFloodPlainVariables end FloodPlainModel Floodplain flow model for each river cell (as part of a `RiverFlowModel`). The -`FloodPlainModel` is defined on an unstructured staggered or unstaggered grid. +`FloodPlainModel` can be part of three different river routing schemes: +* Local inertial equation on a staggered grid. +* Kinematic wave using Manning's equation on a staggered grid. +* Kinematic wave approach by solving the kinematic wave equation using Newton's method. + +On a staggered grid the river and floodplain flow routing scheme are equal. For the +kinematic wave river flow routing using Newton's method, the floodplain flow routing is +solved by using Manning's equation, as solving kinematic wave routing for near zero flows is +computationally expensive. """ @with_kw struct FloodPlainModel{ T <: AbstractRoutingMethod, From 8deb87dbb873c3f7bfe07bb9e850b9a0d4ce4d36 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 5 Jun 2026 15:11:32 +0200 Subject: [PATCH 49/90] Add docstrings related to `FloodPlainModel` --- Wflow/src/routing/surface/floodplain.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index ce75e130a..84a161dfb 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -12,9 +12,9 @@ Floodplain flow model for each river cell (as part of a `RiverFlowModel`). The * Kinematic wave approach by solving the kinematic wave equation using Newton's method. On a staggered grid the river and floodplain flow routing scheme are equal. For the -kinematic wave river flow routing using Newton's method, the floodplain flow routing is -solved by using Manning's equation, as solving kinematic wave routing for near zero flows is -computationally expensive. +kinematic wave river flow routing solved by using Newton's method, the floodplain flow +routing is solved by using Manning's equation, as solving kinematic wave routing for near +zero flows is computationally expensive. """ @with_kw struct FloodPlainModel{ T <: AbstractRoutingMethod, @@ -342,6 +342,7 @@ function compute_flood_depth( return flood_depth end +"Compute floodplain flow area (excluding river channel area)" function compute_floodplain_flow_area( profile::FloodPlainProfile, h::Float64, @@ -397,6 +398,10 @@ function FloodPlainModel( return floodplain_model end +""" +Initialize floodplain geometry, model variables and parameters for floodplain flow routing +as part of kinematic wave river flow routing (solved using Newton's method). +""" function FloodPlainModel(dataset::NCDataset, config::Config, domain::DomainRiver) (; indices) = domain.network n = length(indices) From b9c461203d6fdb2adbe8175b6ae9bbcdbfdc10f0 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 5 Jun 2026 17:33:01 +0200 Subject: [PATCH 50/90] Update and add docstrings For routing on a staggered grid. Also removed `cell` from functions, for a staggered grid typically `node` and `edge` terms are used. --- .../surface/surface_staggered_scheme.jl | 67 ++++++++++++------- Wflow/test/routing_process.jl | 12 ++-- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index f38467cd7..b7fcab525 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -34,8 +34,7 @@ flow_width_at_edge::Vector{Float64} = Float64[] end -abstract type AbstractFloodPlainModel end - +"Get river parameters for a river flow model on a staggered grid" function get_river_parameters( dataset::NCDataset, config::Config, @@ -76,6 +75,10 @@ function get_river_parameters( return mannings_n, bankfull_depth, bankfull_elevation, flow_length, flow_width end +""" +Generate info log message for river flow routing on a staggered grid, solving the local +inertial or Manning's equation. +""" function log_message_staggered_flow(config::Config) (; river_routing) = config.model waterdepth_threshold = config.model.river_water_flow_threshold__depth # depth threshold for flow at edge @@ -93,7 +96,7 @@ function log_message_staggered_flow(config::Config) return waterdepth_threshold, froude_limit end -"Initialize river flow model parameters on a staggered grid" +"Initialize river flow model parameters on a staggered grid." function RiverFlowStaggeredParameters( dataset::NCDataset, config::Config, @@ -151,7 +154,7 @@ function RiverFlowStaggeredParameters( return parameters end -"Struct for storing local inertial river flow model variables" +"Struct for storing river flow model variables on a staggered grid." @with_kw struct RiverFlowStaggeredVariables <: AbstractRiverFlowVariables n_cells::Int n_edges::Int @@ -187,6 +190,7 @@ end error::Vector{Float64} = zeros(n_cells) end +"Initialize river flow model variables on a staggered grid." function RiverFlowStaggeredVariables( dataset::NCDataset, config::Config, @@ -226,7 +230,7 @@ function RiverFlowStaggeredVariables( return variables end -"Initialize river flow model on a staggered grid" +"Initialize river flow model on a staggered grid." function init_staggered_river_flow( dataset::NCDataset, config::Config, @@ -530,7 +534,7 @@ function update_floodplain_flow!( end """ -Update floodplain flow for the manning river flow model on a staggered grid. +Update floodplain flow for a river flow model using Manning's equation on a staggered grid. """ function update_floodplain_flow!( river_flow_model::RiverFlowModel{T, F}, @@ -667,7 +671,8 @@ update_bc_reservoir_model!( ) = nothing """ -Update floodplain water depth and storage. +Update river and floodplain water depth and storage for river and floodplain models on a +staggered grid. """ function update_water_depth_and_storage!( floodplain_model::AbstractFloodPlainModel, @@ -716,7 +721,7 @@ update_water_depth_and_storage!( ) = nothing """ -Update water depth and storage for river. +Update water depth and storage for river flow model on a staggered grid. """ function update_water_depth_and_storage!( river_flow_model::RiverFlowModel{<:AbstractStaggeredRoutingMethod}, @@ -888,7 +893,7 @@ end froude_limit::Bool end -"Initialize shallow water overland flow model parameters" +"Initialize local inertial overland flow model parameters" function LocalInertialOverlandFlowParameters( dataset::NCDataset, config::Config, @@ -991,7 +996,7 @@ end stable_timestep(river_flow_model::RiverFlowModel{<:LocalInertial}, parameters::RiverParameters) stable_timestep(overland_flow_model::OverlandFlowModel{<:LocalInertial}, parameters::LandParameters) -Compute flow_area stable timestep size for the local inertial approach, based on Bates et al. (2010). +Compute stable timestep size for the local inertial approach, based on Bates et al. (2010). dt = α * (Δx / sqrt(g max(h)) """ @@ -1037,6 +1042,11 @@ function stable_timestep( return dt_min end +""" + stable_timestep(river_flow_model::RiverFlowModel{<:ManningStaggered}, parameters::RiverParameters) + +Compute stable timestep size for a river flow model using the Manning's equation on staggered grid. +""" function stable_timestep( river_flow_model::RiverFlowModel{<:ManningStaggered}, parameters::RiverParameters, @@ -1139,8 +1149,8 @@ function average_flow_vars!( end """ -Update combined local inertial river and overland flow models for a single timestep `dt`. An -adaptive timestepping method is used (computing a sub timestep `dt_s`). +Update local inertial river and overland flow model for a single timestep `dt`. An adaptive +timestepping method is used (computing a sub timestep `dt_s`). """ function update_overland_flow_model!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -1186,7 +1196,7 @@ function update_overland_flow_model!( end """ -Update flow for flow_area single direction in the local inertial overland flow model. +Update flow for the local inertial overland flow model at edge `i` in a single direction. `is_x_direction`: true for x-direction (idx_right/idx_left), false for y-direction (idx_up/idx_down) """ @inline function update_directional_flow!( @@ -1286,8 +1296,8 @@ function local_inertial_update_fluxes!( end """ -Update boundary condition inflow to a reservoir from land of combined local inertial river -and overland flow models for a single timestep. +Update boundary condition inflow to a reservoir from land of the local inertial river and +overland flow model for a single timestep. """ function update_inflow_reservoir!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -1310,7 +1320,8 @@ function update_inflow_reservoir!( end """ -Compute storage change for flow_area river cell from fluxes. +Compute storage change at node index `i` containing a river, from river and overland flow +fluxes of the local inertial river and overland flow model. """ @inline function compute_river_storage_change( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -1342,7 +1353,8 @@ Compute storage change for flow_area river cell from fluxes. end """ -Compute external inflow for river cells, including negative inflow (abstraction). +Compute external inflow at river node index `river_idx`, including negative inflow +(abstraction), of the local inertial river and overland flow model. Returns tuple: (inflow, abstraction_to_add) """ @inline function compute_external_inflow( @@ -1405,7 +1417,8 @@ Returns tuple: (river_h, land_h, river_storage) end """ -Compute storage change for flow_area land cell from horizontal fluxes and runoff. +Compute storage change for local inertial overland flow model at node index `i` (non-river) +from horizontal fluxes and runoff. """ @inline function compute_land_storage_change( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -1425,9 +1438,10 @@ Compute storage change for flow_area land cell from horizontal fluxes and runoff end """ -Update storage and water depth for flow_area single river cell. +Update storage and water depth for the local inertial overland flow model at node index `i` +containing a river. """ -@inline function update_river_cell_storage_and_depth!( +@inline function update_river_and_land_storage_and_depth!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, river_flow_model::RiverFlowModel{<:LocalInertial}, domain::Domain, @@ -1472,9 +1486,10 @@ Update storage and water depth for flow_area single river cell. end """ -Update storage and water depth for flow_area single land cell (non-river). +Update storage and water depth for local inertial overland flow model at node index `i` +(non-river). """ -@inline function update_land_cell_storage_and_depth!( +@inline function update_land_storage_and_depth!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, domain::DomainLand, i::Int, @@ -1500,8 +1515,8 @@ Update storage and water depth for flow_area single land cell (non-river). end """ -Update storage and water depth for combined local inertial river and overland flow models -for a single timestep `dt`. +Update storage and water depth for local inertial river and overland flow model for a single +timestep `dt`. """ function local_inertial_update_water_depth!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -1515,7 +1530,7 @@ function local_inertial_update_water_depth!( if river_location[i] # Process river cells (excluding reservoir outlets) if !reservoir_outlet[i] - update_river_cell_storage_and_depth!( + update_river_and_land_storage_and_depth!( overland_flow_model, river_flow_model, domain, @@ -1525,7 +1540,7 @@ function local_inertial_update_water_depth!( end else # Process land cells (non-river) - update_land_cell_storage_and_depth!(overland_flow_model, domain.land, i, dt) + update_land_storage_and_depth!(overland_flow_model, domain.land, i, dt) end end return nothing diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 87aa2b139..fd0651206 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -882,7 +882,7 @@ end dt = Wflow.stable_timestep(river_flow_model, domain.river.parameters) @test dt ≈ 201.394687315008 - # Test functions called by function update_river_cell_storage_and_depth! + # Test functions called by function update_river_and_land_storage_and_depth! storage_change = Wflow.compute_river_storage_change( overland_flow_model, river_flow_model, @@ -901,8 +901,8 @@ end @test Wflow.compute_water_depths(total_storage, 1, 3, river_flow_model, domain) |> collect ≈ [river_h_expected, land_h_expected, river_storage_expected] - # Test update_river_cell_storage_and_depth! - Wflow.update_river_cell_storage_and_depth!( + # Test update_river_and_land_storage_and_depth! + Wflow.update_river_and_land_storage_and_depth!( overland_flow_model, river_flow_model, domain, @@ -913,7 +913,7 @@ end @test overland_flow_model.variables.h[3] ≈ land_h_expected @test river_flow_model.variables.storage[1] ≈ river_storage_expected - # Test function called by function update_land_cell_storage_and_depth! + # Test function called by function update_land_storage_and_depth! @test Wflow.compute_land_storage_change( overland_flow_model, domain.land.network, @@ -921,8 +921,8 @@ end dt, ) ≈ 880.1704436259577 - # Test update_land_cell_storage_and_depth! - Wflow.update_land_cell_storage_and_depth!(overland_flow_model, domain.land, 2, dt) + # Test update_land_storage_and_depth! + Wflow.update_land_storage_and_depth!(overland_flow_model, domain.land, 2, dt) @test overland_flow_model.variables.storage[2] ≈ 784038.1273051423 @test overland_flow_model.variables.h[2] ≈ 1.3770166561681556 end From 5100d42c59a5d6a46a44227a107bc6477b181219 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 5 Jun 2026 17:46:34 +0200 Subject: [PATCH 51/90] Some more docstrings --- Wflow/src/routing/surface/surface_flow.jl | 3 +++ Wflow/src/routing/surface/surface_process.jl | 1 + 2 files changed, 4 insertions(+) diff --git a/Wflow/src/routing/surface/surface_flow.jl b/Wflow/src/routing/surface/surface_flow.jl index 4a93083c8..f9f25a8d8 100644 --- a/Wflow/src/routing/surface/surface_flow.jl +++ b/Wflow/src/routing/surface/surface_flow.jl @@ -33,6 +33,7 @@ end alpha::Vector{Float64} # Constant in momentum equation A = alpha*Q^beta, based on Manning's equation [s3/5 m1/5] end +"River flow model" @with_kw struct RiverFlowModel{ T <: AbstractRoutingMethod, F <: Union{AbstractFloodPlainModel, Nothing}, @@ -49,6 +50,7 @@ end allocation::A end +"Overland flow model" @with_kw struct OverlandFlowModel{ T <: AbstractRoutingMethod, B <: AbstractOverlandFlowBC, @@ -62,6 +64,7 @@ end variables::V end +"Compute wetted perimeter for a rectangular river channel " function wetted_perimeter_channel(h::Float64, flow_width::Float64) channel_perimeter = 2.0 * h + flow_width return channel_perimeter diff --git a/Wflow/src/routing/surface/surface_process.jl b/Wflow/src/routing/surface/surface_process.jl index a70ba7717..d56b97b84 100644 --- a/Wflow/src/routing/surface/surface_process.jl +++ b/Wflow/src/routing/surface/surface_process.jl @@ -171,6 +171,7 @@ function local_inertial_flow( return q end +"Compute flow using Manning's equation" function manning_flow(mannings_n, hydraulic_radius, slope, area) q = cbrt(hydraulic_radius^2) * sqrt(slope) * area / mannings_n return q From 860ed6172d20259c2cd51e675ffbe7785601b303 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 8 Jun 2026 09:09:20 +0200 Subject: [PATCH 52/90] Update `stable_timestep` staggered scheme --- .../surface/surface_staggered_scheme.jl | 20 ++++++++----------- Wflow/test/routing_process.jl | 9 +++++---- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index b7fcab525..6e0b64156 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -808,7 +808,7 @@ function update_river_flow_model!( update_h = true, ) (; reservoir) = river_flow_model.boundary_conditions - (; parameters) = domain.river + (; flow_length) = domain.river.parameters set_reservoir_vars!(reservoir) update_index_hq!(reservoir, clock) @@ -820,7 +820,7 @@ function update_river_flow_model!( t = 0.0 while t < dt - dt_s = stable_timestep(river_flow_model, parameters) + dt_s = stable_timestep(river_flow_model, flow_length) dt_s = check_timestepsize(dt_s, t, dt) staggered_scheme_river_update!(river_flow_model, domain, dt_s, update_h) t += dt_s @@ -993,7 +993,7 @@ function init_local_inertial_overland_flow( end """ - stable_timestep(river_flow_model::RiverFlowModel{<:LocalInertial}, parameters::RiverParameters) + stable_timestep(river_flow_model::RiverFlowModel{<:LocalInertial}, flow_length::Vector{Float64}) stable_timestep(overland_flow_model::OverlandFlowModel{<:LocalInertial}, parameters::LandParameters) Compute stable timestep size for the local inertial approach, based on Bates et al. (2010). @@ -1002,14 +1002,13 @@ dt = α * (Δx / sqrt(g max(h)) """ function stable_timestep( river_flow_model::RiverFlowModel{<:LocalInertial}, - parameters::RiverParameters, + flow_length::Vector{Float64}, ) dt_min = Inf dt_min_default = 60.0 (; alpha_coefficient) = river_flow_model.timestepping (; n) = river_flow_model.parameters (; h) = river_flow_model.variables - (; flow_length) = parameters @batch per = thread reduction = ((min, dt_min),) for i in 1:(n) @fastmath @inbounds dt = alpha_coefficient * flow_length[i] / sqrt(GRAVITATIONAL_ACCELERATION * h[i]) @@ -1043,14 +1042,11 @@ function stable_timestep( end """ - stable_timestep(river_flow_model::RiverFlowModel{<:ManningStaggered}, parameters::RiverParameters) + stable_timestep(river_flow_model::RiverFlowModel{<:ManningStaggered}, _) Compute stable timestep size for a river flow model using the Manning's equation on staggered grid. """ -function stable_timestep( - river_flow_model::RiverFlowModel{<:ManningStaggered}, - parameters::RiverParameters, -) +function stable_timestep(river_flow_model::RiverFlowModel{<:ManningStaggered}, _) dt_min = Inf dt_min_default = 60.0 (; alpha_coefficient) = river_flow_model.timestepping @@ -1161,7 +1157,7 @@ function update_overland_flow_model!( update_h = false, ) (; reservoir) = river_flow_model.boundary_conditions - river_parameters = domain.river.parameters + (; flow_length) = domain.river.parameters land_parameters = domain.land.parameters set_reservoir_vars!(reservoir) @@ -1171,7 +1167,7 @@ function update_overland_flow_model!( t = 0.0 while t < dt - dt_river = stable_timestep(river_flow_model, river_parameters) + dt_river = stable_timestep(river_flow_model, flow_length) dt_land = stable_timestep(overland_flow_model, land_parameters) dt_s = min(dt_river, dt_land) dt_s = check_timestepsize(dt_s, t, dt) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index fd0651206..34119abf8 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -498,7 +498,7 @@ end network = Wflow.NetworkReservoir(; river_indices = [2]), ), ) - dt = Wflow.stable_timestep(river_flow_model, domain.river.parameters) + dt = Wflow.stable_timestep(river_flow_model, domain.river.parameters.flow_length) Wflow.update_river_channel_flow!(river_flow_model, domain.river, dt) @@ -665,7 +665,7 @@ end ), reservoir = Wflow.DomainReservoir(; network = Wflow.NetworkReservoir()), ) - dt = Wflow.stable_timestep(river_flow_model, domain.river.parameters) + dt = Wflow.stable_timestep(river_flow_model, domain.river.parameters.flow_length) Wflow.update_river_channel_flow!(river_flow_model, domain.river, dt) @@ -879,7 +879,7 @@ end ), ) - dt = Wflow.stable_timestep(river_flow_model, domain.river.parameters) + dt = Wflow.stable_timestep(river_flow_model, domain.river.parameters.flow_length) @test dt ≈ 201.394687315008 # Test functions called by function update_river_and_land_storage_and_depth! @@ -1042,10 +1042,11 @@ end # run until steady state is reached epsilon = 1e-12 + (; flow_length) = domain_river.parameters while true sw_river.boundary_conditions.inwater[1] = 20.0 h0 = mean(sw_river.variables.h) - dt = Wflow.stable_timestep(sw_river, domain_river.parameters) + dt = Wflow.stable_timestep(sw_river, flow_length) Wflow.staggered_scheme_river_update!(sw_river, domain, dt, true) d = abs(h0 - mean(sw_river.variables.h)) if d <= epsilon From 1add29c4a011894ebf4d2e68a46128715cff112c Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 8 Jun 2026 09:15:14 +0200 Subject: [PATCH 53/90] Add small comment --- Wflow/src/routing/surface/surface_staggered_scheme.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 6e0b64156..94c1920a7 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -1063,6 +1063,7 @@ function stable_timestep(river_flow_model::RiverFlowModel{<:ManningStaggered}, _ @fastmath @inbounds h_r = (flow_width_at_edge[i] * water_depth_at_edge[i]) / wetted_perimeter_channel(water_depth_at_edge[i], flow_width_at_edge[i]) + # estimate wave celerity for wide rectangular channel celerity = cbrt(h_r^2) * sqrt(slope_at_edge[i]) / mannings_n_at_edge[i] / BETA_KINWAVE dt = alpha_coefficient * flow_length_at_edge[i] / celerity From 810bda538f61b11fa2f51aeda5179058b9b29399 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 8 Jun 2026 09:42:52 +0200 Subject: [PATCH 54/90] Add docstring and renaming function --- Wflow/src/routing/surface/surface_kinwave.jl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index 393ddb141..f3a966522 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -552,7 +552,11 @@ function kinwave_river_update!( end end -function river_floodplain_exchange!( +""" +Exchange of channel-floodpain water (assumed instantaneously) based on total storage and +river channel capacity (`bankfull_storage`). +""" +function river_channel_floodplain_exchange!( river_flow_model::RiverFlowModel{T, F}, parameters::RiverParameters, dt::Float64, @@ -583,7 +587,7 @@ function river_floodplain_exchange!( end end -river_floodplain_exchange!( +river_channel_floodplain_exchange!( river_flow_model::RiverFlowModel{T, F}, parameters::RiverParameters, dt::Float64, @@ -621,7 +625,7 @@ function update_river_flow_model!( adaptive ? stable_timestep(river_flow_model, flow_length, 0.05) : river_flow_model.timestepping.dt_fixed dt_s = check_timestepsize(dt_s, t, dt) - river_floodplain_exchange!(river_flow_model, domain.river.parameters, dt_s) + river_channel_floodplain_exchange!(river_flow_model, domain.river.parameters, dt_s) kinwave_river_update!(river_flow_model, domain.river, dt_s) update_floodplain_model!(river_flow_model, domain.river, dt) t += dt_s From 6f3a2714b689c2be741d37065623dbd39063847b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 8 Jun 2026 15:17:55 +0200 Subject: [PATCH 55/90] Update docs routing on staggered grid --- docs/_quarto.yml | 2 +- ...{local-inertial.qmd => staggered-grid.qmd} | 107 +++++++++++++----- 2 files changed, 82 insertions(+), 27 deletions(-) rename docs/model_docs/routing/{local-inertial.qmd => staggered-grid.qmd} (69%) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 9c351eeec..ff6991370 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -109,7 +109,7 @@ website: - text: Routing concepts contents: - file: model_docs/routing/kinwave.qmd - - file: model_docs/routing/local-inertial.qmd + - file: model_docs/routing/staggered-grid.qmd - file: model_docs/routing/reservoirs.qmd - file: model_docs/routing/gwf.qmd - file: model_docs/routing/sediment_flux.qmd diff --git a/docs/model_docs/routing/local-inertial.qmd b/docs/model_docs/routing/staggered-grid.qmd similarity index 69% rename from docs/model_docs/routing/local-inertial.qmd rename to docs/model_docs/routing/staggered-grid.qmd index 7a69231df..68a9434e1 100644 --- a/docs/model_docs/routing/local-inertial.qmd +++ b/docs/model_docs/routing/staggered-grid.qmd @@ -1,8 +1,11 @@ --- -title: Local inertial +title: Staggered grid --- +This section provides an overview of available flow routing schemes on a staggered grid. + ## River and floodplain routing +### Local inertial approximation The local inertial approximation of shallow water flow neglects only the convective acceleration term in the Saint-Venant momentum conservation equation. The numerical solution of the local inertial approximation on a staggered grid is as follows (Bates et al., 2010): @@ -18,17 +21,17 @@ time step, $\SIb{Q_t}{m^3\ s^{-1}}$ is the river flow at the previous time step, the water surface slope at the previous time step and $\SIb{n}{m^{-\frac{1}{3}}\ s}$ is the Manning's roughness coefficient. -The momentum equation is applied to each link between two river grid cells, while the -continuity equation over $\Delta t$ is applied to each river cell: +The momentum equation is applied to each edge between two river nodes, while the continuity +equation over $\Delta t$ is applied to each river node: $$ h^{t+\Delta t} = h^t + \Delta t \frac{\subtext{Q^{t+\Delta t}}{src} - \subtext{Q^{t+\Delta t}}{dst}}{A} $$ -where $\SIb{h^{t+\Delta t}}{m}$ is the water depthat time step $t+\Delta t$, $\SIb{h^t}{m}$ +where $\SIb{h^{t+\Delta t}}{m}$ is the water depth at time step $t+\Delta t$, $\SIb{h^t}{m}$ is the water depth at the previous time step, $\SIb{A}{m^2}$ is the river area and $\SIb{\subtext{Q}{src}}{m^3\ s^{-1}}$ and $\SIb{\subtext{Q}{dst}}{m^3\ s^{-1}}$ represent -river flow at the upstream and downstream link of the river cell, respectively. +river flow at the upstream and downstream edge of the river node, respectively. The model time step $\Delta t$ for the local inertial model is estimated based on the Courant-Friedrichs-Lewy condition (Bates et al., 2010): @@ -37,8 +40,8 @@ $$ \Delta t = \alpha \min\left(\frac{\Delta x_i}{\sqrt{gh_i}}\right) $$ -where $\sqrt{gh_i}$ is the wave celerity for river cell $i$, $\SIb{\Delta x_i}{m}$ is the -river length for river cell $i$ and $\alpha$ is a coefficient (typically between $0.2$ and +where $\sqrt{gh_i}$ is the wave celerity for river node $i$, $\SIb{\Delta x_i}{m}$ is the +river length for river node $i$ and $\alpha$ is a coefficient (typically between $0.2$ and $0.7$) to enhance the stability of the simulation. In the TOML file the following properties related to the local inertial model can be provided @@ -66,10 +69,8 @@ model_boundary_condition_river_bank_water__depth = "riverdepth_bc" # optional r These boundary conditions and river bed elevation and Manning's roughness coefficient are copied to a ghost node (downstream of the river outlet node) in the code. -The optional 1D floodplain schematization is based on provided flood volumes as a function of -flood depth (per flood depth interval) for each river cell. Wflow calculates from these flood -volumes a rectangular floodplain profile for each flood depth interval. Routing is done -separately for the river channel and floodplain. +It is possible to include a [1D floodplain +schematization](#optional-1d-floodplain-schematization) for this routing type. The momentum equation is most stable for low slope environments, and to keep the simulation stable for (partly) steep environments the `river_water_flow__froude_limit_flag` option is @@ -87,15 +88,69 @@ the water velocity from the local inertial model is causing the Froude number to than $1.0$ , the water velocity (and flow) is reduced in order to maintain a Froude number of $1.0$. +### Using Manning's equation +The use of Manning's equation (assuming that the topography controls water flow mostly) for +river and floodplain routing on a staggered grid is suitable for steep terrain where +backwater effects are negligible: + + +$$ +Q_{t+\Delta t} = n^{-1} R_t^{2/3} \sqrt{c_\mathrm{river\ slope}} A_t +$$ + +where $\SIb{Q_{t+\Delta t}}{m^3\ s^{-1}}$ is the river flow at time step $t+\Delta t$, +$\SIb{n}{m^{-\frac{1}{3}}\ s}$ is the Manning's roughness coefficient, $\SIb{R_t}{m}$ is the +hydraulic radius at the previous time step, $c_\mathrm{river\ slope}$ is the river bed +slope between nodes and $\SIb{A_t}{m^2}$ is the cross sectional flow area at the previous +time step. + +As for the [local inertial model](#local-inertial-river-and-floodplain-routing), the +momentum equation is applied to each edge between two river nodes, while the continuity +equation over $\Delta t$ is applied to each river node. + +The model time step $\Delta t$ for Manning's equation on a staggered grid is estimated as +follows assuming a wide rectangular river channel: + +$$ +\begin{gathered} +c_i = \frac{5}{3} n^{-1} R^{2/3} \sqrt{c_\mathrm{river\ slope}},\\ +\Delta t = \alpha \min\left(\frac{\Delta x_i}{c_i}\right) +\end{gathered} +$$ + +where $c_i$ is the wave celerity for river edge $i$, $\SIb{\Delta x_i}{m}$ is the river +length for river edge $i$ and $\alpha$ is a coefficient (typically between $0.2$ and $0.7$) +to enhance the stability of the simulation. + +In the TOML file the following properties related to Manning's flow on a staggered grid can +be provided for the `sbm` and `sbm_gwf` model types: + +```toml +[model] +river_routing = "manning_staggered" # default is "kinematic_wave" +river_staggered_manning_flow__alpha_coefficient = 0.5 # alpha coefficient for model stability (default = 0.7) +floodplain_1d__flag = true # include 1D floodplain schematization (default = false) +``` + +It is possible to include a [1D floodplain +schematization](#optional-1d-floodplain-schematization) for this routing type. + +## 1D floodplain schematization +The optional 1D floodplain schematization is based on provided flood volumes as a function +of flood depth (per flood depth interval) for each river node. Wflow calculates from these +flood volumes a rectangular floodplain profile for each flood depth interval. Routing is +done separately for the river channel and floodplain. + +## Reservoirs [Reservoir](./reservoirs.qmd) models representing reservoirs, (natural) lakes or other water -storage features can be included as part of the local inertial model for river flow (1D) and -river and overland flow combined (see next section). Reservoir models are included as a -boundary point with zero water depth for both river and overland flow. For river flow the -reservoir model replaces the local inertial model at the reservoir location, and river flow -$Q$ is set by the outflow from the reservoir. Overland flow at a reservoir location is not -allowed to or from the downstream river grid cell. - -## Overland flow (2D) +storage features can be included as part of 1D river flow and river and overland flow +combined (see next section) on a staggered grid. Reservoir models are included as a boundary +point with zero water depth for both local inertial river and overland flow routing. For +river flow the reservoir model replaces the river flow model at the reservoir location, and +river flow $Q$ is set by the outflow from the reservoir. Overland flow at a reservoir +location is not allowed to or from the downstream river node. + +## Local inertial overland flow (2D) For the simulation of 2D overland flow on a staggered grid the numerical scheme proposed by de Almeida et al. (2012) is adopted. The explicit solution for the estimation of water discharge between two cells in the x-direction is of the following form (following the notation of @@ -147,18 +202,18 @@ routing](#river-and-floodplain-routing) section of the local inertial model. ## External inflows External inflows $\SIb{}{m^3\ s^{-1}}$ for example water supply or abstractions can to added -the local inertial model for river flow (1D) and river and overland flow combined (1D-2D), -as a static or cyclic parameter or as part of forcing. For example, cyclic inflow can be -provided in the TOML file as follows: +the 1D river flow model on a staggered grid and local inertial river and overland flow +combined (1D-2D), as a static or cyclic parameter or as part of forcing. For example, cyclic +inflow can be provided in the TOML file as follows: ```toml [input.cyclic] river_water__external_inflow_volume_flow_rate = "river_inflow" ``` -These inflows are added or subtracted as part of the continuity equation of the local -inertial model. Abstractions are limited to a maximum of $\SI{80}{\%}$ of the river storage. -Negative external inflows are satisfied first, and the remaining river storage is available -for internal abstractions as part of water demand and allocation computations. +These inflows are added or subtracted as part of the continuity equation of river flow model +on a staggered grid. Abstractions are limited to a maximum of $\SI{80}{\%}$ of the river +storage. Negative external inflows are satisfied first, and the remaining river storage is +available for internal abstractions as part of water demand and allocation computations. ## Abstractions Internal abstractions $\SIb{}{m^3\ s^{-1}}$ from the river are possible when water demand and From db41a72aa69387e196b79da8836a0c4c2ba23e01 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 8 Jun 2026 15:29:34 +0200 Subject: [PATCH 56/90] Fix links staggered grid docs --- docs/model_docs/routing/staggered-grid.qmd | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/model_docs/routing/staggered-grid.qmd b/docs/model_docs/routing/staggered-grid.qmd index 68a9434e1..3ef60d660 100644 --- a/docs/model_docs/routing/staggered-grid.qmd +++ b/docs/model_docs/routing/staggered-grid.qmd @@ -69,8 +69,7 @@ model_boundary_condition_river_bank_water__depth = "riverdepth_bc" # optional r These boundary conditions and river bed elevation and Manning's roughness coefficient are copied to a ghost node (downstream of the river outlet node) in the code. -It is possible to include a [1D floodplain -schematization](#optional-1d-floodplain-schematization) for this routing type. +It is possible to include a [1D floodplain schematization] for this routing type. The momentum equation is most stable for low slope environments, and to keep the simulation stable for (partly) steep environments the `river_water_flow__froude_limit_flag` option is @@ -104,9 +103,9 @@ hydraulic radius at the previous time step, $c_\mathrm{river\ slope}$ is the ri slope between nodes and $\SIb{A_t}{m^2}$ is the cross sectional flow area at the previous time step. -As for the [local inertial model](#local-inertial-river-and-floodplain-routing), the -momentum equation is applied to each edge between two river nodes, while the continuity -equation over $\Delta t$ is applied to each river node. +As for the [local inertial model](#local-inertial-approximation), the momentum equation is +applied to each edge between two river nodes, while the continuity equation over $\Delta t$ +is applied to each river node. The model time step $\Delta t$ for Manning's equation on a staggered grid is estimated as follows assuming a wide rectangular river channel: @@ -132,8 +131,7 @@ river_staggered_manning_flow__alpha_coefficient = 0.5 # alpha coefficient for mo floodplain_1d__flag = true # include 1D floodplain schematization (default = false) ``` -It is possible to include a [1D floodplain -schematization](#optional-1d-floodplain-schematization) for this routing type. +It is possible to include a [1D floodplain schematization] for this routing type. ## 1D floodplain schematization The optional 1D floodplain schematization is based on provided flood volumes as a function From 5caf409083290463bc62441afaeb85e206867109 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 9 Jun 2026 08:21:37 +0200 Subject: [PATCH 57/90] Add docs floodplain routing kinematic wave --- docs/model_docs/routing/kinwave.qmd | 26 ++++++++++++++++++++++ docs/model_docs/routing/staggered-grid.qmd | 6 ++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/model_docs/routing/kinwave.qmd b/docs/model_docs/routing/kinwave.qmd index 8a9563f01..8c1747e19 100644 --- a/docs/model_docs/routing/kinwave.qmd +++ b/docs/model_docs/routing/kinwave.qmd @@ -76,6 +76,32 @@ kinematic_wave__adaptive_time_step_flag = true # optional, default is false [Reservoir](./reservoirs.qmd) models representing reservoirs, (natural) lakes or other water storage features can be included as part of the river kinematic wave network. +## Floodplain routing +It is possible to include a [floodplain +schematization](./staggered-grid.qmd#floodplain-schematization) as part of the kinematic +wave river flow routing, using the following setting in the TOML file: + +```toml +[model] +floodplain_1d__flag = true # include 1D floodplain schematization (default = false) +``` + +At the start of each time step, channel-floodplain water exchange, assumed to be +instantaneous, is computed based on total water volume and the channel storage capacity. The +exchange is added to the lateral inflow term of the kinematic wave equation. Routing is done +separately for the river channel and floodplain. Floodplain water is routed down slope using +the `accucapacityflux` function with a transport capacity based on Manning's equation: + +$$ +Q_{t+\Delta t} = n^{-1} R_t^{2/3} \sqrt{c_\mathrm{floodplain\ slope}} A_t +$$ + +where $\SIb{Q_{t+\Delta t}}{m^3\ s^{-1}}$ is the floodplain flow at time step $t+\Delta t$, +$\SIb{n}{m^{-\frac{1}{3}}\ s}$ is the Manning's roughness coefficient, $\SIb{R_t}{m}$ is the +hydraulic radius at the previous time step, $c_\mathrm{floodplain\ slope}$ is the +floodplain bed slope and $\SIb{A_t}{m^2}$ is the cross sectional flow area at the previous +time step. + ## External inflows External inflows $\SIb{}{m^3\ s^{-1}}$ for example water supply or abstractions can be added to the kinematic wave for river flow routing, as a static or cyclic parameter or as part of diff --git a/docs/model_docs/routing/staggered-grid.qmd b/docs/model_docs/routing/staggered-grid.qmd index 3ef60d660..ea04f399a 100644 --- a/docs/model_docs/routing/staggered-grid.qmd +++ b/docs/model_docs/routing/staggered-grid.qmd @@ -69,7 +69,7 @@ model_boundary_condition_river_bank_water__depth = "riverdepth_bc" # optional r These boundary conditions and river bed elevation and Manning's roughness coefficient are copied to a ghost node (downstream of the river outlet node) in the code. -It is possible to include a [1D floodplain schematization] for this routing type. +It is possible to include a [floodplain schematization] for this routing type. The momentum equation is most stable for low slope environments, and to keep the simulation stable for (partly) steep environments the `river_water_flow__froude_limit_flag` option is @@ -131,9 +131,9 @@ river_staggered_manning_flow__alpha_coefficient = 0.5 # alpha coefficient for mo floodplain_1d__flag = true # include 1D floodplain schematization (default = false) ``` -It is possible to include a [1D floodplain schematization] for this routing type. +It is possible to include a [floodplain schematization] for this routing type. -## 1D floodplain schematization +## Floodplain schematization The optional 1D floodplain schematization is based on provided flood volumes as a function of flood depth (per flood depth interval) for each river node. Wflow calculates from these flood volumes a rectangular floodplain profile for each flood depth interval. Routing is From 970c00a0265a15c204fd57b7367cea15728d6aed Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 9 Jun 2026 13:55:58 +0200 Subject: [PATCH 58/90] Update docs and introduce standard name `floodplain__slope` To support different slope values for river channel and floodplain for `kinematic_wave` routing. --- .../standard_name/standard_name_routing.jl | 29 ++++++++++++++++--- .../test/sbm_river-floodplain-kw_config.toml | 1 + Wflow/test/utils.jl | 3 ++ docs/model_docs/parameters_routing.qmd | 19 ++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/Wflow/src/standard_name/standard_name_routing.jl b/Wflow/src/standard_name/standard_name_routing.jl index 8fe4c6749..021923d55 100644 --- a/Wflow/src/standard_name/standard_name_routing.jl +++ b/Wflow/src/standard_name/standard_name_routing.jl @@ -408,14 +408,26 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( unit = Unit(; m = 3), dimname = :flood_depth, description = "Floodplain profile (cumulative volume per flood depth)", - tags = [:local_inertial_floodplain_1D_flow_input], + tags = [ + :local_inertial_floodplain_1D_flow_input, + :kinematic_wave_floodplain_1D_flow_input, + ], ), "floodplain_water_flow__manning_n_parameter" => ParameterMetadata(; lens = @optic(_.routing.river_flow.floodplain.parameters.mannings_n), unit = Unit(; s = 1, m = -1 // 3), default = 0.072, description = "Manning's roughness", - tags = [:local_inertial_floodplain_1D_flow_input], + tags = [ + :local_inertial_floodplain_1D_flow_input, + :kinematic_wave_floodplain_1D_flow_input, + ], + ), + "floodplain__slope" => ParameterMetadata(; + lens = @optic(_.routing.river_flow.floodplain.parameters.slope), + unit = Unit(; m = (1, 1)), + description = "Floodplain slope", + tags = [:kinematic_wave_floodplain_1D_flow_input], ), #### States "floodplain_water__instantaneous_volume_flow_rate" => ParameterMetadata(; @@ -425,6 +437,7 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( tags = [ :local_inertial_floodplain_1D_flow_state, :local_inertial_floodplain_1D_flow_output, + :kinematic_wave_floodplain_1D_flow_output, ], ), "floodplain_water__depth" => ParameterMetadata(; @@ -434,6 +447,8 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( tags = [ :local_inertial_floodplain_1D_flow_state, :local_inertial_floodplain_1D_flow_output, + :kinematic_wave_floodplain_1D_flow_state, + :kinematic_wave_floodplain_1D_flow_output, ], ), #### Output @@ -441,13 +456,19 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( lens = @optic(_.routing.river_flow.floodplain.variables.storage), unit = Unit(; m = 3), description = "Floodplain water volume", - tags = [:local_inertial_floodplain_1D_flow_output], + tags = [ + :local_inertial_floodplain_1D_flow_output, + :kinematic_wave_floodplain_1D_flow_output, + ], ), "floodplain_water__volume_flow_rate" => ParameterMetadata(; lens = @optic(_.routing.river_flow.floodplain.variables.q_average), unit = Unit(; m = 3, s = -1), description = "Floodplain discharge", - tags = [:local_inertial_floodplain_1D_flow_output], + tags = [ + :local_inertial_floodplain_1D_flow_output, + :kinematic_wave_floodplain_1D_flow_output, + ], ), ### Overland flow #### Input diff --git a/Wflow/test/sbm_river-floodplain-kw_config.toml b/Wflow/test/sbm_river-floodplain-kw_config.toml index b6465b0e6..ae4d1412e 100644 --- a/Wflow/test/sbm_river-floodplain-kw_config.toml +++ b/Wflow/test/sbm_river-floodplain-kw_config.toml @@ -95,6 +95,7 @@ river__slope = "RiverSlope" river__width = "wflow_riverwidth" river_bank_water__depth = "RiverDepth" river_bank_water__elevation = "RiverZ" +floodplain__slope = "RiverSlope" floodplain_water__sum_of_volume_per_depth = "floodplain_volume" land_surface_water_flow__manning_n_parameter = "N" diff --git a/Wflow/test/utils.jl b/Wflow/test/utils.jl index 9b98d2ded..e2ed79921 100644 --- a/Wflow/test/utils.jl +++ b/Wflow/test/utils.jl @@ -189,6 +189,9 @@ end "soil_exponential_vertical_saturated_hydraulic_conductivity_profile_below_surface__depth", "soil_layer_water__vertical_saturated_hydraulic_conductivity", ]) + elseif map_name == "routing" + # The lens of this standard name is valid but not part of any test model. + Set(["floodplain__slope"]) else Set{String}() end diff --git a/docs/model_docs/parameters_routing.qmd b/docs/model_docs/parameters_routing.qmd index cb2d4cd93..99ad9bce8 100644 --- a/docs/model_docs/parameters_routing.qmd +++ b/docs/model_docs/parameters_routing.qmd @@ -70,6 +70,25 @@ generate_table(Wflow.Routing, :kinematic_wave_river_state; relative_widths = [3, generate_table(Wflow.Routing, :kinematic_wave_river_output; relative_widths = [3, 3, 1, 1, 2]) ``` +### 1D floodplain flow +#### Input +```{julia} +# | code-fold: true +generate_table(Wflow.Routing, :kinematic_wave_floodplain_1D_flow_input; relative_widths = [6, 4, 2, 2, 2, 2]) +``` + +#### States +```{julia} +# | code-fold: true +generate_table(Wflow.Routing, :kinematic_wave_floodplain_1D_flow_state; relative_widths = [3, 3, 1, 1, 2]) +``` + +#### Output +```{julia} +# | code-fold: true +generate_table(Wflow.Routing, :kinematic_wave_floodplain_1D_flow_output; relative_widths = [3, 3, 1, 1, 2]) +``` + ### Overland flow #### Input ```{julia} From 3771eb69b849ee63a6d6a1c44887f3bfb67724ff Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 9 Jun 2026 14:08:27 +0200 Subject: [PATCH 59/90] Update routing_metadata json output --- utils/export_parameter_data/routing_metadata.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/utils/export_parameter_data/routing_metadata.json b/utils/export_parameter_data/routing_metadata.json index 75247ce9d..640b8f50e 100644 --- a/utils/export_parameter_data/routing_metadata.json +++ b/utils/export_parameter_data/routing_metadata.json @@ -1,4 +1,13 @@ { + "floodplain__slope": { + "unit": "m m-1", + "default": null, + "fill": null, + "type": "Float64", + "description": "Floodplain slope", + "allow_missing": false, + "allow_dynamic_input": false + }, "floodplain_water__depth": { "unit": "m", "default": null, From 79fe9feb04154079856b8c513c796a7729b933a4 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 9 Jun 2026 15:35:47 +0200 Subject: [PATCH 60/90] Update model_config docs --- docs/model_docs/model_configurations.qmd | 38 ++++++++++++++++-------- docs/user_guide/model_config.qmd | 22 +++++++++++--- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/docs/model_docs/model_configurations.qmd b/docs/model_docs/model_configurations.qmd index 5c38d21cb..e82fc8f4e 100644 --- a/docs/model_docs/model_configurations.qmd +++ b/docs/model_docs/model_configurations.qmd @@ -11,13 +11,22 @@ different model configurations are possible. The following model configurations supported in wflow: - wflow\_sbm: - - Land Hydrology `SBM` + kinematic wave for subsurface and surface flow - - Land Hydrology `SBM` + kinematic wave for subsurface and overland flow + local inertial river (+ - optional floodplain) - - Land Hydrology `SBM` + kinematic wave for subsurface flow + local inertial river (1D) and land (2D) - - Land Hydrology `SBM` + groundwater flow + kinematic wave for surface flow + + The Land Hydrology `SBM` concept can be combined with the following subsurface flow + concepts: + - kinematic wave lateral subsurface flow routing + - groundwater flow routing + + and with the following surface routing schemes: + - kinematic wave for overland and river flow + - kinematic wave for overland flow + local inertial river + - kinematic wave for overland flow + river flow on a staggered grid using Manning's equation + - local inertial river (1D) and land (2D) - wflow\_sediment as post processing of wflow\_sbm output + The river flow routing schemes support an optional 1D floodplain schematization (routing is +done separately for the river channel and floodplain). + Below, some explanation will be given on how to prepare a basic wflow\_sbm model. Example data for other model configurations is provided in the section with [sample data](../getting_started/download_example_models.qmd). @@ -59,16 +68,21 @@ four directions (adjacent cells) is used. This is described in more detail in th type = "sbm_gwf" ``` -### Local inertial river -By default the model types `sbm` and `sbm_gwf` use the kinematic wave approach for river flow. -There is also the option to use the local inertial model for river flow with an optional 1D -floodplain schematization (routing is done separately for the river channel and floodplain), by -providing the following in the TOML file: +### River flow on a staggered grid +By default the model types `sbm` and `sbm_gwf` use the kinematic wave approach for river +flow, including an optional 1D floodplain schematization. There is also the option to use +river flow routing on a staggered grid, with the following two routing schemes: + +- Local inertial routing +- Mannings flow + +Both support an optional 1D floodplain schematization. +The river routing scheme can be set as follows in the TOML file: ```toml [model] -river_routing = "local_inertial" # optional, default is "kinematic_wave" -floodplain_1d__flag = true # optional, default is false +river_routing = "local_inertial" # one of ["kinematic_wave", "local_inertial", "manning_staggered"], default is "kinematic_wave" +floodplain_1d__flag = true # optional, default is false ``` ### Local inertial river (1D) and land (2D) diff --git a/docs/user_guide/model_config.qmd b/docs/user_guide/model_config.qmd index 157eaff0b..f14f6646e 100644 --- a/docs/user_guide/model_config.qmd +++ b/docs/user_guide/model_config.qmd @@ -59,13 +59,12 @@ reservoir_water_surface__elevation = "waterlevel_reservoir" ``` ## Enabling floodplain routing -As part of the local inertial model for river flow. -### 1D floodplains +### 1D floodplains on a staggered grid ```toml [model] -river_routing = "local_inertial" +river_routing = "local_inertial" # one of ("local_inertial" or "manning_staggered") floodplain_1d__flag = true [input.static] @@ -77,7 +76,22 @@ floodplain_water__instantaneous_volume_flow_rate = "q_floodplain" floodplain_water__depth = "h_floodplain" ``` -### 2D floodplains +### 1D floodplains (kinematic wave routing) + +```toml +[model] +river_routing = "kinematic_wave" +floodplain_1d__flag = true + +[input.static] +floodplain_water__sum_of_volume_per_depth = "floodplain_volume" +floodplain_water_flow__manning_n_parameter = "floodplain_n" + +[state.variables] +floodplain_water__depth = "h_floodplain" +``` + +### 2D floodplains (local inertial routing) ```toml [model] From f87acd77236145fbd5715dbc248092df7fccaac8 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 9 Jun 2026 15:43:09 +0200 Subject: [PATCH 61/90] Small formatting --- docs/user_guide/toml_file.qmd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/user_guide/toml_file.qmd b/docs/user_guide/toml_file.qmd index 71677e36d..d0dc7066c 100644 --- a/docs/user_guide/toml_file.qmd +++ b/docs/user_guide/toml_file.qmd @@ -13,9 +13,9 @@ The file paths provided in this file are relative to the location of the TOML fi from Delft-FEWS](fews.qmd), the `fews_run__flag` setting needs to be set at `true`. ```toml -dir_input = "data/input" # optional, default is the path of the TOML -dir_output = "data/output" # optional, default is the path of the TOML -fews_run__flag = false # optional, default value is false +dir_input = "data/input" # optional, default is the path of the TOML +dir_output = "data/output" # optional, default is the path of the TOML +fews_run__flag = false # optional, default value is false ``` @@ -71,7 +71,7 @@ Model-specific settings can be included in the model section of the TOML file. ```toml [model] type = "sbm" # one of ("sbm" or "sbm_gwf") -snow_gravitational_transport__flag = true # include lateral snow transport in the model, default is false +snow_gravitational_transport__flag = true # include lateral snow transport in the model, default is false snow__flag = true # include snow modelling, default is false cold_start__flag = true # cold (cold_start__flag = true) or warm state (cold_start__flag = false), default is true reservoir__flag = true # include reservoir modelling, default is false From d345b23cd93a53739b801ca639a37d1fb7d54ccd Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 9 Jun 2026 16:27:20 +0200 Subject: [PATCH 62/90] Update faq docs --- docs/model_docs/model_configurations.qmd | 2 +- docs/user_guide/faq.qmd | 54 +++++++++++++----------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/docs/model_docs/model_configurations.qmd b/docs/model_docs/model_configurations.qmd index e82fc8f4e..b3cf7ba57 100644 --- a/docs/model_docs/model_configurations.qmd +++ b/docs/model_docs/model_configurations.qmd @@ -24,7 +24,7 @@ supported in wflow: - local inertial river (1D) and land (2D) - wflow\_sediment as post processing of wflow\_sbm output - The river flow routing schemes support an optional 1D floodplain schematization (routing is +The river flow routing schemes support an optional 1D floodplain schematization (routing is done separately for the river channel and floodplain). Below, some explanation will be given on how to prepare a basic wflow\_sbm model. Example diff --git a/docs/user_guide/faq.qmd b/docs/user_guide/faq.qmd index b8e48a3c1..7861ab784 100644 --- a/docs/user_guide/faq.qmd +++ b/docs/user_guide/faq.qmd @@ -34,7 +34,14 @@ output](./toml_file.qmd#output-netcdf-section). The choice of a specific river routing option can vary depending on the model and use case. However, the numerical properties of the routing schemes provide an indication of their -advantages and disadvantages. +advantages and disadvantages. The following river routing schemes are available: + +- Kinematic wave +- Mannings flow on a staggered grid +- Local inertial routing on a staggered grid + +These routing schemes support an optional 1D floodplain schematization (routing is done +separately for the river channel and floodplain). The kinematic wave method is driven by the river slope within each cell, assuming that the water surface slope is parallel to the bed slope. This results in sharper discharge peaks @@ -43,6 +50,10 @@ approach is especially suitable for steep (upstream) areas where flow propagatio dominated by topography. The kinematic wave approach does not include any form of backwater effects. +As the kinematic wave method, the use of Manning's equation on a staggered grid is also +driven by the river slope and thus most suitable for steep terrain where backwater effects +are negligible. + On the other hand, the local inertial method incorporates the slope of the water surface into the momentum equation. This results in 'damping effects' on flow propagating through the cells and yields better results in scenarios where the water surface slope differs from @@ -51,19 +62,16 @@ slope is limited or during the propagation of larger flood waves where the water slope is greater than the river slope itself. The local inertial method does incorporate backwater effects, although not as comprehensively as the full dynamic wave equation. -Generally, the kinematic wave method is computationally faster than the local inertial -method for river (and land) routing. For the multi-threading execution of the kinematic -wave, solved with a nonlinear scheme using Newton's method, the order of execution -(sub-basins) is important (from upstream to downstream sub-basin). For the momentum equation -of the local inertial approach an explicit numerical method is used and can be solved -independently for each cell during multi-threading execution. As a consequence, speedups for -local inertial routing are larger than for kinematic wave routing using multi-threading -(compared to a single thread run) and the difference between local inertial and kinematic -wave routing run times gets smaller as the number of threads increase. - -Currently, only the local inertial approach supports the inclusion of floodplains in river -routing, see also [this -question](./faq.qmd#what-is-the-difference-between-1d-2d-and-no-floodplains). +Generally, the kinematic wave method and the use of Manning's equation on a staggered grid +is computationally faster than the local inertial method for river (and land) routing. For +the multi-threading execution of the kinematic wave, solved with a nonlinear scheme using +Newton's method, the order of execution (sub-basins) is important (from upstream to +downstream sub-basin). For the momentum equation of river flow on a staggered grid an +explicit numerical method is used and can be solved independently for each cell during +multi-threading execution. As a consequence, speedups for river flow on a staggered grid are +larger than for kinematic wave routing using multi-threading (compared to a single thread +run) and the difference between river flow on a staggered grid and kinematic wave routing +run times gets smaller as the number of threads increase. ### Which land routing option should I choose? @@ -85,11 +93,11 @@ combination of kinematic wave for land routing and local inertial for river rout Effects of floodplain flow can be included in several ways. A one-dimensional sub-grid approximation (hence the name 1D floodplains) can be included when kinematic wave land -routing is combined with local inertial river routing. The water surface elevations of the -river channel and the floodplain are the same and it is assumed that water is exchanged -instantaneously between the river channel and the floodplain. When the water depth in the -river channel rises above the river bankfull depth, water is exchanged between the river -channel and the floodplain resulting in an attenuation of the discharge peaks. +routing is combined with river routing. The water surface elevations of the river channel +and the floodplain are the same and it is assumed that water is exchanged instantaneously +between the river channel and the floodplain. When the water depth in the river channel +rises above the river bankfull depth, water is exchanged between the river channel and the +floodplain resulting in an attenuation of the discharge peaks. ![Schematization of the 1D floodplain concept (side view)](../images/floodplains_1d.png) @@ -110,8 +118,6 @@ the 1D floodplain method can be used instead. ![Schematization of the 2D floodplain concept (top view)](../images/floodplains_2d.png) -It is also possible to run the model without 1D or 2D floodplains. This means that the model -is using kinematic wave for land routing and either kinematic wave or local inertial for -river routing. This can lead to unrealistically large water depths when the bankfull -capacity is exceeded as there is no exchange of water between the river channel and (1D or -2D) floodplains. +It is also possible to run the model without 1D floodplains. This can lead to +unrealistically large water depths when the bankfull capacity is exceeded as there is no +exchange of water between the river channel and floodplains. From a4b5b7092e20cc0a7ce05a10dd4cf43d9dd4e299 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 10 Jun 2026 08:07:20 +0200 Subject: [PATCH 63/90] Remove flow_area from test description --- Wflow/test/routing_process.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 34119abf8..462b6ecd9 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -553,7 +553,7 @@ end @test river_flow_model.variables.h ≈ [0.03653704705843743, 0.0, 0.10966599406601261] end -@testitem "unit: local inertial river flow with flow_area floodplain" begin +@testitem "unit: local inertial river flow with floodplain" begin # Test local inertial river routing with a floodplain on a 3-node graph (1 → 2 → 3). # Each sub-step of the local inertial update is called and verified individually: # 1. update_river_channel_flow! — edge discharge, water surface elevations From 753729cf7110bf976c077cb52514ba1e3842b28b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 10 Jun 2026 15:40:49 +0200 Subject: [PATCH 64/90] Use `floodplain__slope` for kinematic wave floodplain --- Wflow/src/routing/surface/floodplain.jl | 6 +++++- Wflow/src/routing/surface/surface_kinwave.jl | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 84a161dfb..d2a547d13 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -248,6 +248,8 @@ end profile::FloodPlainProfile # manning's roughness[s m-1/3] mannings_n::Vector{Float64} = Float64[] + # slope [m m⁻¹] + slope::Vector{Float64} = Float64[] end "Struct to store floodplain variables" @@ -413,7 +415,9 @@ function FloodPlainModel(dataset::NCDataset, config::Config, domain::DomainRiver Routing; sel = indices, ) - parameters = FloodPlainParameters(; profile, mannings_n) + slope = ncread(dataset, config, "floodplain__slope", Routing; sel = indices) + clamp!(slope, 0.00001, Inf) + parameters = FloodPlainParameters(; profile, mannings_n, slope) variables = FloodPlainVariables(; n) floodplain_model = FloodPlainModel(; routing_method = KinematicWave(), parameters, variables) diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index f3a966522..6bc935ee9 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -390,8 +390,8 @@ function update_floodplain_model!( dt::Float64, ) where {T <: KinematicWave, F <: FloodPlainModel{<:KinematicWave}} (; floodplain) = river_flow_model - (; slope, flow_length) = domain.parameters - (; profile, mannings_n) = floodplain.parameters + (; flow_length) = domain.parameters + (; profile, mannings_n, slope) = floodplain.parameters (; flow_capacity, h, q, q_cumulative, qin, qin_cumulative, storage) = floodplain.variables From 06770365696eb62fa2d82879f09ada0940b1557c Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 11 Jun 2026 10:10:39 +0200 Subject: [PATCH 65/90] Add unit test river flow with floodplain For river flow on a staggered grid using Manning's equation. --- Wflow/test/routing_process.jl | 185 ++++++++++++++++++++++++++++++++-- 1 file changed, 179 insertions(+), 6 deletions(-) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index c3c93106a..e60f7fa92 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -581,12 +581,9 @@ end zb_max_at_edge = [165.10784077644348, 165.10784077644348], bankfull_storage = [107921.00118967971, 200292.17449130033, 69688.46493603183], bankfull_depth = [1.592156171798706, 1.592156171798706, 1.592156171798706], + mannings_n = [0.029999999329447746, 0.029999999329447746, 0.029999999329447746], mannings_n_sq_at_edge = [0.0008999999597668652, 0.0008999999597668652], - mannings_n_at_edge = [ - 0.029999999329447746, - 0.029999999329447746, - 0.029999999329447746, - ], + mannings_n_at_edge = [0.029999999329447746, 0.029999999329447746], flow_length_at_edge = [648.828125, 568.34375], flow_width_at_edge = [149.17837524414062, 149.17837524414062], ), @@ -635,7 +632,8 @@ end 828.5732065990505 981.3369851412588 1304.3660916852448; ], ), - mannings_n_at_edge = [0.072, 0.072, 0.072], + mannings_n = [0.072, 0.072, 0.072], + mannings_n_at_edge = [0.072, 0.072], mannings_n_sq_at_edge = [0.005184, 0.005184], zb_max_at_edge = [166.6999969482422, 166.6999969482422], ), @@ -727,6 +725,181 @@ end [0.2449102618909183, 0.22760349104031377, 0.20085420755385677] end +@testitem "unit: river flow on a staggered grid using Manning's equation with floodplain" begin + # Test river flow on a staggered grid using Manning's equation with a floodplain on a + # 3-node graph (1 → 2 → 3). + # Each sub-step of the update is called and verified individually: + # 1. update_river_channel_flow! — edge discharge, water surface elevations + # 2. update_floodplain_flow! — floodplain discharge and geometry + # 3. update_bc_reservoir_model! — no reservoir + # 4. update_water_depth_and_storage! + n = 3 + river_flow_model = Wflow.RiverFlowModel(; + routing_method = Wflow.ManningStaggered(), + timestepping = Wflow.TimeStepping(; alpha_coefficient = 0.7), + boundary_conditions = Wflow.RiverFlowBC(; + n, + external_inflow = zeros(n), + inwater = [0.5469041025637776, 1.2789538388011605, 0.1723191462506401], + reservoir = nothing, + ), + parameters = Wflow.RiverFlowStaggeredParameters(; + n, + n_edges = 2, + active_n = [1, 2, 3], + active_e = [1, 2], + h_thresh = 0.001, + zb = [415.6000061035156, 415.6000061035156, 415.6000061035156], + zb_max_at_edge = [415.6000061035156, 415.6000061035156], + bankfull_storage = [36208.125, 30283.125, 25717.5], + bankfull_depth = [1.0, 1.0, 1.0], + mannings_n = [0.029999999329447746, 0.029999999329447746, 0.029999999329447746], + mannings_n_at_edge = [0.029999999329447746, 0.029999999329447746], + flow_length_at_edge = [1108.1875, 933.34375], + flow_width_at_edge = [30.0, 30.0], + slope_at_edge = [1.0e-5, 1.0e-5], + ), + variables = Wflow.RiverFlowStaggeredVariables(; + n_cells = n, + n_edges = 2, + h = [1.0029472866230495, 2.432593354336019, 0.40220945912422745], + storage = [36314.840722458204, 73666.52862352695, 10343.82176502732], + q = [12.586919865724491, 12.586919865724491], + water_depth_at_edge = [2.4325367293470777, 2.4325367293470777], + ), + floodplain = Wflow.FloodPlainModel(; + routing_method = Wflow.ManningStaggered(), + parameters = Wflow.FloodPlainStaggeredParameters(; + profile = Wflow.FloodPlainProfile(; + depth = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5], + storage = [ + 0.0 0.0 0.0 + 37399.0 36242.0 34514.0; + 79402.0 73635.0 69028.0; + 150174.0 111029.0 103542.0; + 233029.0 155325.0 138056.0; + 341777.0 212852.0 172570.0; + ], + width = [ + 30.0 30.0 30.0; + 61.97338304593237 71.8063277815615 80.52260134149898; + 69.60260991144943 74.08680577054052 80.52260134149898; + 117.27533530112372 74.08878707200793 80.52260134149898; + 137.2979131065196 87.76372980001238 80.52260134149898; + 180.20485733519754 113.9783295152003 80.52260134149898; + ], + flow_area = [ + 0.0 0.0 0.0; + 30.986691522966186 35.90316389078075 40.26130067074949; + 65.7879964786909 72.94656677605101 80.52260134149898; + 124.42566412925277 109.99096031205498 120.78390201224848; + 193.07462068251255 153.87282521206117 161.04520268299797; + 283.1770493501113 210.86198996966132 201.30650335374747; + ], + wetted_perimeter = [ + 31.973383045932373 41.80632778156151 50.522601341498984; + 32.97338304593237 42.80632778156151 51.522601341498984; + 41.602609911449434 46.08680577054052 52.522601341498984; + 90.27533530112372 47.08878707200793 53.522601341498984; + 111.29791310651959 61.763729800012385 54.522601341498984; + 155.20485733519754 88.9783295152003 55.522601341498984; + ], + ), + mannings_n = [0.072, 0.072, 0.072], + mannings_n_at_edge = [0.072, 0.072], + zb_max_at_edge = [416.6000061035156, 416.6000061035156], + slope_at_edge = [1.0e-5, 1.0e-5], + ), + variables = Wflow.FloodPlainStaggeredVariables(; + n, + n_edges = 2, + q = [2.0759155671111853, 3.279029180032925], + water_depth_at_edge = [1.4325367293470777, 1.4325367293470777], + h = [0.0029472866230494782, 1.432593354336019, 0.0], + storage = [113.73542237265065, 62604.388160555245, 0.0], + ), + ), + allocation = Wflow.AllocationRiverModel(; n), + ) + domain = Wflow.Domain(; + river = Wflow.DomainRiver(; + network = Wflow.NetworkRiver(; + nodes_at_edge = Wflow.NodesAtEdge(; src = [1, 2], dst = [2, 3]), + edges_at_node = Wflow.EdgesAtNode(; + src = [[], [1], [2]], + dst = [[1], [2], []], + ), + ), + parameters = Wflow.RiverParameters(; + flow_width = [30.0, 30.0, 30.0], + flow_length = [1206.9375, 1009.4375, 857.25], + ), + ), + reservoir = Wflow.DomainReservoir(; network = Wflow.NetworkReservoir()), + ) + dt = Wflow.stable_timestep(river_flow_model, domain.river.parameters.flow_length) + + Wflow.update_river_channel_flow!(river_flow_model, domain.river, dt) + + @test dt ≈ 2272.7523105527266 + @test river_flow_model.variables.zs_src ≈ [416.6029533901387, 418.03259945785163] + @test river_flow_model.variables.zs_dst ≈ [418.03259945785163, 416.00221556263983] + @test river_flow_model.variables.zs_max_at_edge ≈ [418.0325994578516, 418.0325994578516] + @test river_flow_model.variables.water_depth_at_edge ≈ + [2.4325933543360065, 2.4325933543360065] + @test river_flow_model.variables.flow_area_at_edge ≈ + [72.9778006300802, 72.9778006300802] + @test river_flow_model.variables.hydraulic_radius_at_edge ≈ + [2.093142401326319, 2.093142401326319] + @test river_flow_model.variables.q ≈ [12.587380945649473, 12.587380945649473] + @test river_flow_model.variables.q_cumulative ≈ [28607.999128032203, 28607.999128032203] + + Wflow.update_floodplain_flow!(river_flow_model, domain.river, dt) + + @test river_flow_model.floodplain.variables.water_depth_at_edge ≈ + [1.4325933543360065, 1.4325933543360065] + @test river_flow_model.floodplain.variables.hf_index == [1, 2] + @test river_flow_model.floodplain.variables.flow_area_at_edge ≈ + [62.01908306413688, 62.01908306413688] + @test river_flow_model.floodplain.variables.hydraulic_radius_at_edge ≈ + [1.3209041787011515, 1.3209041787011515] + @test river_flow_model.floodplain.variables.q ≈ + [0.050043034537710154, 3.279243909752683] + + @test river_flow_model.floodplain.variables.q_cumulative ≈ + [113.73542237265065, 7452.909172756367] + + Wflow.update_bc_reservoir_model!( + river_flow_model.boundary_conditions.reservoir, + river_flow_model, + domain, + dt, + ) + + Wflow.update_water_depth_and_storage!(river_flow_model, domain.river, dt) + + @test river_flow_model.variables.storage ≈ + [8949.81915717859, 76573.27391575256, 39343.45963085314] + @test river_flow_model.variables.h ≈ + [0.2471770951182529, 2.528578999550164, 1.5298322010636003] + + Wflow.update_water_depth_and_storage!( + river_flow_model.floodplain, + river_flow_model, + domain.river, + dt, + ) + + @test river_flow_model.variables.storage ≈ + [8949.81915717859, 71871.75158640924, 33570.77415623845] + @test river_flow_model.variables.h ≈ + [0.2471770951182529, 2.373326781381025, 1.3053669352090385] + @test river_flow_model.floodplain.variables.storage ≈ + [0.0, 59966.73673951485, 13225.594647371057] + @test river_flow_model.floodplain.variables.h ≈ + [0.0, 1.3733267813810248, 0.3053669352090384] +end + @testitem "unit: update_directional_flow!" begin # Test local inertial overland flow routing in x-direction at edge 2 (i = 2) using 3 # nodes. From 252f9f2231c8adf4a861e5fe39d684e4e30a6139 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 11 Jun 2026 10:16:14 +0200 Subject: [PATCH 66/90] Small change comment in unit test --- Wflow/test/routing_process.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index e60f7fa92..f49be23ec 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -728,7 +728,8 @@ end @testitem "unit: river flow on a staggered grid using Manning's equation with floodplain" begin # Test river flow on a staggered grid using Manning's equation with a floodplain on a # 3-node graph (1 → 2 → 3). - # Each sub-step of the update is called and verified individually: + # Each sub-step of the update of Manning's flow on staggered grid is called and + # verified individually: # 1. update_river_channel_flow! — edge discharge, water surface elevations # 2. update_floodplain_flow! — floodplain discharge and geometry # 3. update_bc_reservoir_model! — no reservoir From 026dde3ad3f49c0d78f46a55d795e7afb423041e Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 11 Jun 2026 14:08:17 +0200 Subject: [PATCH 67/90] Add unit test kinematic wave routing with floodplain --- Wflow/test/routing_process.jl | 133 +++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index f49be23ec..ccd9262a5 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -318,7 +318,6 @@ end # evaporation for the reservoir. using Graphs: DiGraph, add_edge! - dt = 86400.0 n = 2 bankfull_depth = [1.0, 1.0] flow_width = [94.73094177246094, 94.73094177246094] @@ -901,6 +900,138 @@ end [0.0, 1.3733267813810248, 0.3053669352090384] end +@testitem "unit: kinematic river flow including 1D floodplain schematization" begin + # Test river kinematic wave routing with floodplain on a 2-node graph (1 → 2). + # Each sub-step of the update of kinematic wave routing with floodplain is called and + # verified individually: + # 1. river_channel_floodplain_exchange! + # 2. kinwave_river_update! + # 3. update_floodplain_model! + using Graphs: DiGraph, add_edge! + + n = 2 + flow_length = [750.953125, 851.8125] + flow_width = [229.91920471191406, 229.91920471191406] + + river_flow_model = Wflow.RiverFlowModel(; + routing_method = Wflow.KinematicWave(), + timestepping = Wflow.TimeStepping(; stable_timesteps = zeros(n)), + boundary_conditions = Wflow.RiverFlowBC(; + n, + inwater = [0.02953203136486251, 0.0001849569866329713], + reservoir = nothing, + ), + parameters = Wflow.RiverFlowParameters(; + flow = Wflow.ManningFlowParameters(; + slope = [1.0e-5, 1.0e-5], + mannings_n = [0.03, 0.03], + alpha_pow = 0.4, + alpha_term = [3.8572052304976667, 3.8572052304976667], + alpha = [34.0789466790827, 34.0789466790827], + ), + bankfull_depth = [2.1051321029663086, 2.1051321029663086], + bankfull_storage = [363469.04651181493, 412286.02275520907], + ), + variables = Wflow.RiverFlowVariables(; + n, + q = [296.52948301601174, 192.8313119108856], + qlat = [3.9326064945614956e-5, 2.1713344971219756e-7], + h = [4.509741437854894, 3.4835130995322534], + storage = [778645.3962305915, 682239.2566234164], + ), + allocation = Wflow.NoAllocationRiverModel(n), + floodplain = Wflow.FloodPlainModel(; + routing_method = Wflow.KinematicWave(), + parameters = Wflow.FloodPlainParameters(; + profile = Wflow.FloodPlainProfile(; + depth = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5], + + storage = [ + 0.0 0.0; + 86329.2726379633 97924.02628183365; + 172659.2726379633 207762.02628183365; + 258989.2726379633 386716.02628183365; + 369518.2726379633 605009.0262818336; + 724648.2726379633 869843.0262818336; + ], + width = [ + 229.91920471191406 229.91920471191406; + 229.91920471191406 229.91920471191406; + 229.9211418821914 257.89243524836746; + 229.9211418821914 420.1722796977034; + 294.369904912507 512.5376770122533; + 945.8113647239966 621.8128989654414; + ], + flow_area = [ + 0.0 0.0; + 114.95960235595703 114.95960235595703; + 229.9201732970527 243.90581998014076; + 344.8807442381484 453.99195982899244; + 492.06569669440194 710.260798335119; + 964.9713790564002 1021.1672478178398; + ], + wetted_perimeter = [ + 0.0 0.0; + 1.0 1.0; + 2.00193717027733 29.9732305364534; + 3.00193717027733 193.25307498578934; + 68.45070020059296 286.61847230033925; + 720.8921600120825 396.8936942535273; + ], + ), + mannings_n = [0.072, 0.072], + slope = [1.0e-5, 1.0e-5], + ), + variables = Wflow.FloodPlainVariables(; + n, + q = [1.2861909826521447, 1.9846650910027395], + h = [0.0, 0.0], + storage = [0.0, 0.0], + ), + ), + ) + graph = DiGraph(2) + add_edge!(graph, 1, 2) + domain = Wflow.DomainRiver(; + network = Wflow.NetworkRiver(; + graph, + order = [1, 2], + order_of_subdomains = [[1]], + order_subdomain = [[1, 2]], + subdomain_indices = [[1, 2]], + upstream_nodes = [[], [1]], + reservoir_indices = [0, 0], + ), + parameters = Wflow.RiverParameters(; flow_width, flow_length), + ) + dt = Wflow.stable_timestep(river_flow_model, domain.parameters.flow_length, 0.05) + + Wflow.river_channel_floodplain_exchange!(river_flow_model, domain.parameters, dt) + + @test dt ≈ 1602.881460805217 + @test river_flow_model.variables.h ≈ [4.509741437854894, 3.4835130995322534] + @test river_flow_model.variables.storage ≈ [778645.3962305915, 682239.2566234164] + @test river_flow_model.floodplain.variables.h ≈ [2.0642836103410205, 1.1737631111525133] + @test river_flow_model.floodplain.variables.storage ≈ + [58760.1445203583, 40074.01437791623] + + Wflow.kinwave_river_update!(river_flow_model, domain, dt) + + @test river_flow_model.variables.h ≈ [2.872002930358695, 3.1138609375883397] + @test river_flow_model.variables.storage ≈ [495875.84798393055, 609843.6005807515] + @test river_flow_model.variables.q ≈ [139.7837242183345, 159.9486203252721] + @test river_flow_model.variables.q_cumulative ≈ [224056.7400718776, 256378.67820075116] + + Wflow.update_floodplain_model!(river_flow_model, domain, dt) + + @test river_flow_model.floodplain.variables.h ≈ [0.3054891336736904, 0.2126646996773224] + @test river_flow_model.floodplain.variables.storage ≈ + [52745.30941770246, 41649.967280840756] + @test river_flow_model.floodplain.variables.q ≈ [3.752513987924126, 2.7693140810933157] + @test river_flow_model.floodplain.variables.q_cumulative ≈ + [6014.835102655834, 4438.882199731312] +end + @testitem "unit: update_directional_flow!" begin # Test local inertial overland flow routing in x-direction at edge 2 (i = 2) using 3 # nodes. From d23346f7fbd75058ca8767f3154dbdcab98cb97b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 11 Jun 2026 15:09:31 +0200 Subject: [PATCH 68/90] Update model settings docs --- docs/model_docs/model_settings.qmd | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/model_docs/model_settings.qmd b/docs/model_docs/model_settings.qmd index 54ad547c4..bc5c2fb94 100644 --- a/docs/model_docs/model_settings.qmd +++ b/docs/model_docs/model_settings.qmd @@ -55,6 +55,7 @@ The Tables in this subsection show the available models settings for the model t | `subsurface_kinematic_wave__alpha_coefficient` | Stability coefficient for internal model time step subsurface flow routing (CFL condition) | - | 1.0 | | `drain__flag` | Include drainage boundary condition | - | `false` | | `river_subsurface_exchange_head_based__flag` | Include head-dependent river flux boundary | - | `false` | +| `floodplain_1d__flag` | Include 1D floodplain schematization for kinematic wave river routing | - | `false` | : {.striped .hover} ### Local inertial routing @@ -63,13 +64,21 @@ The Tables in this subsection show the available models settings for the model t | `river_local_inertial_flow__alpha_coefficient` | Stability coefficient for internal model time step river flow routing (CFL condition) | - | 0.7 | | `land_local_inertial_flow__alpha_coefficient` | Stability coefficient for internal model time step overland flow routing (CFL condition) | - | 0.7 | | `land_local_inertial_flow__theta_coefficient` | Weighting factor that adjusts the amount of artificial numerical diffusion for local inertial overland flow | - | 1.0 | -| `river_water_flow_threshold__depth` | Water depth threshold for calculating river flow between cells | m | 0.001 | -| `land_surface_water_flow_threshold__depth` | Water depth threshold for calculating overland flow between cells | m | 0.001 | +| `river_water_flow_threshold__depth` | Water depth threshold for calculating river flow between nodes | m | 0.001 | +| `land_surface_water_flow_threshold__depth` | Water depth threshold for calculating overland flow between nodes | m | 0.001 | | `river_water_flow__froude_limit_flag` | Limit river flow to subcritical-critical according to Froude number | - | `true` | | `land_surface_water_flow__froude_limit_flag` | Limit overland flow to subcritical-critical according to Froude number | - | `true` | | `floodplain_1d__flag` | Include 1D floodplain schematization for local inertial river routing | - | `false` | : {.striped .hover} +### River routing using Manning's equation on a staggered grid +| Setting | Description | Unit | Default | +| --- | ----- | - | - | +| `river_staggered_manning_flow__alpha_coefficient` | Stability coefficient for internal model time step river flow routing (CFL condition) | - | 0.7 | +| `river_water_flow_threshold__depth` | Water depth threshold for calculating river flow between nodes | m | 0.001 | +| `floodplain_1d__flag` | Include 1D floodplain schematization | - | `false` | +: {.striped .hover} + ### Groundwater flow | Setting | Description | Unit | Default | | --- | ----- | - | - | From ee06568db3d8a8254ab606fc0f9857715229f617 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Thu, 11 Jun 2026 18:38:16 +0200 Subject: [PATCH 69/90] Rename inds `EdgeConnectivity` Keeping `x` and `y` dir in the name, bit more generic than `left`/`right`. --- Wflow/src/bmi.jl | 16 +++---- Wflow/src/mass_balance.jl | 6 +-- Wflow/src/network.jl | 24 +++++----- .../surface/surface_staggered_scheme.jl | 45 ++++++++++--------- Wflow/src/utils.jl | 36 +++++++-------- Wflow/test/routing_process.jl | 8 ++-- 6 files changed, 69 insertions(+), 66 deletions(-) diff --git a/Wflow/src/bmi.jl b/Wflow/src/bmi.jl index 6acab70e9..b8f723730 100644 --- a/Wflow/src/bmi.jl +++ b/Wflow/src/bmi.jl @@ -312,9 +312,9 @@ function BMI.get_grid_edge_count(model::Model, grid::Int) if grid == 3 return ne(domain.river.network.graph) elseif grid == 4 - return length(domain.land.network.edge_indices.idx_right) + return length(domain.land.network.edge_indices.ind_x_up) elseif grid == 5 - return length(domain.land.network.edge_indices.idx_up) + return length(domain.land.network.edge_indices.ind_y_up) elseif grid in 0:2 || grid == 6 @warn("edges are not provided for grid type $grid (variables are located at nodes)") else @@ -334,16 +334,16 @@ function BMI.get_grid_edge_nodes(model::Model, grid::Int, edge_nodes::Vector{Int edge_nodes[range(2, n; step = 2)] = nodes_at_edge.dst return edge_nodes elseif grid == 4 - idx_right = domain.land.network.edge_indices.idx_right + ind_x_up = domain.land.network.edge_indices.ind_x_up edge_nodes[range(1, n; step = 2)] = 1:m - idx_right[idx_right .== m + 1] .= -999 - edge_nodes[range(2, n; step = 2)] = idx_right + ind_x_up[ind_x_up .== m + 1] .= -999 + edge_nodes[range(2, n; step = 2)] = ind_x_up return edge_nodes elseif grid == 5 - idx_up = domain.land.network.edge_indices.idx_up + ind_y_up = domain.land.network.edge_indices.ind_y_up edge_nodes[range(1, n; step = 2)] = 1:m - idx_up[idx_up .== m + 1] .= -999 - edge_nodes[range(2, n; step = 2)] = idx_up + ind_y_up[ind_y_up .== m + 1] .= -999 + edge_nodes[range(2, n; step = 2)] = ind_y_up return edge_nodes elseif grid in 0:2 || grid == 6 @warn("edges are not provided for grid type $grid (variables are located at nodes)") diff --git a/Wflow/src/mass_balance.jl b/Wflow/src/mass_balance.jl index acfe6072e..0e1ce064f 100644 --- a/Wflow/src/mass_balance.jl +++ b/Wflow/src/mass_balance.jl @@ -426,14 +426,14 @@ function compute_flow_balance!( qy_av_average = qy_average for i in 1:(overland_flow_model.parameters.n) - idx_down = indices.idx_down[i] - idx_left = indices.idx_left[i] + ind_y_down = indices.ind_y_down[i] + ind_x_down = indices.ind_x_down[i] total_in = 0.0 total_out = 0.0 total_in, total_out = add_inflow( total_in, total_out, - [qx_av_average[idx_left], qy_av_average[idx_down], runoff[i]], + [qx_av_average[ind_x_down], qy_av_average[ind_y_down], runoff[i]], ) total_in, total_out = add_outflow(total_in, total_out, [qx_av_average[i], qy_av_average[i]]) diff --git a/Wflow/src/network.jl b/Wflow/src/network.jl index 776568310..be8260b03 100644 --- a/Wflow/src/network.jl +++ b/Wflow/src/network.jl @@ -1,6 +1,6 @@ # maps the fields of struct `EdgeConnectivity` to the defined Wflow cartesian indices of # const `neighbors`. -const DIRS = (:idx_down, :idx_left, :idx_right, :idx_up) +const DIRS = (:ind_y_down, :ind_x_down, :ind_x_up, :ind_y_up) """ Struct for storing 2D staggered grid edge connectivity in `x` and `y` directions. For @@ -15,19 +15,21 @@ Edges without neighbors are handled by an extra index (at `n + 1`, with `n` edge linear index `i` of the `EdgeConnectivity` fields represents the edge between node index `i` and the neighboring nodes in the CartesianIndex(-1,0) and CartesianIndex(0,-1) directions. The edges are defined as follows: -- `idx_right` is the edge between node `i` and node `idx_right` in the `CartesianIndex(1,0)` direction. -- `idx_left` is the edge between node `idx_left` in the `CartesianIndex(-1,0)` direction and the - neighboring node (CartesianIndex(-2,0) direction). -- `idx_up` is the edge between node `i` and node `idx_up` in the `CartesianIndex(0,1)` direction. -- `idx_down` is the edge between node `idx_down` in the `CartesianIndex(0,-1)` direction and the - neighboring node (`CartesianIndex(0,-2)` direction). +- `ind_x_up` is the edge between node `i` and node `ind_x_up` in the `CartesianIndex(1,0)` + direction. +- `ind_x_down` is the edge between node `ind_x_down` in the `CartesianIndex(-1,0)` direction + and the neighboring node (CartesianIndex(-2,0) direction). +- `ind_y_up` is the edge between node `i` and node `ind_y_up` in the `CartesianIndex(0,1)` + direction. +- `ind_y_down` is the edge between node `ind_y_down` in the `CartesianIndex(0,-1)` direction + and the neighboring node (`CartesianIndex(0,-2)` direction). """ @with_kw struct EdgeConnectivity n::Int - idx_right::Vector{Int} = zeros(Int, n) - idx_left::Vector{Int} = zeros(Int, n) - idx_up::Vector{Int} = zeros(Int, n) - idx_down::Vector{Int} = zeros(Int, n) + ind_x_up::Vector{Int} = zeros(Int, n) + ind_x_down::Vector{Int} = zeros(Int, n) + ind_y_up::Vector{Int} = zeros(Int, n) + ind_y_down::Vector{Int} = zeros(Int, n) end "Struct for storing source `src` node and destination `dst` node of an edge." diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 7aacfd298..910a9c5e5 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -931,13 +931,13 @@ function LocalInertialOverlandFlowParameters( zx_max_at_edge = zeros(n) zy_max_at_edge = zeros(n) for i in 1:n - idx_right = edge_indices.idx_right[i] - if idx_right <= n - zx_max_at_edge[i] = max(elevation[i], elevation[idx_right]) + ind_x_up = edge_indices.ind_x_up[i] + if ind_x_up <= n + zx_max_at_edge[i] = max(elevation[i], elevation[ind_x_up]) end - idx_up = edge_indices.idx_up[i] - if idx_up <= n - zy_max_at_edge[i] = max(elevation[i], elevation[idx_up]) + ind_y_up = edge_indices.ind_y_up[i] + if ind_y_up <= n + zy_max_at_edge[i] = max(elevation[i], elevation[ind_y_up]) end end @@ -1197,7 +1197,8 @@ end """ Update flow for the local inertial overland flow model at edge `i` in a single direction. -`is_x_direction`: true for x-direction (idx_right/idx_left), false for y-direction (idx_up/idx_down) +`is_x_direction`: true for x-direction (ind_x_up/ind_x_down), false for y-direction +(ind_y_up/ind_y_down) """ @inline function update_directional_flow!( overland_flow_model::OverlandFlowModel{<:LocalInertial}, @@ -1213,8 +1214,8 @@ Update flow for the local inertial overland flow model at edge `i` in a single d # Select direction-specific parameters based on the boolean flag if is_x_direction - upstream_idx = indices.idx_right[i] - downstream_idx = indices.idx_left[i] + upstream_idx = indices.ind_x_up[i] + downstream_idx = indices.ind_x_down[i] width_at_edge = land_p.ywidth_at_edge[i] z_max_at_edge = land_p.zx_max_at_edge[i] length_vec = x_length @@ -1222,8 +1223,8 @@ Update flow for the local inertial overland flow model at edge `i` in a single d q_prev = land_v.qx0 q_cumulative = land_v.qx_cumulative else - upstream_idx = indices.idx_up[i] - downstream_idx = indices.idx_down[i] + upstream_idx = indices.ind_y_up[i] + downstream_idx = indices.ind_y_down[i] width_at_edge = land_p.xwidth_at_edge[i] z_max_at_edge = land_p.zy_max_at_edge[i] length_vec = y_length @@ -1310,11 +1311,11 @@ function update_inflow_reservoir!( land_v = overland_flow_model.variables for (i, j) in enumerate(reservoir_indices) - idx_down = indices.idx_down[j] - idx_left = indices.idx_left[j] + ind_y_down = indices.ind_y_down[j] + ind_x_down = indices.ind_x_down[j] reservoir_model.boundary_conditions.inflow_overland[i] = land_bc.runoff[j] + - (land_v.qx[idx_left] - land_v.qx[j] + land_v.qy[idx_down] - land_v.qy[j]) + (land_v.qx[ind_x_down] - land_v.qx[j] + land_v.qy[ind_y_down] - land_v.qy[j]) end return nothing end @@ -1335,15 +1336,15 @@ fluxes of the local inertial river and overland flow model. edges_at_node = domain.river.network.edges_at_node river_idx = inds_river[i] - idx_down = indices.idx_down[i] - idx_left = indices.idx_left[i] + ind_y_down = indices.ind_y_down[i] + ind_x_down = indices.ind_x_down[i] net_river_flow = sum_at(river_flow_model.variables.q, edges_at_node.src[river_idx]) - sum_at(river_flow_model.variables.q, edges_at_node.dst[river_idx]) net_land_flow = - overland_flow_model.variables.qx[idx_left] - overland_flow_model.variables.qx[i] + - overland_flow_model.variables.qy[idx_down] - overland_flow_model.variables.qy[i] + overland_flow_model.variables.qx[ind_x_down] - overland_flow_model.variables.qx[i] + + overland_flow_model.variables.qy[ind_y_down] - overland_flow_model.variables.qy[i] net_flow = net_river_flow + net_land_flow + overland_flow_model.boundary_conditions.runoff[i] - river_flow_model.boundary_conditions.abstraction[river_idx] @@ -1427,12 +1428,12 @@ from horizontal fluxes and runoff. dt::Float64, ) indices = network.edge_indices - idx_down = indices.idx_down[i] - idx_left = indices.idx_left[i] + ind_y_down = indices.ind_y_down[i] + ind_x_down = indices.ind_x_down[i] return ( - overland_flow_model.variables.qx[idx_left] - overland_flow_model.variables.qx[i] + - overland_flow_model.variables.qy[idx_down] - overland_flow_model.variables.qy[i] + + overland_flow_model.variables.qx[ind_x_down] - overland_flow_model.variables.qx[i] + + overland_flow_model.variables.qy[ind_y_down] - overland_flow_model.variables.qy[i] + overland_flow_model.boundary_conditions.runoff[i] ) * dt end diff --git a/Wflow/src/utils.jl b/Wflow/src/utils.jl index 1a5ef2dfb..7b1175bd0 100644 --- a/Wflow/src/utils.jl +++ b/Wflow/src/utils.jl @@ -624,44 +624,44 @@ function set_effective_flowwidth!( we_x[idx] = reservoir_outlet[v] ? 0.0 : max(we_x[idx] - 0.5 * w, 0.0) we_y[idx] = reservoir_outlet[v] ? 0.0 : max(we_y[idx] - 0.5 * w, 0.0) elseif dir == CartesianIndex(-1, -1) - if edge_indices.idx_left[idx] <= n - we_y[edge_indices.idx_left[idx]] = + if edge_indices.ind_x_down[idx] <= n + we_y[edge_indices.ind_x_down[idx]] = reservoir_outlet[v] ? 0.0 : - max(we_y[edge_indices.idx_left[idx]] - 0.5 * w, 0.0) + max(we_y[edge_indices.ind_x_down[idx]] - 0.5 * w, 0.0) end - if edge_indices.idx_down[idx] <= n - we_x[edge_indices.idx_down[idx]] = + if edge_indices.ind_y_down[idx] <= n + we_x[edge_indices.ind_y_down[idx]] = reservoir_outlet[v] ? 0.0 : - max(we_x[edge_indices.idx_down[idx]] - 0.5 * w, 0.0) + max(we_x[edge_indices.ind_y_down[idx]] - 0.5 * w, 0.0) end elseif dir == CartesianIndex(1, 0) we_y[idx] = reservoir_outlet[v] ? 0.0 : max(we_y[idx] - w, 0.0) elseif dir == CartesianIndex(0, 1) we_x[idx] = reservoir_outlet[v] ? 0.0 : max(we_x[idx] - w, 0.0) elseif dir == CartesianIndex(-1, 0) - if edge_indices.idx_left[idx] <= n - we_y[edge_indices.idx_left[idx]] = + if edge_indices.ind_x_down[idx] <= n + we_y[edge_indices.ind_x_down[idx]] = reservoir_outlet[v] ? 0.0 : - max(we_y[edge_indices.idx_left[idx]] - w, 0.0) + max(we_y[edge_indices.ind_x_down[idx]] - w, 0.0) end elseif dir == CartesianIndex(0, -1) - if edge_indices.idx_down[idx] <= n - we_x[edge_indices.idx_down[idx]] = + if edge_indices.ind_y_down[idx] <= n + we_x[edge_indices.ind_y_down[idx]] = reservoir_outlet[v] ? 0.0 : - max(we_x[edge_indices.idx_down[idx]] - w, 0.0) + max(we_x[edge_indices.ind_y_down[idx]] - w, 0.0) end elseif dir == CartesianIndex(1, -1) we_y[idx] = max(we_y[idx] - 0.5 * w, 0.0) - if edge_indices.idx_down[idx] <= n - we_x[edge_indices.idx_down[idx]] = + if edge_indices.ind_y_down[idx] <= n + we_x[edge_indices.ind_y_down[idx]] = reservoir_outlet[v] ? 0.0 : - max(we_x[edge_indices.idx_down[idx]] - 0.5 * w, 0.0) + max(we_x[edge_indices.ind_y_down[idx]] - 0.5 * w, 0.0) end elseif dir == CartesianIndex(-1, 1) - if edge_indices.idx_left[idx] <= n - we_y[edge_indices.idx_left[idx]] = + if edge_indices.ind_x_down[idx] <= n + we_y[edge_indices.ind_x_down[idx]] = reservoir_outlet[v] ? 0.0 : - max(we_y[edge_indices.idx_left[idx]] - 0.5 * w, 0.0) + max(we_y[edge_indices.ind_x_down[idx]] - 0.5 * w, 0.0) end we_x[idx] = reservoir_outlet[v] ? 0.0 : max(we_x[idx] - 0.5 * w, 0.0) end diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index ccd9262a5..05294549b 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -1072,8 +1072,8 @@ end network = Wflow.NetworkLand(; edge_indices = Wflow.EdgeConnectivity(; n = 3, - idx_right = [2, 3, 4], - idx_left = [4, 1, 2], + ind_x_up = [2, 3, 4], + ind_x_down = [4, 1, 2], ), ), parameters = Wflow.LandParameters(; @@ -1164,8 +1164,8 @@ end river_indices = [0, 0, 1], edge_indices = Wflow.EdgeConnectivity(; n = 3, - idx_left = [4, 1, 2], - idx_down = [4, 4, 4], + ind_x_down = [4, 1, 2], + ind_y_down = [4, 4, 4], ), ), parameters = Wflow.LandParameters(; From 9174b86a998fd9f9a29780cad9266fc34420d643 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 12 Jun 2026 08:29:08 +0200 Subject: [PATCH 70/90] Update struct model docs --- docs/developments/model_struct.qmd | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/developments/model_struct.qmd b/docs/developments/model_struct.qmd index 5653377cd..6a277c60b 100644 --- a/docs/developments/model_struct.qmd +++ b/docs/developments/model_struct.qmd @@ -7,12 +7,23 @@ such as the domain and shared parameters, routing concepts, land model, clock, m configuration and input and output. ```julia -struct Model{R <: Routing, L <: AbstractLandModel, T <: AbstractModelType} <: - AbstractModel{T} +""" + Model{R <: Routing, L <: AbstractLandModel, M <: AbstractMassBalance, T <: AbstractModelType} <: AbstractModel{T} + +Composite type that represents all different aspects of a Wflow Model, such as the network, +parameters, clock, configuration and input and output. +""" +struct Model{ + R <: Routing, + L <: AbstractLandModel, + M <: AbstractMassBalance, + T <: AbstractModelType, +} <: AbstractModel{T} config::Config # all configuration options domain::Domain # domain connectivity (network) and shared parameters routing::R # routing model (horizontal fluxes), moves along network land::L # land model simulating vertical fluxes, independent of each other + mass_balance::M # mass balance error clock::Clock # to keep track of simulation time reader::NCReader # provides the model with dynamic input writer::Writer # writes model output @@ -28,4 +39,5 @@ flow domains. For example, the wflow\_sbm model with default configuration conta kinematic wave routing concept for river, overland and lateral subsurface flow. The `land` field of the `struct Model` represents a land model, simulating vertical fluxes, for example soil erosion as part of the `sediment` model type and snow melt as part of the `sbm` model -type. +type. Finally, the `mass_balance` field contains water mass balance computations (error and +relative error). From 405a2f0e9dc18fd448450618be3e4f8ce4ab5ce3 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 12 Jun 2026 09:04:38 +0200 Subject: [PATCH 71/90] Update changelog --- docs/changelog.qmd | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/changelog.qmd b/docs/changelog.qmd index 47a3b3aa2..760a30f0e 100644 --- a/docs/changelog.qmd +++ b/docs/changelog.qmd @@ -72,6 +72,16 @@ subsurface_water__instantaneous_volume_flow_rate = "ssf" domain via `input.subbasin_location__count`, or specify specific subcatchments by linking `input.subbasin_location__count` to a subcatchment map, and indicate which subbasins should be active by providing their IDs in `input.subbasin_active_location__count`. +- An optional 1D floodplain schematization to kinematic wave routing of river flow. This is + the same 1D floodplain schematization used by the local inertial river flow model. River + channel-floodplain water exchange is assumed to be instantaneous. Routing is done + separately for the river channel and floodplain.Floodplain water is routed down slope + using the `accucapacityflux` function with a transport capacity based on Manning's + equation. +- Support the use of Manning's equation (assuming that the topography controls water flow + mostly) for river and floodplain routing on a staggered grid. This is an additional + routing scheme to the local inertial river routing scheme, both make us of the staggered + grid method. ## v1.0.2 - 2026-03-05 From 5b801c0e18807d78ab45aca59f5914cc08046803 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 12 Jun 2026 10:21:27 +0200 Subject: [PATCH 72/90] Update docs model parameters staggered grid --- .../standard_name/standard_name_routing.jl | 24 +++++++++---------- docs/model_docs/parameters_routing.qmd | 20 +++++++++------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Wflow/src/standard_name/standard_name_routing.jl b/Wflow/src/standard_name/standard_name_routing.jl index 021923d55..0a80db21b 100644 --- a/Wflow/src/standard_name/standard_name_routing.jl +++ b/Wflow/src/standard_name/standard_name_routing.jl @@ -381,26 +381,26 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( description = "Part of subsurface flow that flows to the river", tags = [:kinematic_lateral_subsurface_output], ), - ## Local inertial + ## Staggered grid (local inertial and Manning's equation) ### River flow #### Input "model_boundary_condition_river__length" => ParameterMetadata(; unit = Unit(; m = 1), default = 1.0e4, description = "Boundary condition river length downstream river outlets", - tags = [:local_inertial_river_input], + tags = [:staggered_grid_river_input], ), "model_boundary_condition_river_bank_water__depth" => ParameterMetadata(; default = 0, unit = Unit(; m = 1), description = "Boundary condition bankfull depth downstream river outlets", - tags = [:local_inertial_river_input], + tags = [:staggered_grid_river_input], ), "river_bank_water__elevation" => ParameterMetadata(; unit = Unit(; m = 1), fill = 0.0, description = "Bankfull elevation of the river", - tags = [:local_inertial_river_input], + tags = [:staggered_grid_river_input], ), ### 1D floodplain flow #### Input @@ -409,7 +409,7 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( dimname = :flood_depth, description = "Floodplain profile (cumulative volume per flood depth)", tags = [ - :local_inertial_floodplain_1D_flow_input, + :staggered_grid_floodplain_1D_flow_input, :kinematic_wave_floodplain_1D_flow_input, ], ), @@ -419,7 +419,7 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( default = 0.072, description = "Manning's roughness", tags = [ - :local_inertial_floodplain_1D_flow_input, + :staggered_grid_floodplain_1D_flow_input, :kinematic_wave_floodplain_1D_flow_input, ], ), @@ -435,8 +435,8 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( unit = Unit(; m = 3, s = -1), description = "Floodplain discharge", tags = [ - :local_inertial_floodplain_1D_flow_state, - :local_inertial_floodplain_1D_flow_output, + :staggered_grid_floodplain_1D_flow_state, + :staggered_grid_floodplain_1D_flow_output, :kinematic_wave_floodplain_1D_flow_output, ], ), @@ -445,8 +445,8 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( unit = Unit(; m = 1), description = "Floodplain water depth", tags = [ - :local_inertial_floodplain_1D_flow_state, - :local_inertial_floodplain_1D_flow_output, + :staggered_grid_floodplain_1D_flow_state, + :staggered_grid_floodplain_1D_flow_output, :kinematic_wave_floodplain_1D_flow_state, :kinematic_wave_floodplain_1D_flow_output, ], @@ -457,7 +457,7 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( unit = Unit(; m = 3), description = "Floodplain water volume", tags = [ - :local_inertial_floodplain_1D_flow_output, + :staggered_grid_floodplain_1D_flow_output, :kinematic_wave_floodplain_1D_flow_output, ], ), @@ -466,7 +466,7 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( unit = Unit(; m = 3, s = -1), description = "Floodplain discharge", tags = [ - :local_inertial_floodplain_1D_flow_output, + :staggered_grid_floodplain_1D_flow_output, :kinematic_wave_floodplain_1D_flow_output, ], ), diff --git a/docs/model_docs/parameters_routing.qmd b/docs/model_docs/parameters_routing.qmd index 99ad9bce8..5335bde2f 100644 --- a/docs/model_docs/parameters_routing.qmd +++ b/docs/model_docs/parameters_routing.qmd @@ -127,52 +127,54 @@ generate_table(Wflow.Routing, :kinematic_lateral_subsurface_state; relative_widt generate_table(Wflow.Routing, :kinematic_lateral_subsurface_output; relative_widths = [3, 3, 1, 1, 2]) ``` -## Local inertial +## Staggered grid +River and floodplain flow routing on a staggered grid makes use of the local inertial or +Manning's equation. ### River flow #### Input ```{julia} # | code-fold: true -generate_table(Wflow.Routing, :local_inertial_river_input; relative_widths = [6, 4, 2, 2, 2, 2]) +generate_table(Wflow.Routing, :staggered_grid_river_input; relative_widths = [6, 4, 2, 2, 2, 2]) ``` #### Static or cyclic/forcing input ```{julia} # | code-fold: true -generate_table(Wflow.Routing, :local_inertial_river_static_cyclic_forcing_input; relative_widths = [6, 4, 2, 2, 2, 2]) +generate_table(Wflow.Routing, :staggered_grid_river_static_cyclic_forcing_input; relative_widths = [6, 4, 2, 2, 2, 2]) ``` #### States ```{julia} # | code-fold: true -generate_table(Wflow.Routing, :local_inertial_river_state; relative_widths = [3, 3, 1, 1, 2]) +generate_table(Wflow.Routing, :staggered_grid_river_state; relative_widths = [3, 3, 1, 1, 2]) ``` #### Output ```{julia} # | code-fold: true -generate_table(Wflow.Routing, :local_inertial_river_output; relative_widths = [3, 3, 1, 1, 2]) +generate_table(Wflow.Routing, :staggered_grid_river_output; relative_widths = [3, 3, 1, 1, 2]) ``` ### 1D floodplain flow #### Input ```{julia} # | code-fold: true -generate_table(Wflow.Routing, :local_inertial_floodplain_1D_flow_input; relative_widths = [6, 4, 2, 2, 2, 2]) +generate_table(Wflow.Routing, :staggered_grid_floodplain_1D_flow_input; relative_widths = [6, 4, 2, 2, 2, 2]) ``` #### States ```{julia} # | code-fold: true -generate_table(Wflow.Routing, :local_inertial_floodplain_1D_flow_state; relative_widths = [3, 3, 1, 1, 2]) +generate_table(Wflow.Routing, :staggered_grid_floodplain_1D_flow_state; relative_widths = [3, 3, 1, 1, 2]) ``` #### Output ```{julia} # | code-fold: true -generate_table(Wflow.Routing, :local_inertial_floodplain_1D_flow_output; relative_widths = [3, 3, 1, 1, 2]) +generate_table(Wflow.Routing, :staggered_grid_floodplain_1D_flow_output; relative_widths = [3, 3, 1, 1, 2]) ``` -### Overland flow +### Local inertial overland flow (2D) #### Input ```{julia} # | code-fold: true From 8b46eaf0332caf3081451f13cb174d7313b40534 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 12 Jun 2026 11:19:18 +0200 Subject: [PATCH 73/90] Fix some doc tags model parameters --- Wflow/src/standard_name/standard_name_routing.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Wflow/src/standard_name/standard_name_routing.jl b/Wflow/src/standard_name/standard_name_routing.jl index 0a80db21b..4cabf2303 100644 --- a/Wflow/src/standard_name/standard_name_routing.jl +++ b/Wflow/src/standard_name/standard_name_routing.jl @@ -233,8 +233,8 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( allow_dynamic_input = true, tags = [ :kinematic_wave_river_static_cyclic_forcing_input, - :local_inertial_river_static_cyclic_forcing_input, - :local_inertial_river_input, + :staggered_grid_river_static_cyclic_forcing_input, + :staggered_grid_river_input, ], ), "river__slope" => ParameterMetadata(; @@ -251,8 +251,8 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( tags = [ :kinematic_wave_river_state, :kinematic_wave_river_output, - :local_inertial_river_state, - :local_inertial_river_output, + :staggered_grid_river_state, + :staggered_grid_river_output, ], ), "river_water__depth" => ParameterMetadata(; From 751925a22ad51a481b33c9ff97260f32523fc108 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 12 Jun 2026 13:18:44 +0200 Subject: [PATCH 74/90] Formatting and typo docs routing parameters --- docs/model_docs/parameters_routing.qmd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/model_docs/parameters_routing.qmd b/docs/model_docs/parameters_routing.qmd index 5335bde2f..d86a2b9a2 100644 --- a/docs/model_docs/parameters_routing.qmd +++ b/docs/model_docs/parameters_routing.qmd @@ -128,8 +128,9 @@ generate_table(Wflow.Routing, :kinematic_lateral_subsurface_output; relative_wid ``` ## Staggered grid -River and floodplain flow routing on a staggered grid makes use of the local inertial or +River and floodplain flow routing on a staggered grid make use of the local inertial or Manning's equation. + ### River flow #### Input ```{julia} From 29aea9f9de21dbc0d2d4d88b3faf4bf522c41563 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 12 Jun 2026 13:50:05 +0200 Subject: [PATCH 75/90] Fix missing space in changelog --- docs/changelog.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.qmd b/docs/changelog.qmd index 760a30f0e..8ca09a01c 100644 --- a/docs/changelog.qmd +++ b/docs/changelog.qmd @@ -75,7 +75,7 @@ subsurface_water__instantaneous_volume_flow_rate = "ssf" - An optional 1D floodplain schematization to kinematic wave routing of river flow. This is the same 1D floodplain schematization used by the local inertial river flow model. River channel-floodplain water exchange is assumed to be instantaneous. Routing is done - separately for the river channel and floodplain.Floodplain water is routed down slope + separately for the river channel and floodplain. Floodplain water is routed down slope using the `accucapacityflux` function with a transport capacity based on Manning's equation. - Support the use of Manning's equation (assuming that the topography controls water flow From e454e145fca4ec3891ace314f27045f56f754b1b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 12 Jun 2026 16:12:51 +0200 Subject: [PATCH 76/90] Rename routing method kinematic wave floodplain From `KinematicWave` to `Manning`, a better description as it uses the Manning equation and not the full kinematic wave equation. --- Wflow/src/routing/routing.jl | 2 +- Wflow/src/routing/surface/floodplain.jl | 3 +-- Wflow/src/routing/surface/surface_kinwave.jl | 4 ++-- Wflow/test/routing_process.jl | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Wflow/src/routing/routing.jl b/Wflow/src/routing/routing.jl index 2cdaeda59..1ae8c710c 100644 --- a/Wflow/src/routing/routing.jl +++ b/Wflow/src/routing/routing.jl @@ -5,7 +5,7 @@ abstract type AbstractSubsurfaceFlowModel end abstract type AbstractRoutingMethod end abstract type AbstractStaggeredRoutingMethod <: AbstractRoutingMethod end -struct AccucapacityFlux <: AbstractRoutingMethod end +struct Manning <: AbstractRoutingMethod end struct KinematicWave <: AbstractRoutingMethod end struct ManningStaggered <: AbstractStaggeredRoutingMethod end struct LocalInertial <: AbstractStaggeredRoutingMethod end diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index d2a547d13..8ae87b9f3 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -419,7 +419,6 @@ function FloodPlainModel(dataset::NCDataset, config::Config, domain::DomainRiver clamp!(slope, 0.00001, Inf) parameters = FloodPlainParameters(; profile, mannings_n, slope) variables = FloodPlainVariables(; n) - floodplain_model = - FloodPlainModel(; routing_method = KinematicWave(), parameters, variables) + floodplain_model = FloodPlainModel(; routing_method = Manning(), parameters, variables) return floodplain_model end diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index 6bc935ee9..fa5df7d2a 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -388,7 +388,7 @@ function update_floodplain_model!( river_flow_model::RiverFlowModel{T, F}, domain::DomainRiver, dt::Float64, -) where {T <: KinematicWave, F <: FloodPlainModel{<:KinematicWave}} +) where {T <: KinematicWave, F <: FloodPlainModel{<:Manning}} (; floodplain) = river_flow_model (; flow_length) = domain.parameters (; profile, mannings_n, slope) = floodplain.parameters @@ -560,7 +560,7 @@ function river_channel_floodplain_exchange!( river_flow_model::RiverFlowModel{T, F}, parameters::RiverParameters, dt::Float64, -) where {T <: KinematicWave, F <: FloodPlainModel{<:KinematicWave}} +) where {T <: KinematicWave, F <: FloodPlainModel{<:Manning}} river_v = river_flow_model.variables river_p = river_flow_model.parameters floodplain_v = river_flow_model.floodplain.variables diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 05294549b..cb6027018 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -941,7 +941,7 @@ end ), allocation = Wflow.NoAllocationRiverModel(n), floodplain = Wflow.FloodPlainModel(; - routing_method = Wflow.KinematicWave(), + routing_method = Wflow.Manning(), parameters = Wflow.FloodPlainParameters(; profile = Wflow.FloodPlainProfile(; depth = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5], From 9396140293d027565ef3c5aacd525134304fecde Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 16 Jun 2026 17:06:10 +0200 Subject: [PATCH 77/90] Fix `NaN`/`Inf` values floodplain hydraulic radius Removed flow area and hydraulic radius fields of local inertial structs as these are used only internally (per function) and are not part of model output. Added check for hydraulic radius floodplain. --- Wflow/src/routing/surface/floodplain.jl | 4 - .../surface/surface_staggered_scheme.jl | 94 ++++++++++--------- Wflow/test/routing_process.jl | 18 ---- 3 files changed, 50 insertions(+), 66 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 8ae87b9f3..7d3a9bc5c 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -218,10 +218,6 @@ end @with_kw struct FloodPlainStaggeredVariables <: AbstractFloodPlainVariables n::Int n_edges::Int - # flow area at edge [m²] - flow_area_at_edge::Vector{Float64} = zeros(n_edges) - # hydraulic radius at edge [m] - hydraulic_radius_at_edge::Vector{Float64} = zeros(n_edges) # water depth at edge [m] water_depth_at_edge::Vector{Float64} = zeros(n_edges) # discharge at edge at previous time step diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 910a9c5e5..5123a77f1 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -179,10 +179,6 @@ end zs_dst::Vector{Float64} = zeros(n_edges) # water depth at edge [m] water_depth_at_edge::Vector{Float64} = zeros(n_edges) - # flow area at edge [m²] - flow_area_at_edge::Vector{Float64} = zeros(n_edges) - # wetted perimeter at edge [m] - hydraulic_radius_at_edge::Vector{Float64} = zeros(n_edges) # river storage [m³] storage::Vector{Float64} = zeros(n_cells) # error storage [m³] @@ -345,13 +341,13 @@ function update_river_channel_flow!( river_v.water_depth_at_edge[i] = (river_v.zs_max_at_edge[i] - river_p.zb_max_at_edge[i]) - river_v.flow_area_at_edge[i] = - river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] # flow area (rectangular channel) - river_v.hydraulic_radius_at_edge[i] = - river_v.flow_area_at_edge[i] / wetted_perimeter_channel( + # rectangular channel + flow_area_at_edge = river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] + hydraulic_radius_at_edge = + flow_area_at_edge / wetted_perimeter_channel( river_v.water_depth_at_edge[i], river_p.flow_width_at_edge[i], - ) # hydraulic radius (rectangular channel) + ) river_v.q[i] = ifelse( river_v.water_depth_at_edge[i] > river_p.h_thresh, @@ -360,8 +356,8 @@ function update_river_channel_flow!( river_v.zs_src[i], river_v.zs_dst[i], river_v.water_depth_at_edge[i], - river_v.flow_area_at_edge[i], - river_v.hydraulic_radius_at_edge[i], + flow_area_at_edge, + hydraulic_radius_at_edge, river_p.flow_length_at_edge[i], river_p.mannings_n_sq_at_edge[i], river_p.froude_limit, @@ -403,21 +399,21 @@ function update_river_channel_flow!( river_v.water_depth_at_edge[i] = (river_v.zs_max_at_edge[i] - river_p.zb_max_at_edge[i]) - river_v.flow_area_at_edge[i] = - river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] # flow area (rectangular channel) - river_v.hydraulic_radius_at_edge[i] = - river_v.flow_area_at_edge[i] / wetted_perimeter_channel( + # rectangular channel + flow_area_at_edge = river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] # flow area (rectangular channel) + hydraulic_radius_at_edge = + flow_area_at_edge / wetted_perimeter_channel( river_v.water_depth_at_edge[i], river_p.flow_width_at_edge[i], - ) # hydraulic radius (rectangular channel) + ) river_v.q[i] = ifelse( river_v.water_depth_at_edge[i] > river_p.h_thresh, manning_flow( river_p.mannings_n_at_edge[i], - river_v.hydraulic_radius_at_edge[i], + hydraulic_radius_at_edge, river_p.slope_at_edge[i], - river_v.flow_area_at_edge[i], + flow_area_at_edge, ), 0.0, ) @@ -477,40 +473,45 @@ function update_floodplain_flow!( i1, i2, ) - floodplain_v.flow_area_at_edge[i] = min(a_src, a_dst) + flow_area_at_edge = min(a_src, a_dst) - floodplain_v.hydraulic_radius_at_edge[i] = if a_src < a_dst + hydraulic_radius_at_edge = ifelse( + a_src < a_dst, a_src / compute_wetted_perimeter( profile, floodplain_v.water_depth_at_edge[i], i_src, i1, - ) - else + ), a_dst / compute_wetted_perimeter( profile, floodplain_v.water_depth_at_edge[i], i_dst, i1, - ) - end + ), + ) + hydraulic_radius_at_edge = ifelse( + floodplain_v.water_depth_at_edge[i] > profile.depth[i1], + hydraulic_radius_at_edge, + 0.0, + ) - floodplain_v.q[i] = if floodplain_v.flow_area_at_edge[i] > 1.0e-05 + floodplain_v.q[i] = ifelse( + flow_area_at_edge > 1.0e-05, local_inertial_flow( floodplain_v.q_previous[i], river_v.zs_src[i], river_v.zs_dst[i], floodplain_v.water_depth_at_edge[i], - floodplain_v.flow_area_at_edge[i], - floodplain_v.hydraulic_radius_at_edge[i], + flow_area_at_edge, + hydraulic_radius_at_edge, river_p.flow_length_at_edge[i], floodplain_p.mannings_n_sq_at_edge[i], river_p.froude_limit, dt, - ) - else - 0.0 - end + ), + 0.0, + ) # limit floodplain q in case water is not available if floodplain_v.h[i_src] <= 0.0 @@ -579,34 +580,39 @@ function update_floodplain_flow!( i1, i2, ) - floodplain_v.flow_area_at_edge[i] = min(a_src, a_dst) + flow_area_at_edge = min(a_src, a_dst) - floodplain_v.hydraulic_radius_at_edge[i] = if a_src < a_dst + hydraulic_radius_at_edge = ifelse( + a_src < a_dst, a_src / compute_wetted_perimeter( profile, floodplain_v.water_depth_at_edge[i], i_src, i1, - ) - else + ), a_dst / compute_wetted_perimeter( profile, floodplain_v.water_depth_at_edge[i], i_dst, i1, - ) - end + ), + ) + hydraulic_radius_at_edge = ifelse( + floodplain_v.water_depth_at_edge[i] > profile.depth[i1], + hydraulic_radius_at_edge, + 0.0, + ) - floodplain_v.q[i] = if floodplain_v.flow_area_at_edge[i] > 1.0e-05 + floodplain_v.q[i] = ifelse( + flow_area_at_edge > 1.0e-05, manning_flow( floodplain_p.mannings_n_at_edge[i], - floodplain_v.hydraulic_radius_at_edge[i], + hydraulic_radius_at_edge, floodplain_p.slope_at_edge[i], - floodplain_v.flow_area_at_edge[i], - ) - else - 0.0 - end + flow_area_at_edge, + ), + 0.0, + ) # limit floodplain q in case water is not available floodplain_v.q[i] = min(floodplain_v.q[i], floodplain_v.storage[i_src]/dt) diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index cb6027018..81112c41d 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -506,8 +506,6 @@ end @test river_flow_model.variables.zs_dst ≈ [314.3999938964844, 0.0] @test river_flow_model.variables.zs_max_at_edge ≈ [315.14484852086804, 0.0] @test river_flow_model.variables.water_depth_at_edge ≈ [0.04484241735241312, 0.0] - @test river_flow_model.variables.flow_area_at_edge ≈ [4.247964427147839, 0.0] - @test river_flow_model.variables.hydraulic_radius_at_edge ≈ [0.04480000374545946, 0.0] @test river_flow_model.variables.q ≈ [0.534558444239785, 0.0] @test river_flow_model.variables.q_cumulative ≈ [486.35699517356477, 0.0] @@ -673,10 +671,6 @@ end [166.98963199894177, 166.92754760786679] @test river_flow_model.variables.water_depth_at_edge ≈ [1.8817912224982933, 1.8197068314233036] - @test river_flow_model.variables.flow_area_at_edge ≈ - [280.7225571209805, 271.4609085323917] - @test river_flow_model.variables.hydraulic_radius_at_edge ≈ - [1.8354842671202385, 1.7763698223484754] @test river_flow_model.variables.q ≈ [137.1827776559179, 133.7538757670657] @test river_flow_model.variables.q_cumulative ≈ [6778.106459961183, 6608.686781773178] @@ -685,10 +679,6 @@ end @test river_flow_model.floodplain.variables.water_depth_at_edge ≈ [0.2896350506995873, 0.22755065962459753] @test river_flow_model.floodplain.variables.hf_index == [1, 2] - @test river_flow_model.floodplain.variables.flow_area_at_edge ≈ - [55.72535396228429, 117.78030190165774] - @test river_flow_model.floodplain.variables.hydraulic_radius_at_edge ≈ - [0.28876564013944644, 0.22735076093732] @test river_flow_model.floodplain.variables.q ≈ [3.3074672215578524, 6.1421232455587536] @test river_flow_model.floodplain.variables.q_cumulative ≈ @@ -847,10 +837,6 @@ end @test river_flow_model.variables.zs_max_at_edge ≈ [418.0325994578516, 418.0325994578516] @test river_flow_model.variables.water_depth_at_edge ≈ [2.4325933543360065, 2.4325933543360065] - @test river_flow_model.variables.flow_area_at_edge ≈ - [72.9778006300802, 72.9778006300802] - @test river_flow_model.variables.hydraulic_radius_at_edge ≈ - [2.093142401326319, 2.093142401326319] @test river_flow_model.variables.q ≈ [12.587380945649473, 12.587380945649473] @test river_flow_model.variables.q_cumulative ≈ [28607.999128032203, 28607.999128032203] @@ -859,10 +845,6 @@ end @test river_flow_model.floodplain.variables.water_depth_at_edge ≈ [1.4325933543360065, 1.4325933543360065] @test river_flow_model.floodplain.variables.hf_index == [1, 2] - @test river_flow_model.floodplain.variables.flow_area_at_edge ≈ - [62.01908306413688, 62.01908306413688] - @test river_flow_model.floodplain.variables.hydraulic_radius_at_edge ≈ - [1.3209041787011515, 1.3209041787011515] @test river_flow_model.floodplain.variables.q ≈ [0.050043034537710154, 3.279243909752683] From 2f8d803d34c17a953f959ffc5613d547af04c500 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 16 Jun 2026 19:50:54 +0200 Subject: [PATCH 78/90] Remove check active floodplain index Already part of release/v1.0 branch: this check was based on `water_depth_at_edge` of river nodes and should be based on `water_depth_at_edge` of floodplain nodes. However, testing showed there is no performance benefit from this check and looping over active floodplain indices. --- Wflow/src/routing/surface/floodplain.jl | 19 ------------- .../surface/surface_staggered_scheme.jl | 28 +++++++------------ Wflow/test/routing_process.jl | 2 -- 3 files changed, 10 insertions(+), 39 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 7d3a9bc5c..4b64741f4 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -228,8 +228,6 @@ end q_cumulative::Vector{Float64} = zeros(n_edges) # average discharge at edge [m³ s⁻¹] for model timestep dt q_average::Vector{Float64} = zeros(n_edges) - # edge index with `water_depth_at_edge` [-] above depth threshold - hf_index::Vector{Int} = zeros(Int, n_edges) # storage [m³] storage::Vector{Float64} = zeros(n) # water depth [m] @@ -354,23 +352,6 @@ function compute_floodplain_flow_area( return floodplain_flow_area end -function active_floodplain_cells(river_flow_model) - (; floodplain) = river_flow_model - (; water_depth_at_edge) = river_flow_model.variables - (; active_e, h_thresh) = river_flow_model.parameters - - n = 0 - @inbounds for i in active_e - @inbounds if water_depth_at_edge[i] > h_thresh - n += 1 - floodplain.variables.hf_index[n] = i - else - floodplain.variables.q[i] = 0.0 - end - end - return n -end - "Initialize floodplain geometry, model variables and parameters on staggered grid" function FloodPlainModel( dataset::NCDataset, diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 5123a77f1..d842f1e16 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -443,18 +443,14 @@ function update_floodplain_flow!( (; profile) = floodplain_p floodplain_v = river_flow_model.floodplain.variables - @batch per = thread minbatch = 1000 for i in 1:length(floodplain_v.water_depth_at_edge) - floodplain_v.water_depth_at_edge[i] = - max(river_v.zs_max_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) - end - - n = active_floodplain_cells(river_flow_model) - - @batch per = thread minbatch = 1000 for j in 1:n - i = floodplain_v.hf_index[j] + @batch per = thread minbatch = 1000 for j in eachindex(river_p.active_e) + i = river_p.active_e[j] i_src = nodes_at_edge.src[i] i_dst = nodes_at_edge.dst[i] + floodplain_v.water_depth_at_edge[i] = + max(river_v.zs_max_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) + i1, i2 = interpolation_indices( floodplain_v.water_depth_at_edge[i], @view profile.depth[:] @@ -550,18 +546,14 @@ function update_floodplain_flow!( (; profile) = floodplain_p floodplain_v = river_flow_model.floodplain.variables - @batch per = thread minbatch = 1000 for i in 1:length(floodplain_v.water_depth_at_edge) - floodplain_v.water_depth_at_edge[i] = - max(river_v.zs_max_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) - end - - n = active_floodplain_cells(river_flow_model) - - @batch per = thread minbatch = 1000 for j in 1:n - i = floodplain_v.hf_index[j] + @batch per = thread minbatch = 1000 for j in eachindex(river_p.active_e) + i = river_p.active_e[j] i_src = nodes_at_edge.src[i] i_dst = nodes_at_edge.dst[i] + floodplain_v.water_depth_at_edge[i] = + max(river_v.zs_max_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) + i1, i2 = interpolation_indices( floodplain_v.water_depth_at_edge[i], @view profile.depth[:] diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 81112c41d..086eda4cd 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -678,7 +678,6 @@ end @test river_flow_model.floodplain.variables.water_depth_at_edge ≈ [0.2896350506995873, 0.22755065962459753] - @test river_flow_model.floodplain.variables.hf_index == [1, 2] @test river_flow_model.floodplain.variables.q ≈ [3.3074672215578524, 6.1421232455587536] @test river_flow_model.floodplain.variables.q_cumulative ≈ @@ -844,7 +843,6 @@ end @test river_flow_model.floodplain.variables.water_depth_at_edge ≈ [1.4325933543360065, 1.4325933543360065] - @test river_flow_model.floodplain.variables.hf_index == [1, 2] @test river_flow_model.floodplain.variables.q ≈ [0.050043034537710154, 3.279243909752683] From 5faf1fd6d3048b42fd5b4e8fcc59b0ab27451b84 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 3 Jul 2026 17:00:56 +0200 Subject: [PATCH 79/90] Fix large `q` fluctuations `manning_staggered` routing By using the "upwind"option (instead of "max" as used for local inertial routing) for water level and depth estimation at edge. --- .../surface/surface_staggered_scheme.jl | 37 +++++++++++++------ Wflow/test/routing_process.jl | 32 ++++++++-------- Wflow/test/run_sbm.jl | 22 +++++------ 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index d842f1e16..65358c7a6 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -171,8 +171,8 @@ end q_channel_average::Vector{Float64} = q_average # water depth [m] h::Vector{Float64} - # maximum water elevation at edge [m] - zs_max_at_edge::Vector{Float64} = zeros(n_edges) + # water elevation at edge [m] + zs_at_edge::Vector{Float64} = zeros(n_edges) # water elevation of source node of edge [m] zs_src::Vector{Float64} = zeros(n_edges) # water elevation of downstream node of edge [m] @@ -337,9 +337,8 @@ function update_river_channel_flow!( river_v.zs_src[i] = river_p.zb[i_src] + river_v.h[i_src] river_v.zs_dst[i] = river_p.zb[i_dst] + river_v.h[i_dst] - river_v.zs_max_at_edge[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) - river_v.water_depth_at_edge[i] = - (river_v.zs_max_at_edge[i] - river_p.zb_max_at_edge[i]) + river_v.zs_at_edge[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) + river_v.water_depth_at_edge[i] = (river_v.zs_at_edge[i] - river_p.zb_max_at_edge[i]) # rectangular channel flow_area_at_edge = river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] @@ -395,12 +394,21 @@ function update_river_channel_flow!( river_v.zs_src[i] = river_p.zb[i_src] + river_v.h[i_src] river_v.zs_dst[i] = river_p.zb[i_dst] + river_v.h[i_dst] - river_v.zs_max_at_edge[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) - river_v.water_depth_at_edge[i] = - (river_v.zs_max_at_edge[i] - river_p.zb_max_at_edge[i]) + # use "upwind" option for water level and depth estimation at edge to avoid large q + # fluctuations (compared to "max" option) + river_v.zs_at_edge[i] = ifelse( + river_v.q[i] == 0.0, + max(river_v.zs_src[i], river_v.zs_dst[i]), + river_v.zs_src[i], + ) + river_v.water_depth_at_edge[i] = ifelse( + river_v.q[i] == 0.0, + river_v.zs_at_edge[i] - river_p.zb_max_at_edge[i], + river_v.h[i_src], + ) # rectangular channel - flow_area_at_edge = river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] # flow area (rectangular channel) + flow_area_at_edge = river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] hydraulic_radius_at_edge = flow_area_at_edge / wetted_perimeter_channel( river_v.water_depth_at_edge[i], @@ -449,7 +457,7 @@ function update_floodplain_flow!( i_dst = nodes_at_edge.dst[i] floodplain_v.water_depth_at_edge[i] = - max(river_v.zs_max_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) + max(river_v.zs_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) i1, i2 = interpolation_indices( floodplain_v.water_depth_at_edge[i], @@ -551,8 +559,13 @@ function update_floodplain_flow!( i_src = nodes_at_edge.src[i] i_dst = nodes_at_edge.dst[i] - floodplain_v.water_depth_at_edge[i] = - max(river_v.zs_max_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) + # use "upwind" option for water level and depth estimation at edge to avoid large q + # fluctuations (compared to "max" option) + floodplain_v.water_depth_at_edge[i] = ifelse( + floodplain_v.q[i] == 0.0, + max(river_v.zs_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0), + floodplain_v.h[i_src], + ) i1, i2 = interpolation_indices( floodplain_v.water_depth_at_edge[i], diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 086eda4cd..4d32cf1ba 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -504,7 +504,7 @@ end @test dt ≈ 909.829412320351 @test river_flow_model.variables.zs_src ≈ [315.14484852086804, 0.0] @test river_flow_model.variables.zs_dst ≈ [314.3999938964844, 0.0] - @test river_flow_model.variables.zs_max_at_edge ≈ [315.14484852086804, 0.0] + @test river_flow_model.variables.zs_at_edge ≈ [315.14484852086804, 0.0] @test river_flow_model.variables.water_depth_at_edge ≈ [0.04484241735241312, 0.0] @test river_flow_model.variables.q ≈ [0.534558444239785, 0.0] @test river_flow_model.variables.q_cumulative ≈ [486.35699517356477, 0.0] @@ -667,8 +667,7 @@ end @test dt ≈ 49.40931052556788 @test river_flow_model.variables.zs_src ≈ [166.98963199894177, 166.92754760786679] @test river_flow_model.variables.zs_dst ≈ [166.92754760786679, 166.86980277988906] - @test river_flow_model.variables.zs_max_at_edge ≈ - [166.98963199894177, 166.92754760786679] + @test river_flow_model.variables.zs_at_edge ≈ [166.98963199894177, 166.92754760786679] @test river_flow_model.variables.water_depth_at_edge ≈ [1.8817912224982933, 1.8197068314233036] @test river_flow_model.variables.q ≈ [137.1827776559179, 133.7538757670657] @@ -833,21 +832,20 @@ end @test dt ≈ 2272.7523105527266 @test river_flow_model.variables.zs_src ≈ [416.6029533901387, 418.03259945785163] @test river_flow_model.variables.zs_dst ≈ [418.03259945785163, 416.00221556263983] - @test river_flow_model.variables.zs_max_at_edge ≈ [418.0325994578516, 418.0325994578516] + @test river_flow_model.variables.zs_at_edge ≈ [416.6029533901387, 418.03259945785163] @test river_flow_model.variables.water_depth_at_edge ≈ - [2.4325933543360065, 2.4325933543360065] - @test river_flow_model.variables.q ≈ [12.587380945649473, 12.587380945649473] - @test river_flow_model.variables.q_cumulative ≈ [28607.999128032203, 28607.999128032203] + [1.0029472866230495, 2.432593354336019] + @test river_flow_model.variables.q ≈ [3.0436243193280137, 12.587380945649572] + @test river_flow_model.variables.q_cumulative ≈ [6917.404204207213, 28607.999128032432] Wflow.update_floodplain_flow!(river_flow_model, domain.river, dt) @test river_flow_model.floodplain.variables.water_depth_at_edge ≈ - [1.4325933543360065, 1.4325933543360065] - @test river_flow_model.floodplain.variables.q ≈ - [0.050043034537710154, 3.279243909752683] + [0.0029472866230494782, 1.432593354336019] + @test river_flow_model.floodplain.variables.q ≈ [8.50693948416018e-5, 3.279243909752732] @test river_flow_model.floodplain.variables.q_cumulative ≈ - [113.73542237265065, 7452.909172756367] + [0.1933416636835727, 7452.909172756478] Wflow.update_bc_reservoir_model!( river_flow_model.boundary_conditions.reservoir, @@ -859,9 +857,9 @@ end Wflow.update_water_depth_and_storage!(river_flow_model, domain.river, dt) @test river_flow_model.variables.storage ≈ - [8949.81915717859, 76573.27391575256, 39343.45963085314] + [30640.414081003582, 54882.67899192734, 39343.459630853366] @test river_flow_model.variables.h ≈ - [0.2471770951182529, 2.528578999550164, 1.5298322010636003] + [0.8462303441838974, 1.8123188736937599, 1.529832201063609] Wflow.update_water_depth_and_storage!( river_flow_model.floodplain, @@ -871,13 +869,13 @@ end ) @test river_flow_model.variables.storage ≈ - [8949.81915717859, 71871.75158640924, 33570.77415623845] + [30753.95616171255, 63042.829748341144, 33570.77415623857] @test river_flow_model.variables.h ≈ - [0.2471770951182529, 2.373326781381025, 1.3053669352090385] + [0.849366161923948, 2.0817808514920815, 1.3053669352090433] @test river_flow_model.floodplain.variables.storage ≈ - [0.0, 59966.73673951485, 13225.594647371057] + [0.0, 46991.52157304865, 13225.594647371276] @test river_flow_model.floodplain.variables.h ≈ - [0.0, 1.3733267813810248, 0.3053669352090384] + [0.0, 1.0817808514920815, 0.3053669352090434] end @testitem "unit: kinematic river flow including 1D floodplain schematization" begin diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index 552f4bea6..4efc75e66 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -1163,18 +1163,18 @@ end Wflow.run_timestep!(model) (; q_average, h) = river_flow.variables - @test sum(q_average) ≈ 2184.304753007843 - @test q_average[1622] ≈ 0.0002190265220138185 - @test q_average[43] ≈ 10.354333905435187 - @test q_average[501] ≈ 0.02141198454056767 - @test q_average[5808] ≈ 0.004920531981445287 - @test h[1622] ≈ 0.0017967526822043184 - @test h[43] ≈ 1.3192174478288576 - @test h[501] ≈ 0.005400600297111819 - @test h[5808] ≈ 0.005892191643178794 + @test sum(q_average) ≈ 2154.461702897435 + @test q_average[1622] ≈ 0.00021785973058570252 + @test q_average[43] ≈ 10.354328744948932 + @test q_average[501] ≈ 0.02130865759028468 + @test q_average[5808] ≈ 0.00491918818651958 + @test h[1622] ≈ 0.0017917389093431615 + @test h[43] ≈ 1.3192092568550473 + @test h[501] ≈ 0.005359393674061471 + @test h[5808] ≈ 0.005889217903694072 (; q_average, h) = river_flow.floodplain.variables - @test maximum(q_average) ≈ 1.0162290359867245 - @test maximum(h) ≈ 1.149051908326585 + @test maximum(q_average) ≈ 0.8621921506398305 + @test maximum(h) ≈ 1.109416852290252 end @testitem "Kinematic river flow including 1D floodplain schematization" begin From 36b12d9016776540f5d50da08712daf234ec0255 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 3 Jul 2026 17:57:46 +0200 Subject: [PATCH 80/90] Address review comment --- Wflow/src/routing/surface/floodplain.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 4b64741f4..88284824b 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -309,8 +309,8 @@ function compute_flood_flow_area( i1::Int, i2::Int, ) - dh = h - profile.depth[i1] # depth at i1 - flow_area = profile.flow_area[i1, idx] + (profile.width[i2, idx] * dh) # area at i1, width at i2 + delta_h = h - profile.depth[i1] # depth at i1 + flow_area = profile.flow_area[i1, idx] + (profile.width[i2, idx] * delta_h) # area at i1, width at i2 return flow_area end @@ -319,8 +319,8 @@ Compute floodplain wetted perimeter based on flow depth `h` and floodplain `dept `wetted_perimeter` of a floodplain profile. """ function compute_wetted_perimeter(profile::FloodPlainProfile, h::Float64, idx::Int, i1::Int) - dh = h - profile.depth[i1] # depth at i1 - wetted_perimeter = profile.wetted_perimeter[i1, idx] + 2.0 * dh # p at i1 + delta_h = h - profile.depth[i1] # depth at i1 + wetted_perimeter = profile.wetted_perimeter[i1, idx] + 2.0 * delta_h # p at i1 return wetted_perimeter end @@ -332,9 +332,9 @@ function compute_flood_depth( i::Int, ) i1, i2 = interpolation_indices(flood_storage, @view profile.storage[:, i]) - ΔA = (flood_storage - profile.storage[i1, i]) / flow_length - dh = ΔA / profile.width[i2, i] - flood_depth = profile.depth[i1] + dh + delta_A = (flood_storage - profile.storage[i1, i]) / flow_length + delta_h = delta_A / profile.width[i2, i] + flood_depth = profile.depth[i1] + delta_h return flood_depth end From 61b5d5f710529009fb2078609deeeafa5f021929 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Fri, 3 Jul 2026 22:20:50 +0200 Subject: [PATCH 81/90] Small change "upwind" option `manning_staggered` For zero flows the maximum waterlevel between two adjacent cells was computed. For slope based mannings flow (one direction) this is not required. --- Wflow/src/routing/surface/floodplain.jl | 9 ++--- .../surface/surface_staggered_scheme.jl | 34 +++++++------------ Wflow/test/routing_process.jl | 18 +++++----- Wflow/test/run_sbm.jl | 16 ++++----- 4 files changed, 34 insertions(+), 43 deletions(-) diff --git a/Wflow/src/routing/surface/floodplain.jl b/Wflow/src/routing/surface/floodplain.jl index 88284824b..bf1f527a2 100644 --- a/Wflow/src/routing/surface/floodplain.jl +++ b/Wflow/src/routing/surface/floodplain.jl @@ -156,8 +156,8 @@ end mannings_n_at_edge::Vector{Float64} = Float64[] # manning's roughness squared at edge [(s m-1/3)2] mannings_n_sq_at_edge::Vector{Float64} = Float64[] - # maximum bankfull elevation at edge [m] - zb_max_at_edge::Vector{Float64} = Float64[] + # bankfull elevation at edge [m] + zb_at_edge::Vector{Float64} = Float64[] # slope at edge [-] slope_at_edge::Vector{Float64} = Float64[] end @@ -186,12 +186,13 @@ function FloodPlainStaggeredParameters( append!(mannings_n, mannings_n[index_pit]) # copy to ghost nodes mannings_n_at_edge = compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) - zb_max_at_edge = compute_value_at_edge(zb_floodplain, nodes_at_edge, n_edges, maximum) if river_routing == RoutingType.local_inertial + zb_at_edge = compute_value_at_edge(zb_floodplain, nodes_at_edge, n_edges, maximum) mannings_n_sq_at_edge = mannings_n_at_edge .* mannings_n_at_edge slope_at_edge = [] elseif river_routing == RoutingType.manning_staggered + zb_at_edge = compute_value_at_edge(zb_floodplain, nodes_at_edge, n_edges, first) mannings_n_sq_at_edge = [] flow_length_at_edge = compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) @@ -208,7 +209,7 @@ function FloodPlainStaggeredParameters( mannings_n, mannings_n_at_edge, mannings_n_sq_at_edge, - zb_max_at_edge, + zb_at_edge, slope_at_edge, ) return parameters diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 65358c7a6..f1601bd22 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -18,8 +18,8 @@ bankfull_storage::Vector{Float64} = Float64[] # bankfull depth [m] bankfull_depth::Vector{Float64} = Float64[] - # maximum channel bed elevation at edge [m] - zb_max_at_edge::Vector{Float64} = Float64[] + # channel bed elevation at edge [m] + zb_at_edge::Vector{Float64} = Float64[] # Manning's roughness [s m-1/3] mannings_n::Vector{Float64} = Float64[] # Manning's roughness at edge [s m-1/3] @@ -118,15 +118,16 @@ function RiverFlowStaggeredParameters( bankfull_storage = bankfull_depth .* flow_width .* flow_length # determine parameters at edges - zb_max_at_edge = compute_value_at_edge(zb, nodes_at_edge, n_edges, maximum) flow_width_at_edge = compute_value_at_edge(flow_width, nodes_at_edge, n_edges, minimum) flow_length_at_edge = compute_value_at_edge(flow_length, nodes_at_edge, n_edges, mean) mannings_n_at_edge = compute_mannings_n_at_edge(mannings_n, flow_length, nodes_at_edge, n_edges) if river_routing == RoutingType.local_inertial + zb_at_edge = compute_value_at_edge(zb, nodes_at_edge, n_edges, maximum) mannings_n_sq_at_edge = mannings_n_at_edge .* mannings_n_at_edge slope_at_edge = [] elseif river_routing == RoutingType.manning_staggered + zb_at_edge = compute_value_at_edge(zb, nodes_at_edge, n_edges, first) mannings_n_sq_at_edge = [] slope_at_edge = compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) @@ -142,7 +143,7 @@ function RiverFlowStaggeredParameters( zb, bankfull_storage, bankfull_depth, - zb_max_at_edge, + zb_at_edge, mannings_n, mannings_n_at_edge, mannings_n_sq_at_edge, @@ -338,7 +339,7 @@ function update_river_channel_flow!( river_v.zs_dst[i] = river_p.zb[i_dst] + river_v.h[i_dst] river_v.zs_at_edge[i] = max(river_v.zs_src[i], river_v.zs_dst[i]) - river_v.water_depth_at_edge[i] = (river_v.zs_at_edge[i] - river_p.zb_max_at_edge[i]) + river_v.water_depth_at_edge[i] = (river_v.zs_at_edge[i] - river_p.zb_at_edge[i]) # rectangular channel flow_area_at_edge = river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] @@ -396,16 +397,8 @@ function update_river_channel_flow!( # use "upwind" option for water level and depth estimation at edge to avoid large q # fluctuations (compared to "max" option) - river_v.zs_at_edge[i] = ifelse( - river_v.q[i] == 0.0, - max(river_v.zs_src[i], river_v.zs_dst[i]), - river_v.zs_src[i], - ) - river_v.water_depth_at_edge[i] = ifelse( - river_v.q[i] == 0.0, - river_v.zs_at_edge[i] - river_p.zb_max_at_edge[i], - river_v.h[i_src], - ) + river_v.zs_at_edge[i] = river_v.zs_src[i] + river_v.water_depth_at_edge[i] = river_v.h[i_src] # rectangular channel flow_area_at_edge = river_p.flow_width_at_edge[i] * river_v.water_depth_at_edge[i] @@ -457,7 +450,7 @@ function update_floodplain_flow!( i_dst = nodes_at_edge.dst[i] floodplain_v.water_depth_at_edge[i] = - max(river_v.zs_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0) + max(river_v.zs_at_edge[i] - floodplain_p.zb_at_edge[i], 0.0) i1, i2 = interpolation_indices( floodplain_v.water_depth_at_edge[i], @@ -559,13 +552,10 @@ function update_floodplain_flow!( i_src = nodes_at_edge.src[i] i_dst = nodes_at_edge.dst[i] - # use "upwind" option for water level and depth estimation at edge to avoid large q + # use "upwind" option for water depth estimation at edge to avoid large q # fluctuations (compared to "max" option) - floodplain_v.water_depth_at_edge[i] = ifelse( - floodplain_v.q[i] == 0.0, - max(river_v.zs_at_edge[i] - floodplain_p.zb_max_at_edge[i], 0.0), - floodplain_v.h[i_src], - ) + floodplain_v.water_depth_at_edge[i] = + max(river_v.zs_at_edge[i] - floodplain_p.zb_at_edge[i], 0.0) i1, i2 = interpolation_indices( floodplain_v.water_depth_at_edge[i], diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 4d32cf1ba..77072ea0d 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -456,7 +456,7 @@ end froude_limit = true, h_thresh = 0.001, zb = [315.1000061035156, 314.3999938964844, 278.8000183105469], - zb_max_at_edge = [315.1000061035156, 314.3999938964844], + zb_at_edge = [315.1000061035156, 314.3999938964844], bankfull_storage = [57155.32165002823, 100397.03622722626, 90180.89622545242], bankfull_depth = [1.0, 1.0, 1.0], mannings_n_sq_at_edge = [0.0008999999597668652, 0.0008999999597668652], @@ -575,7 +575,7 @@ end froude_limit = true, h_thresh = 0.001, zb = [165.10784077644348, 165.10784077644348, 165.10784077644348], - zb_max_at_edge = [165.10784077644348, 165.10784077644348], + zb_at_edge = [165.10784077644348, 165.10784077644348], bankfull_storage = [107921.00118967971, 200292.17449130033, 69688.46493603183], bankfull_depth = [1.592156171798706, 1.592156171798706, 1.592156171798706], mannings_n = [0.029999999329447746, 0.029999999329447746, 0.029999999329447746], @@ -632,7 +632,7 @@ end mannings_n = [0.072, 0.072, 0.072], mannings_n_at_edge = [0.072, 0.072], mannings_n_sq_at_edge = [0.005184, 0.005184], - zb_max_at_edge = [166.6999969482422, 166.6999969482422], + zb_at_edge = [166.6999969482422, 166.6999969482422], ), variables = Wflow.FloodPlainStaggeredVariables(; n, @@ -738,7 +738,7 @@ end active_e = [1, 2], h_thresh = 0.001, zb = [415.6000061035156, 415.6000061035156, 415.6000061035156], - zb_max_at_edge = [415.6000061035156, 415.6000061035156], + zb_at_edge = [415.6000061035156, 415.6000061035156], bankfull_storage = [36208.125, 30283.125, 25717.5], bankfull_depth = [1.0, 1.0, 1.0], mannings_n = [0.029999999329447746, 0.029999999329447746, 0.029999999329447746], @@ -795,7 +795,7 @@ end ), mannings_n = [0.072, 0.072, 0.072], mannings_n_at_edge = [0.072, 0.072], - zb_max_at_edge = [416.6000061035156, 416.6000061035156], + zb_at_edge = [416.6000061035156, 416.6000061035156], slope_at_edge = [1.0e-5, 1.0e-5], ), variables = Wflow.FloodPlainStaggeredVariables(; @@ -1116,7 +1116,7 @@ end froude_limit = true, h_thresh = 1e-3, zb = [], - zb_max_at_edge = [], + zb_at_edge = [], bankfull_storage = [171137.5821314017], bankfull_depth = [1.3683528900146484], mannings_n_sq_at_edge = [], @@ -1254,12 +1254,12 @@ end n_edges = ne(graph) # determine z, width, length and manning's n at edges - zb_max_at_edge = fill(0.0, n_edges) + zb_at_edge = fill(0.0, n_edges) flow_width_at_edge = fill(0.0, n_edges) flow_length_at_edge = fill(0.0, n_edges) mannings_n_sq_at_edge = fill(0.0, n_edges) for i in 1:n_edges - zb_max_at_edge[i] = max(zb[nodes_at_edge.src[i]], zb[nodes_at_edge.dst[i]]) + zb_at_edge[i] = max(zb[nodes_at_edge.src[i]], zb[nodes_at_edge.dst[i]]) flow_width_at_edge[i] = min(width[nodes_at_edge.dst[i]], width[nodes_at_edge.src[i]]) flow_length_at_edge[i] = 0.5 * (dl[nodes_at_edge.dst[i]] + dl[nodes_at_edge.src[i]]) @@ -1298,7 +1298,7 @@ end active_n = collect(1:(n - 1)), active_e = collect(1:n_edges), h_thresh, - zb_max_at_edge, + zb_at_edge, mannings_n_sq_at_edge, flow_width_at_edge, flow_length_at_edge, diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index 4efc75e66..cb4198c77 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -1163,17 +1163,17 @@ end Wflow.run_timestep!(model) (; q_average, h) = river_flow.variables - @test sum(q_average) ≈ 2154.461702897435 - @test q_average[1622] ≈ 0.00021785973058570252 + @test sum(q_average) ≈ 2154.377097406722 + @test q_average[1622] ≈ 0.00021688684091240499 @test q_average[43] ≈ 10.354328744948932 - @test q_average[501] ≈ 0.02130865759028468 - @test q_average[5808] ≈ 0.00491918818651958 - @test h[1622] ≈ 0.0017917389093431615 + @test q_average[501] ≈ 0.021302052064530574 + @test q_average[5808] ≈ 0.004918586569211439 + @test h[1622] ≈ 0.0017876514245616358 @test h[43] ≈ 1.3192092568550473 - @test h[501] ≈ 0.005359393674061471 - @test h[5808] ≈ 0.005889217903694072 + @test h[501] ≈ 0.005358355348907546 + @test h[5808] ≈ 0.005888712037653059 (; q_average, h) = river_flow.floodplain.variables - @test maximum(q_average) ≈ 0.8621921506398305 + @test maximum(q_average) ≈ 0.8621923238703156 @test maximum(h) ≈ 1.109416852290252 end From 20ce151f0b9d98730e5a59239bbe6ae2e9cd69e8 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 6 Jul 2026 12:02:38 +0200 Subject: [PATCH 82/90] Address review comment Fix floodplain states check. --- Wflow/src/states.jl | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Wflow/src/states.jl b/Wflow/src/states.jl index 8fcefc7d0..a7cb2f087 100644 --- a/Wflow/src/states.jl +++ b/Wflow/src/states.jl @@ -124,9 +124,16 @@ function extract_required_states(config::Config) end # Floodplain states - floodplain_states = - do_floodplains ? - get_states_by_tag(Routing, :local_inertial_floodplain_1D_flow_state) : String[] + floodplain_states = if !do_floodplains + String[] + else + if config.model.river_routing == RoutingType.local_inertial || + config.model.river_routing == RoutingType.manning_staggered + get_states_by_tag(Routing, :staggered_grid_floodplain_1D_flow_state) + else + get_states_by_tag(Routing, :kinematic_wave_floodplain_1D_flow_state) + end + end # Reservoir states reservoir_states = From 7fe5253b3418082bee682c5feed42ca6d449796a Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 8 Jul 2026 10:26:00 +0200 Subject: [PATCH 83/90] Fix states kinematic wave flood routing The floodplain water depth was incorrect as it was based on the floodplain storage excluding river channel storage using the floodplain profile, while river channel storage should be included. This is now resolved by using the floodplain storage as state instead of the water depth. Additonally, for floodplain routing a check if the downstream floodplain is active (based on flow area) has been added. --- Wflow/src/io.jl | 2 +- Wflow/src/routing/surface/surface_kinwave.jl | 41 ++++++++++++------- Wflow/src/sbm_model.jl | 10 ++++- .../standard_name/standard_name_routing.jl | 5 +-- Wflow/test/routing_process.jl | 1 - Wflow/test/run_sbm.jl | 10 ++--- .../test/sbm_river-floodplain-kw_config.toml | 3 +- 7 files changed, 44 insertions(+), 28 deletions(-) diff --git a/Wflow/src/io.jl b/Wflow/src/io.jl index 613b947c2..2f088389e 100644 --- a/Wflow/src/io.jl +++ b/Wflow/src/io.jl @@ -682,7 +682,7 @@ function get_reducer_func(col, domain, args...) domain.land.network.indices, args..., ) - elseif occursin("river", parameter) + elseif occursin("river", parameter) || occursin("floodplain", parameter) reducer_func = reducer( col, domain.river.network.reverse_indices, diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index fa5df7d2a..7750853b2 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -323,12 +323,12 @@ function kinwave_land_update!( ) end - q[v], crossarea = + q[v], flow_area = kinematic_wave(qin[v], q[v], qlat[v], alpha[v], dt, flow_length[v]) # update h, only if flow width > 0.0 if surface_flow_width[v] > 0.0 - h[v] = crossarea / surface_flow_width[v] + h[v] = flow_area / surface_flow_width[v] end storage[v] = flow_length[v] * surface_flow_width[v] * h[v] @@ -391,20 +391,36 @@ function update_floodplain_model!( ) where {T <: KinematicWave, F <: FloodPlainModel{<:Manning}} (; floodplain) = river_flow_model (; flow_length) = domain.parameters + (; graph, order) = domain.network (; profile, mannings_n, slope) = floodplain.parameters (; flow_capacity, h, q, q_cumulative, qin, qin_cumulative, storage) = floodplain.variables - for i in eachindex(flow_capacity) - if h[i] > 0.0 - i1, i2 = interpolation_indices(h[i], @view profile.depth[:]) - flow_area = compute_floodplain_flow_area(profile, h[i], i, i1, i2) - wetted_perimeter = compute_wetted_perimeter(profile, h[i], i, i1) - hydraulic_radius = flow_area/wetted_perimeter - flow_capacity[i] = - manning_flow(mannings_n[i], hydraulic_radius, slope[i], flow_area) + for (i, v) in enumerate(order) + i_ds = outneighbors(graph, v) + n = length(i_ds) + (n > 1) && error("bifurcations not supported") + to_pit = iszero(n) + if h[v] > 0.0 + # check if downstream floodplain is active based on minimum flow area + i1, i2 = interpolation_indices(h[v], @view profile.depth[:]) + flow_area = compute_floodplain_flow_area(profile, h[v], v, i1, i2) + if !to_pit + flow_area_ds = + compute_floodplain_flow_area(profile, h[v], only(i_ds), i1, i2) + else + flow_area_ds = flow_area + end + if flow_area > 1.0e-05 && flow_area_ds > 1.0e-05 + wetted_perimeter = compute_wetted_perimeter(profile, h[v], v, i1) + hydraulic_radius = flow_area/wetted_perimeter + flow_capacity[v] = + manning_flow(mannings_n[v], hydraulic_radius, slope[v], flow_area) + else + flow_capacity[v] = 0.0 + end else - flow_capacity[i] = 0.0 + flow_capacity[v] = 0.0 end end q .= accucapacityflux(storage, domain.network, flow_capacity, dt) @@ -413,9 +429,6 @@ function update_floodplain_model!( flux_in!(qin, q, domain.network) @. qin_cumulative += qin * dt - for i in eachindex(storage) - h[i] = compute_flood_depth(profile, storage[i], flow_length[i], i) - end end update_floodplain_model!( diff --git a/Wflow/src/sbm_model.jl b/Wflow/src/sbm_model.jl index 68db24817..d70015abc 100644 --- a/Wflow/src/sbm_model.jl +++ b/Wflow/src/sbm_model.jl @@ -106,7 +106,13 @@ function set_states!(model::AbstractModel{<:Union{SbmModel, SbmGwfModel}}) land_v = routing.overland_flow.variables river_v = routing.river_flow.variables - (; land_routing, cold_start__flag, reservoir__flag, floodplain_1d__flag) = config.model + (; + river_routing, + land_routing, + cold_start__flag, + reservoir__flag, + floodplain_1d__flag, + ) = config.model # read and set states in model object if cold_start=false if !cold_start__flag @@ -165,7 +171,7 @@ function set_states!(model::AbstractModel{<:Union{SbmModel, SbmGwfModel}}) river_v.storage[1:nriv] .= river_v.h[1:nriv] .* flow_width[1:nriv] .* flow_length[1:nriv] - if floodplain_1d__flag + if floodplain_1d__flag && river_routing != RoutingType.kinematic_wave initialize_storage!(routing.river_flow, domain, nriv) end diff --git a/Wflow/src/standard_name/standard_name_routing.jl b/Wflow/src/standard_name/standard_name_routing.jl index 4cabf2303..374acda43 100644 --- a/Wflow/src/standard_name/standard_name_routing.jl +++ b/Wflow/src/standard_name/standard_name_routing.jl @@ -447,20 +447,19 @@ const routing_standard_name_map = OrderedDict{String, ParameterMetadata}( tags = [ :staggered_grid_floodplain_1D_flow_state, :staggered_grid_floodplain_1D_flow_output, - :kinematic_wave_floodplain_1D_flow_state, - :kinematic_wave_floodplain_1D_flow_output, ], ), - #### Output "floodplain_water__volume" => ParameterMetadata(; lens = @optic(_.routing.river_flow.floodplain.variables.storage), unit = Unit(; m = 3), description = "Floodplain water volume", tags = [ :staggered_grid_floodplain_1D_flow_output, + :kinematic_wave_floodplain_1D_flow_state, :kinematic_wave_floodplain_1D_flow_output, ], ), + #### Output "floodplain_water__volume_flow_rate" => ParameterMetadata(; lens = @optic(_.routing.river_flow.floodplain.variables.q_average), unit = Unit(; m = 3, s = -1), diff --git a/Wflow/test/routing_process.jl b/Wflow/test/routing_process.jl index 77072ea0d..21142c55b 100644 --- a/Wflow/test/routing_process.jl +++ b/Wflow/test/routing_process.jl @@ -1002,7 +1002,6 @@ end Wflow.update_floodplain_model!(river_flow_model, domain, dt) - @test river_flow_model.floodplain.variables.h ≈ [0.3054891336736904, 0.2126646996773224] @test river_flow_model.floodplain.variables.storage ≈ [52745.30941770246, 41649.967280840756] @test river_flow_model.floodplain.variables.q ≈ [3.752513987924126, 2.7693140810933157] diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index cb4198c77..ec3a55ce2 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -1189,17 +1189,17 @@ end (; q_average, h) = river_flow.variables @test sum(q_average) ≈ 3716.1374347813075 - @test q_average[1622] ≈ 0.0007502658165783512 + @test q_average[1622] ≈ 0.0007502658174033337 @test q_average[43] ≈ 11.640174323692953 - @test q_average[501] ≈ 0.18015658196174816 + @test q_average[501] ≈ 0.1801556062913406 @test q_average[5808] ≈ 0.04397662216730808 @test h[1622] ≈ 0.000979026922365117 @test h[43] ≈ 0.1629855245095962 - @test h[501] ≈ 0.16313139291053447 + @test h[501] ≈ 0.16313131033747957 @test h[5808] ≈ 0.005801893758984071 (; q_average, h) = river_flow.floodplain.variables - @test maximum(q_average) ≈ 6.1187682616864505 - @test maximum(h) ≈ 0.12485258241910993 + @test maximum(q_average) ≈ 6.118766353571045 + @test maximum(h) ≈ 1.3772452681705927 end @testitem "run wflow sbm" begin diff --git a/Wflow/test/sbm_river-floodplain-kw_config.toml b/Wflow/test/sbm_river-floodplain-kw_config.toml index ae4d1412e..688899fdb 100644 --- a/Wflow/test/sbm_river-floodplain-kw_config.toml +++ b/Wflow/test/sbm_river-floodplain-kw_config.toml @@ -36,8 +36,7 @@ snowpack_liquid_water__depth = "snowwater" river_water__depth = "h_river" river_water__instantaneous_volume_flow_rate = "q_river" -floodplain_water__instantaneous_volume_flow_rate = "q_floodplain" -floodplain_water__depth = "h_floodplain" +floodplain_water__volume = "storage_floodplain" reservoir_water_surface__elevation = "waterlevel_reservoir" subsurface_water__instantaneous_volume_flow_rate = "ssf" From 444e263b104d85907710446b2c89833b71ba89f6 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 8 Jul 2026 11:30:08 +0200 Subject: [PATCH 84/90] Update docs Model structure --- Wflow/src/Wflow.jl | 2 +- docs/developments/model_struct.qmd | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Wflow/src/Wflow.jl b/Wflow/src/Wflow.jl index c5bb24a32..6c1d61783 100644 --- a/Wflow/src/Wflow.jl +++ b/Wflow/src/Wflow.jl @@ -141,7 +141,7 @@ include("routing/routing.jl") include("domain.jl") """ - Model{R <: Routing, L <: AbstractLandModel, M <: AbstractMassBalance, T <: AbstractModelType} <: AbstractModel{T} + Model{R <: Routing, L <: AbstractLandModel, M <: AbstractMassBalance, W <: Writer, T <: AbstractModelType} <: AbstractModel{T} Composite type that represents all different aspects of a Wflow Model, such as the network, parameters, clock, configuration and input and output. diff --git a/docs/developments/model_struct.qmd b/docs/developments/model_struct.qmd index 6a277c60b..ba976d82d 100644 --- a/docs/developments/model_struct.qmd +++ b/docs/developments/model_struct.qmd @@ -8,7 +8,7 @@ configuration and input and output. ```julia """ - Model{R <: Routing, L <: AbstractLandModel, M <: AbstractMassBalance, T <: AbstractModelType} <: AbstractModel{T} + Model{R <: Routing, L <: AbstractLandModel, M <: AbstractMassBalance, W <: Writer, T <: AbstractModelType} <: AbstractModel{T} Composite type that represents all different aspects of a Wflow Model, such as the network, parameters, clock, configuration and input and output. @@ -17,6 +17,7 @@ struct Model{ R <: Routing, L <: AbstractLandModel, M <: AbstractMassBalance, + W <: Writer, T <: AbstractModelType, } <: AbstractModel{T} config::Config # all configuration options @@ -26,7 +27,7 @@ struct Model{ mass_balance::M # mass balance error clock::Clock # to keep track of simulation time reader::NCReader # provides the model with dynamic input - writer::Writer # writes model output + writer::W # writes model output type::T # model type end ``` From 13dd728b126d304ceb91795985c2e3e60b8becd5 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 8 Jul 2026 11:36:24 +0200 Subject: [PATCH 85/90] Update docs state kinematic wave floodplain --- docs/user_guide/model_config.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide/model_config.qmd b/docs/user_guide/model_config.qmd index f14f6646e..6f6180312 100644 --- a/docs/user_guide/model_config.qmd +++ b/docs/user_guide/model_config.qmd @@ -88,7 +88,7 @@ floodplain_water__sum_of_volume_per_depth = "floodplain_volume" floodplain_water_flow__manning_n_parameter = "floodplain_n" [state.variables] -floodplain_water__depth = "h_floodplain" +floodplain_water__volume = "storage_floodplain" ``` ### 2D floodplains (local inertial routing) From 58fdc0115340db53851dfe5ee4184ece9f255bd2 Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Mon, 13 Jul 2026 19:29:02 +0200 Subject: [PATCH 86/90] Update tests --- Wflow/test/run_sbm.jl | 44 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index 88d5e23e4..cd06e55aa 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -1163,18 +1163,18 @@ end Wflow.run_timestep!(model) (; q_average, h) = river_flow.variables - @test sum(q_average) ≈ 2154.377097406722 - @test q_average[1622] ≈ 0.00021688684091240499 - @test q_average[43] ≈ 10.354328744948932 - @test q_average[501] ≈ 0.021302052064530574 - @test q_average[5808] ≈ 0.004918586569211439 - @test h[1622] ≈ 0.0017876514245616358 - @test h[43] ≈ 1.3192092568550473 - @test h[501] ≈ 0.005358355348907546 - @test h[5808] ≈ 0.005888712037653059 + @test sum(q_average) ≈ 2156.7740284069446 + @test q_average[1622] ≈ 0.000216893766604954 + @test q_average[43] ≈ 10.362921270030188 + @test q_average[501] ≈ 0.021301823784704774 + @test q_average[5808] ≈ 0.0049185166802011605 + @test h[1622] ≈ 0.001787783986962345 + @test h[43] ≈ 1.3195684133779013 + @test h[501] ≈ 0.005358321292557224 + @test h[5808] ≈ 0.005888613968265871 (; q_average, h) = river_flow.floodplain.variables - @test maximum(q_average) ≈ 0.8621923238703156 - @test maximum(h) ≈ 1.109416852290252 + @test maximum(q_average) ≈ 0.8692896468290046 + @test maximum(h) ≈ 1.1111707025189723 end @testitem "Kinematic river flow including 1D floodplain schematization" begin @@ -1188,18 +1188,18 @@ end Wflow.run_timestep!(model) (; q_average, h) = river_flow.variables - @test sum(q_average) ≈ 3716.1374347813075 - @test q_average[1622] ≈ 0.0007502658174033337 - @test q_average[43] ≈ 11.640174323692953 - @test q_average[501] ≈ 0.1801556062913406 - @test q_average[5808] ≈ 0.04397662216730808 - @test h[1622] ≈ 0.000979026922365117 - @test h[43] ≈ 0.1629855245095962 - @test h[501] ≈ 0.16313131033747957 - @test h[5808] ≈ 0.005801893758984071 + @test sum(q_average) ≈ 3720.480709961395 + @test q_average[1622] ≈ 0.0007502671736663475 + @test q_average[43] ≈ 11.647181706571397 + @test q_average[501] ≈ 0.18574032636015966 + @test q_average[5808] ≈ 0.0439765670778149 + @test h[1622] ≈ 0.0009790264545664975 + @test h[43] ≈ 0.16302929690891788 + @test h[501] ≈ 0.1655315417523159 + @test h[5808] ≈ 0.005801900639633136 (; q_average, h) = river_flow.floodplain.variables - @test maximum(q_average) ≈ 6.118766353571045 - @test maximum(h) ≈ 1.3772452681705927 + @test maximum(q_average) ≈ 6.126840040417338 + @test maximum(h) ≈ 1.3787910985721359 end @testitem "run wflow sbm" begin From 32a21689d6e731da7ccddc167d252bbca840258b Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Tue, 14 Jul 2026 11:31:36 +0200 Subject: [PATCH 87/90] Remove `hydraulic_radius` check floodplain --- Wflow/src/routing/surface/surface_staggered_scheme.jl | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index f1601bd22..37f316360 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -487,11 +487,6 @@ function update_floodplain_flow!( i1, ), ) - hydraulic_radius_at_edge = ifelse( - floodplain_v.water_depth_at_edge[i] > profile.depth[i1], - hydraulic_radius_at_edge, - 0.0, - ) floodplain_v.q[i] = ifelse( flow_area_at_edge > 1.0e-05, @@ -592,11 +587,6 @@ function update_floodplain_flow!( i1, ), ) - hydraulic_radius_at_edge = ifelse( - floodplain_v.water_depth_at_edge[i] > profile.depth[i1], - hydraulic_radius_at_edge, - 0.0, - ) floodplain_v.q[i] = ifelse( flow_area_at_edge > 1.0e-05, From 92ea747c5e7c70f0deb2589c53edb8359de11e6d Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 15 Jul 2026 14:49:04 +0200 Subject: [PATCH 88/90] Fix `dt` input `update_floodplain_model!` --- Wflow/src/routing/surface/surface_kinwave.jl | 2 +- Wflow/test/run_sbm.jl | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Wflow/src/routing/surface/surface_kinwave.jl b/Wflow/src/routing/surface/surface_kinwave.jl index 7750853b2..0e21e426b 100644 --- a/Wflow/src/routing/surface/surface_kinwave.jl +++ b/Wflow/src/routing/surface/surface_kinwave.jl @@ -640,7 +640,7 @@ function update_river_flow_model!( dt_s = check_timestepsize(dt_s, t, dt) river_channel_floodplain_exchange!(river_flow_model, domain.river.parameters, dt_s) kinwave_river_update!(river_flow_model, domain.river, dt_s) - update_floodplain_model!(river_flow_model, domain.river, dt) + update_floodplain_model!(river_flow_model, domain.river, dt_s) t += dt_s end diff --git a/Wflow/test/run_sbm.jl b/Wflow/test/run_sbm.jl index 996f71c25..3f537beb7 100644 --- a/Wflow/test/run_sbm.jl +++ b/Wflow/test/run_sbm.jl @@ -1204,18 +1204,18 @@ end Wflow.run_timestep!(model) (; q_average, h) = river_flow.variables - @test sum(q_average) ≈ 3720.480709961395 - @test q_average[1622] ≈ 0.0007502671736663475 + @test sum(q_average) ≈ 3664.1750799952056 + @test q_average[1622] ≈ 0.000750262072108146 @test q_average[43] ≈ 11.647181706571397 - @test q_average[501] ≈ 0.18574032636015966 - @test q_average[5808] ≈ 0.0439765670778149 - @test h[1622] ≈ 0.0009790264545664975 + @test q_average[501] ≈ 0.1624521220430912 + @test q_average[5808] ≈ 0.04397694950619904 + @test h[1622] ≈ 0.0009790317470641876 @test h[43] ≈ 0.16302929690891788 - @test h[501] ≈ 0.1655315417523159 - @test h[5808] ≈ 0.005801900639633136 + @test h[501] ≈ 0.1542630086179196 + @test h[5808] ≈ 0.005801865652235309 (; q_average, h) = river_flow.floodplain.variables - @test maximum(q_average) ≈ 6.126840040417338 - @test maximum(h) ≈ 1.3787910985721359 + @test maximum(q_average) ≈ 0.606783057904105 + @test maximum(h) ≈ 1.5799312217287604 end @testitem "run wflow sbm" begin From 3438eef415eb3108a68fd83b5e10b154065e23ec Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 15 Jul 2026 18:54:46 +0200 Subject: [PATCH 89/90] Fix reading `alpha_coefficient` `manning_staggered` --- .../src/routing/surface/surface_staggered_scheme.jl | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 37f316360..4ad182b1e 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -129,8 +129,8 @@ function RiverFlowStaggeredParameters( elseif river_routing == RoutingType.manning_staggered zb_at_edge = compute_value_at_edge(zb, nodes_at_edge, n_edges, first) mannings_n_sq_at_edge = [] - slope_at_edge = - compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) + slope_at_edge = domain.parameters.slope + #compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) end parameters = RiverFlowStaggeredParameters(; @@ -245,7 +245,14 @@ function init_staggered_river_flow( # The following boundary conditions can be set at ghost nodes, downstream of river # outlets (pits): river length and river depth - alpha_coefficient = config.model.river_local_inertial_flow__alpha_coefficient # stability coefficient for model time step (0.2-0.7) + + # stability coefficient for model time step (0.2-0.7) + (; river_routing) = config.model + alpha_coefficient = if river_routing == RoutingType.local_inertial + config.model.river_local_inertial_flow__alpha_coefficient + elseif river_routing == RoutingType.manning_staggered + config.model.river_staggered_manning_flow__alpha_coefficient + end timestepping = TimeStepping(; alpha_coefficient) parameters = RiverFlowStaggeredParameters(dataset, config, domain) From e4f94f7d2fe3d4028fe57509cafabb2d2a99841c Mon Sep 17 00:00:00 2001 From: Willem van Verseveld Date: Wed, 15 Jul 2026 20:23:19 +0200 Subject: [PATCH 90/90] Revert small change used for testing --- Wflow/src/routing/surface/surface_staggered_scheme.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Wflow/src/routing/surface/surface_staggered_scheme.jl b/Wflow/src/routing/surface/surface_staggered_scheme.jl index 4ad182b1e..a9fbd325d 100644 --- a/Wflow/src/routing/surface/surface_staggered_scheme.jl +++ b/Wflow/src/routing/surface/surface_staggered_scheme.jl @@ -129,8 +129,8 @@ function RiverFlowStaggeredParameters( elseif river_routing == RoutingType.manning_staggered zb_at_edge = compute_value_at_edge(zb, nodes_at_edge, n_edges, first) mannings_n_sq_at_edge = [] - slope_at_edge = domain.parameters.slope - #compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) + slope_at_edge = + compute_slope_at_edge(zb, flow_length_at_edge, nodes_at_edge, n_edges) end parameters = RiverFlowStaggeredParameters(;