Flow Routing#
Flow routing consists of finding the paths along which some material (e.g., water, sediment) “flows” over the topographic surface, from upslope to downslope areas. It is an important component of most Landscape Evolution Models (LEMs) ; some of the main driver processes like river channel erosion depend on it.
Flow routing is also a complex problem that requires advanced algorithms and (graph-based) data structures in order to compute the flow paths more-or-less accurately, which often represents the main bottleneck in landscape evolution simulations.
Fastscapelib implements a collection of efficient, state-of-the-art algorithms for flow routing (e.g., [BW13], [BLM14], [CBB19]). Those algorithms are optimized for LEMs (i.e., require updating the flow paths repeatedly) and are accessible through a flexible and user-friendly API.
Flow Graph#
Flow paths are contained in a “flow graph” data structure (more precisely, a Directed Acyclic Graph or DAG) that connects each grid node to its flow receiver(s) (neighbors).
The flow graph data structure is distinct although closely related to a grid (Figure 1):
each graph node correspond to a grid node
graph edges together form a subset of the grid (implicit or explicit) edges
A flow_graph
(C++) or
FlowGraph
(Python) object can be created from any grid
object (see Section Grids), e.g., in the example below from a
raster grid:
1#include "fastscapelib/flow/flow_graph.hpp"
2#include "fastscapelib/flow/flow_router.hpp"
3#include "fastscapelib/grid/raster_grid.hpp"
4
5namespace fs = fastscapelib;
6
7fs::raster_boundary_status boundaries{ fs::node_status::fixed_value };
8fs::raster_grid grid({ 100, 100 }, { 200.0, 200.0 }, boundaries);
9
10fs::flow_graph<fs::raster_grid> graph(grid, { fs::single_flow_router() });
1import fastscapelib as fs
2
3grid = fs.RasterGrid([100, 100], [200.0, 200.0], fs.NodeStatus.FIXED_VALUE)
4
5graph = fs.FlowGraph(grid, [fs.SingleFlowRouter()])
A sequence of flow operators must be passed as second argument to the flow graph constructor in order to define the flow routing strategy (see Sections Flow Operators and Flow Routing Strategies).
Base Level Nodes#
The flow routing algorithms implemented in Fastscapelib require one or more nodes of the graph to be set as “base level” (or outlet) nodes (Figure 1).
These specific nodes may collect input flow but cannot release any output flow (it could also mean that the flow is leaving the modeled domain or sub-domain, e.g., surface land water entering the sea).
When creating a new flow graph, all grid nodes with status
FIXED_VALUE
are set as base
level nodes by default.
It is possible to set alternative base level nodes via the
set_base_levels()
method (C++) or the
base_levels
property (Python). This can be
done each time prior to updating the flow paths in the cases where the base levels are evolving over
time (e.g., inner lake, sea or ocean dynamics, planet craters).
Mask#
A binary mask can be set via the set_mask()
method (C++) or the mask
property (Python) to
exclude grid nodes from the computation of the flow graph. By default, all grid
nodes are included in the flow graph (no mask).
Setting a mask is useful if the modeled domain encompass multiple sub-domains such as land vs. ocean. Those sub-domains may each have their own flow graph with different flow routing strategies, base level nodes and masks.
Like for the base level nodes, a flow graph’s mask can be set or updated each time prior to updating the flow paths, e.g., according to sea level change over time.
Initializing / Updating the Flow Paths#
Creating a new flow graph object doesn’t compute any flow path yet. To
(re)compute the flow paths, the update_routes()
method must be called with an input topographic surface (i.e., an elevation
field defined on the grid):
1#include "xtensor/xarray.hpp"
2#include "xtensor/xrandom.hpp"
3#include "fastscapelib/flow/flow_graph.hpp"
4#include "fastscapelib/flow/flow_router.hpp"
5#include "fastscapelib/grid/raster_grid.hpp"
6
7namespace fs = fastscapelib;
8
9fs::raster_boundary_status boundaries{ fs::node_status::fixed_value };
10fs::raster_grid grid({ 100, 100 }, { 200.0, 200.0 }, boundaries);
11
12fs::flow_graph<fs::raster_grid> graph(grid, { fs::single_flow_router() });
13
14xt::xarray<double> elevation = xt::random::rand<double>(grid.shape());
15
16// update_routes returns a const reference!
17const auto new_elevation = graph.update_routes(elevation);
1import numpy as np
2import fastscapelib as fs
3
4grid = fs.RasterGrid([100, 100], [200.0, 200.0], fs.NodeStatus.FIXED_VALUE)
5
6graph = fs.FlowGraph(grid, [fs.SingleFlowRouter()])
7
8elevation = np.random.uniform(size=grid.shape)
9
10new_elevation = graph.update_routes(elevation)
update_routes()
returns another elevation
field, which may differ from the input elevation field depending on the applied
flow operators (e.g., some operators fill the closed depressions found in the
input topography).
Note
update_routes()
never updates in-place the
values of the input elevation field.
Flow Operators#
Flow can be routed over the topographic surface in many different ways ; choosing one approach over another highly depends on the case studied. Fastscapelib relies on the concept of “flow operators” that provide a flexible and convenient solution for implementing simple to advanced flow routing strategies.
A flow operator is a “routing unit” that:
may read and/or modify in-place the flow graph instance to which it has been attached
may read and/or update (a copy of) the elevation values passed to the
update_routes()
methodmay expose its own parameters
There are currently three categories of operators (see the C++ and Python API reference for a full list of available operators).
Flow Routers#
Flow router operators generally compute new flow paths from scratch and fully
update the flow graph. The updated flow graph has a defined
FlowDirection
: either SINGLE
with one unique
receiver per node or MULTI
where the flow is partitioned among multiple
receiver nodes with fixed or variable weights (Figure 1).
1#include "fastscapelib/flow/flow_router.hpp"
2
3namespace fs = fastscapelib;
4
5fs::single_flow_router::graph_updated // true
6fs::single_flow_router::out_flowdir // fs::flow_direction::single
7
8fs::multi_flow_router::graph_updated // true
9fs::multi_flow_router::out_flowdir // fs::flow_direction::multi
1import fastscapelib as fs
2
3fs.SingleFlowRouter.graph_updated # True
4fs.SingleFlowRouter.out_flowdir # fs.FlowDirection.SINGLE
5
6fs.MultiFlowRouter.graph_updated # True
7fs.MultiFlowRouter.out_flowdir # fs.FlowDirection.MULTI
Sink Resolvers#
Sink resolver operators may either update the flow graph or the topographic surface (or both) so that no flow is trapped in closed depressions (i.e., every flow path is ensured to reach one of the base level nodes).
Some operators like PFloodSinkResolver
only update the
topographic surface and do not require any pre-existing flow paths, while other
operators like MSTSinkResolver
require single flow
paths that will be updated in-place.
1#include "fastscapelib/flow/sink_resolver.hpp"
2
3namespace fs = fastscapelib;
4
5fs::pflood_sink_resolver::graph_updated // false
6fs::pflood_sink_resolver::elevation_updated // true
7fs::pflood_sink_resolver::in_flowdir // fs::flow_direction::undefined
8fs::pflood_sink_resolver::out_flowdir // fs::flow_direction::undefined
9
10fs::mst_sink_resolver::graph_updated // true
11fs::mst_sink_resolver::elevation_updated // true
12fs::mst_sink_resolver::in_flowdir // fs::flow_direction::single
13fs::mst_sink_resolver::out_flowdir. // fs::flow_direction::single
1import fastscapelib as fs
2
3fs.PFloodSinkResolver.graph_updated # False
4fs.PFloodSinkResolver.elevation_updated # True
5fs.PFloodSinkResolver.in_flowdir # fs.FlowDirection.UNDEFINED
6fs.PFloodSinkResolver.out_flowdir # fs.FlowDirection.UNDEFINED
7
8fs.MSTSinkResolver.graph_updated # True
9fs.MSTSinkResolver.elevation_updated # True
10fs.MSTSinkResolver.in_flowdir # fs.FlowDirection.SINGLE
11fs.MSTSinkResolver.out_flowdir # fs.FlowDirection.SINGLE
Flow Snapshots#
FlowSnapshot
is a special operator that can be used to
read and save intermediate states of the flow graph and/or topographic elevation
between two other operators.
1#include "fastscapelib/flow/flow_router.hpp"
2
3namespace fs = fastscapelib;
4
5fs::flow_snapshot::graph_updated // false
6fs::flow_snapshot::elevation_updated // false
1import fastscapelib as fs
2
3fs.FlowSnapshot.graph_updated # False
4fs.FlowSnapshot.elevation_updated # False
Flow Routing Strategies#
A flow routing strategy is defined by an ordered sequence of flow operators
passed to the FlowGraph
constructor. Calling
update_routes()
will execute all the
operators one after each other.
Here below are a few examples from simple to more advanced strategies (grid setup is skipped for more clarity, see the flow graph full example above).
Single Direction Flow Routing#
The simplest example is to compute the flow paths along the steepest descent. It
only requires the SingleFlowRouter
operator.
fs::flow_graph<fs::raster_grid> single_graph(grid, { fs::single_flow_router() });
single_graph = fs.FlowGraph(grid, [fs.SingleFlowRouter()])
Applied on a raster grid with 8-node connectivity, this is equivalent to the so-called “D8” algorithm [OCallaghanM84].
Flow Routing Across Closed Depressions#
The priority flood sink resolver PFloodSinkResolver
[BLM14] can be used to fill the closed depressions in the
topography before computing the flow paths.
#include "fastscapelib/flow/sink_resolver.hpp"
fs::flow_graph<fs::raster_grid> single_graph_nosink(
grid, { fs::pflood_sink_resolver(), fs::single_flow_router() });
single_graph_nosink = fs.FlowGraph(
grid, [fs.PFloodSinkResolver(), fs.SingleFlowRouter()]
)
Flow Routing Across Closed Depression 2#
Alternatively, MSTSinkResolver
[CBB19] can be used after computing the flow paths to re-route
the flow trapped in closed depressions.
#include "fastscapelib/flow/sink_resolver.hpp"
fs::flow_graph<fs::raster_grid> single_graph_nosink2(
grid, { fs::single_flow_router(), fs::mst_sink_resolver() });
single_graph_nosink2 = fs.FlowGraph(
grid, [fs.SingleFlowRouter(), fs.MSTSinkResolver()]
)
Compared to PFloodSinkResolver
,
MSTSinkResolver
can be much faster to execute when the
number of closed depressions found in the topography is small (often the case in
LEM simulations already after a few time steps if no process is creating new
closed depressions). However, MSTSinkResolver
requires
pre-computed single flow paths while
PFloodSinkResolver
has no specific requirement.
Multiple Direction Flow Routing (with Snapshots)#
Here is a more advanced example where depression-free multiple direction flow paths are obtained after applying several operators. Two graph snapshots are also saved: the single direction flow paths before and after resolving closed depressions, respectively.
#include <memory>
#include "fastscapelib/flow/flow_snapshot.hpp"
auto mrouter_ptr = std::make_shared<fs::multi_flow_router>(1.0);
fs::flow_graph<fs::raster_grid> multi_graph(
grid,
{ fs::single_flow_router(),
fs::flow_snapshot("single")
fs::mst_sink_resolver(),
fs::flow_snapshot("single_nosink"),
mrouter_ptr });
mrouter = fs.MultiFlowRouter(1.0)
multi_graph = fs.FlowGraph(
grid,
[
fs.SingleFlowRouter(),
fs.FlowSnapshot("single"),
fs.MSTSinkResolver(),
fs.FlowSnapshot("single_nosink"),
mrouter,
],
)
Note that flow operators may be instantiated outside of the flow graph
constructor, like MultiFlowRouter
in the example
above. This is useful for changing parameter values between two successive calls
to update_routes()
, e.g.,
multi_graph.update_routes(elevation);
mrouter->m_slope_exp = 1.5;
multi_graph.update_routes(elevation);
multi_graph.update_routes(elevation)
mrouter.slope_exp = 1.5
multi_graph.update_routes(elevation)
The flow partitioning method used in MultiFlowRouter
is equivalent to Quinn et al. [QBCP91] method (slope_exp = 1
) or
Holmgren [Hol94] method.
Graph snapshots are separate instances of FlowGraph
accessible via the graph_snapshot()
method of
the main graph instance. They are read-only.
auto snapshot = multi_graph.graph_snapshot("single");
snapshot.update_routes(elevation) // Error (read-only)!
snapshot = multi_graph.graph_snapshot("single")
snapshot.update_routes(elevation) # Error (read-only)!
Flow Accumulation#
After computing the flow paths, a
FlowGraph
object is ready to accumulate some locally
produced quantity or flux along the network via the
accumulate()
method. This is handy for
computing a range of internal variables, model outputs and/or diagnostics during
a simulation. Flow accumulation requires a source term, which may be spatially
uniform or variable.
Note
The input source term passed to accumulate()
must expressed in units per-area. It will be integrated uniformly over the area
surrounded by each grid node given by nodes_areas()
.
A few examples:
drainage area (or upslope contributing area)
auto drainage_area = graph.accumulate(1.0);
drainage_area = graph.accumulate(1.0)
Where the source term is equal to 1 (unit-less) so that drainage_area
has
dimensions \([L^2]\) and only results from the accumulation of the node
(cell) areas along the flow paths.
water discharge computed from local surface runoff rate
xt::xarray<double> runoff_rate = xt::random::rand<double>(grid.shape());
auto discharge = graph.accumulate(runoff_rate);
runoff_rate = np.random.uniform(size=grid.shape)
discharge = graph.accumulate(runoff_rate)
Where runoff_rate
has dimensions \([L/T]\) and discharge
has
dimensions \([L^3/T]\).
sediment volume computed from vertical (local) erosion
double dt = 1e3;
xt::xarray<double> erosion = channel_eroder.erode(elevation, drainage_area, dt);
auto sediment_vol = graph.accumulate(erosion);
dt = 1e3
erosion = channel_eroder.erode(elevation, drainage_area, dt)
sediment_vol = graph.accumulate(erosion)
Where erosion
has dimensions \([L]\) and sediment_vol
has
dimensions \([L^3]\).
Advanced Usage#
For advanced use cases it is possible to have read-only access to the graph
internal data via the impl()
method, which
returns the FlowGraphImpl
instance attached to the
flow graph.
Warning
Relying on graph internal data for implementing custom logic in 3rd-party code
should be done only when there is no alternative. Always prefer
accumulate()
when possible. Feedback and/or
feature requests are also
welcome.
FlowGraphImpl
is an implementation detail and
shouldn’t be taken as stable API. In C++ this class is under the
fastscapelib::detail
namespace.
Graph internal data should never be updated directly, or at your own risk! Only flow operators may do this internally. If direct update is possible this is probably a bug.