Grids#

Fastscapelib provides convenient classes for dealing with different kinds of grids via some unified API, on top of which various algorithms (flow routing, differential equation solvers, etc.) may be implemented in a grid-agnostic fashion.

Types of Grids#

The table below describes the type of grids that are currently available in Fastscapelib (also illustrated in Figure 1).

Grid types#

C++ / Python Class

Properties

Usage Examples

profile_grid_xt ProfileGrid

1-dimensional, uniform, static

Evolution of a river longitudinal profile, hillslope or escarpment cross-section.

Solving flow routing or differential equations in this special case is often trivial, but this grid is still is useful for quickly switching between the 1D and 2D cases without the need to re-implement anything.

raster_grid_xt RasterGrid

2-dimensional, rectangular, uniform, static

Evolution of an hillslope, escarpment, drainage basin, orogenic belt, etc.

trimesh_xt TriMesh

2-dimensional, triangular (irregular) mesh, static

More complex than uniform grids but supports variable resolution (mesh refinement) and domain limits with complex shapes or planetary domains (no boundary).

Single direction flow routing results in more “natural” river networks [BS97] than applied on raster grids (D8), although it is still constrained by the grid geometry.

Note: this class doesn’t implement any triangulation (triangles must be given to the grid constructor).

Grid types and representation

Figure 1: Grid types (left: profile, center: raster, right: triangular mesh) and representation (points: nodes, dashed lines: implicit or explicit connectivity).#

Grid Representation#

Fastscapelib currently implements a simple representation for all of its grids, which only includes the grid nodes (points) and their connectivity (Figure 1).

Grid method names follow these conventions:

  • nodes_xxx(): get node-specific grid information either for all nodes, a subset of the nodes or one specific node depending on the (overloaded) method.

  • neighbors_xxx(): get grid information about the neighbors of a given grid node.

Although grid faces or cells are not represented explicitly (i.e., cell geometry is not stored as grid data), some of their properties are exposed through the grid API using the conventions above (e.g., nodes_areas() returns the area of the direct vicinity of each node).

Grid connectivity may be either implicit (e.g., in a structured_grid node neighbors are implied by the grid structure) or explicit (e.g., the indices of triangle vertices must be given to trimesh). The neighbors() method and other related methods make abstraction of the grid connectivity, which may be viewed as an implementation detail.

See Sections Grid Node Iterators and Connectivity and Node Neighbors for more details on how to iterate through grid nodes and their neighbors.

Node Status and Boundary Conditions#

Each node of the grid has a given status (or label), which usually serves to determine how the model should behave at the domain limits, located on the grid edges and/or inside the grid. All possible labels are defined in the node_status (C++) and NodeStatus (Python) enum classes.

All grids expose a nodes_status parameter in their constructor, which allows setting specific statuses for one or more nodes anywhere on the grid (some restrictions may apply for the LOOPED status). Each grid also exposes a nodes_status read-only getter or property that returns the status of all grid nodes as an array.

Uniform grids ProfileGrid and RasterGrid also provide convenient ways to set the status of the edge or border nodes in their constructors, respectively via ProfileBoundaryStatus and RasterBoundaryStatus (which may be constructed implicitly).

Note

The status at nodes is defined once when creating a grid and cannot be changed afterwards.

Warning

The usage or interpretation of grid node status and boundary conditions may differ depending on the case. For example:

  • FlowGraph sets as base levels all nodes having the FIXED_VALUE status by default.

  • DiffusionADIEroder ignores the grid node status and always assumes fixed value boundaries on the raster grid borders.

Below are a few examples of creating new grids with different statuses inside the grid or on the grid boundaries. See also the Examples.

  1. A profile grid of 501 nodes with a total length equal to 500 m and with fixed-value boundaries, e.g., for simulating the evolution of a hillslope cross-section with base levels (river channel) at both sides.

1#include "fastscapelib/grid/profile_grid.hpp"
2
3namespace fs = fastscapelib;
4
5fs::profile_boundary_status bs(fs::node_status::fixed_value);
6auto grid = fs::profile_grid::from_length(501, 500.0, bs);
1import fastscapelib as fs
2
3grid = fs.ProfileGrid.from_length(501, 500.0, NodeStatus.FIXED_VALUE)
  1. A profile grid of 501 nodes with uniform node spacing of 1 meter, with CORE node status at both left and right edges and a fixed-value node at the middle of the grid, e.g., for simulating a valley cross-section:

1#include "fastscapelib/grid/profile_grid.hpp"
2
3namespace fs = fastscapelib;
4
5fs::profile_boundary_status bs(fs::node_status::core);
6auto grid = fs::profile_grid(501, 1.0, bs, { {250, fs::node_status::fixed_value} });
1import fastscapelib as fs
2
3grid = fs.ProfileGrid(501, 1.0, fs.NodeStatus.CORE, {250: fs.NodeStatus.FIXED_VALUE})
  1. A raster grid of 101x201 (row x col) nodes with a uniform node spacing of 100 meters, with looped boundaries on the top-down borders, fixed-value on the left border and free (core) on the right border, e.g., for simulating an escarpment:

 1#include "fastscapelib/grid/raster_grid.hpp"
 2
 3namespace fs = fastscapelib;
 4
 5fs::raster_boundary_status bs{ fs::node_status::fixed_value,
 6                               fs::node_status::core,
 7                               fs::node_status::looped,
 8                               fs::node_status::looped };
 9
10auto grid = fs::raster_grid({ 101, 201 }, { 1e2, 1e2 }, bs);
 1import fastscapelib as fs
 2
 3bs = [
 4    fs.NodeStatus.FIXED_VALUE,
 5    fs.NodeStatus.CORE,
 6    fs.NodeStatus.LOOPED,
 7    fs.NodeStatus.LOOPED,
 8]
 9
10grid = fs.RasterGrid([101, 201], [1e2, 1e2], bs)

Tip

How to choose the shape (number of nodes) of a uniform grid?

A round number + 1 like in the examples above results in a round value for the uniform node spacing when the grid is constructed from a given, round total length.

Alternatively, you might want to choose a power of two (e.g., 256, 512, 1024, etc.) for optimal memory alignment of the grid data and internal arrays.

Grid Data Variables#

Fastscapelib grid objects do not hold any grid data variable (fields), neither by ownership nor by external reference (or pointer). This leaves users the freedom of managing those data variables in a way that best suits their needs.

Grid data variables should be defined as Xtensor-compatible containers in C++ (e.g., xt::xtensor or xt::xarray objects) or NumPy arrays in Python.

The grid size() and shape() methods are helpful for validating the shape of the array containers at runtime.

Additionally, the grid_inner_types C++ class specialized for each grid exposes some properties like grid_data_type, xt_selector and xt_ndims (for convenience, those are also exposed as static data members of the grid classes). This is particularly useful for maximizing code reusability in C++ (via type aliases) when creating new grid data variables.

Note

The dimension(s) of the grid data arrays (i.e., the length of shape() and the value of xt_dims) do not always match the number of physical axes of the grid. For example, variables on a 2-dimensional triangular mesh are stored in 1-dimensional arrays.

Below is an example of setting random initial topographic elevation values on a raster grid.

 1#include "xtensor/xarray.hpp"
 2#include "xtensor/xrandom.hpp"
 3#include "fastscapelib/grid/raster_grid.hpp"
 4
 5namespace fs = fastscapelib;
 6
 7fs::raster_boundary_status bs{ fs::node_status::fixed_value };
 8fs::raster_grid grid({ 101, 101 }, { 200.0, 200.0 }, bs);
 9
10//
11// setting a xt::xarray with the double data type
12//
13xt::xarray<double> elevation = xt::random::rand<double>(grid.shape());
14
15//
16// setting a xt::xtensor with dimensions and data type from grid inner types
17//
18using dtype = fs::raster_grid::grid_data_type;
19using ndims = fs::grid_inner_types<fs::raster_grid>::xt_ndims;
20xt::xtensor<dtype, ndims> elevation_alt = xt::random::rand<dtype>(grid.shape());
21
22//
23// use the xtensor container selector set for the grid
24//
25using selector = fs::raster_grid::xt_selector;
26using ctype = fs::xt_container<selector, dtype, ndims>::tensor_type;
27ctype elevation_alt2 = xt::random::rand<dtype>(grid.shape());
1import numpy as np
2import fastscapelib as fs
3
4grid = fs.RasterGrid([101, 101], [200.0, 200.0], fs.NodeStatus.FIXED_VALUE)
5
6elevation = np.random.uniform(size=grid.shape)

Grid Node Iterators#

Fastscapelib provides a convenient, stl-compatible way to iterate over the grid nodes in C++, possibly filtered by node status, using nodes_indices().

Note

Iterating over grid nodes using pure-Python loops is very slow. In general it is possible to obtain the same results much faster using operations on NumPy arrays (vectorization). If that is not possible, great speed-ups can still be obtained, e.g., by using Numba’s jit compiler (non-object mode).

 1#include "fastscapelib/grid/raster_grid.hpp"
 2
 3namespace fs = fastscapelib;
 4
 5using fixed_value = fs::node_status::fixed_value;
 6fs::raster_boundary_status bs(fixed_value);
 7fs::raster_grid grid({ 101, 101 }, { 200.0, 200.0 }, bs);
 8
 9//
10// iterate over all grid nodes
11//
12for (auto& idx_flat : grid.nodes_indices())
13{
14    // on a raster grid, flat index corresponds to the
15    // current row index * total nb. of rows + current column index
16    std::cout << "current node flat index: " << idx_flat << std::endl;
17}
18
19//
20// iterate over fixed-value nodes (all border nodes in this case)
21//
22for (auto& idx_flat : grid.nodes_indices(fixed_value))
23{
24    std::cout << "current node flat index: " << idx_flat << std::endl;
25}
 1import fastscapelib as fs
 2import numpy as np
 3
 4fixed_value = fs.NodeStatus.FIXED_VALUE
 5grid = fs.RasterGrid([100, 100], [200.0, 200.0], fixed_value)
 6
 7# iterate over all grid nodes (slow!)
 8for idx_flat in grid.nodes_indices():
 9    print(f"current node flat index: {idx_flat}")
10
11# iterate over fixed-value nodes (slow!)
12for idx_flat in grid.nodes_indices(fixed_value):
13    print(f"current node flat index: {idx_flat}")
14
15# do vectorized operations when possible (fast!)
16data = np.random.rand(size=grid.shape)
17data.ravel()[grid.node_indices(fixed_value)] = 0.0

Connectivity and Node Neighbors#

Iterating over the neighbors of a grid node is simple, does not depend on the type of grid and follows looped boundaries if any.

A typical way to iterate over grid nodes and their neighbors is illustrated in the example below.

Note

Iterating over grid nodes and their neighbors in Python is slow. Grid neighbors methods are still exposed in Python for development and debugging purposes.

Note that it is not possible to use Fastscapelib grid (Python) objects directly in Numba jitted functions in non-object mode.

 1#include <iostream>
 2#include "fastscapelib/grid/raster_grid.hpp"
 3
 4namespace fs = fastscapelib;
 5
 6fs::raster_boundary_status bs(fs::node_status::fixed_value);
 7fs::raster_grid grid({ 101, 101 }, { 200.0, 200.0 }, bs);
 8
 9//
10// optimization: initialize the neighbors container out of the loops
11//
12fs::raster_grid::neighbors_type neighbors;
13
14for (auto& idx_flat : grid.nodes_indices())
15{
16    for (auto& nb : grid.neighbors(idx_flat, neighbors))
17    {
18        std::cout << "flat index: " << nb.idx << std::endl;
19        std::cout << "distance to neighbor: " << nb.distance << std::endl;
20        std::cout << "status of neighbor: " << nb.status << std::endl;
21    }
22}
 1import fastscapelib as fs
 2
 3grid = fs.RasterGrid([100, 100], [200.0, 200.0], fs.NodeStatus.FIXED_VALUE)
 4
 5for idx_flat in range(grid.size):
 6    neighbors = grid.neighbors(idx_flat)
 7    for nb in neighbors:
 8        print(f"flat index: {nb.idx}")
 9        print(f"distance to neighbor: {nb.distance}")
10        print(f"status of neighbor: {nb.status}")

The example above is using the simple neighbor (C++) and Neighbor (Python) structures to store information about a node neighbor. If you just need the total number of neighbors for a given grid node or only the neighbor (flat) indices or the distances from one node to its neighbors, you can use the alternative methods neighbors_count(), neighbors_indices() and neighbors_distances().

raster_grid_xt (C++) also provides some method overloads to use row and column indices instead of grid flat indices.

Raster Grid Connectivity#

raster_grid_xt (C++) exposes a template parameter that allows choosing the grid connectivity among three modes (see raster_connect and Figure 2):

  • queen (8-node connectivity, including diagonals, set by default)

  • rook (4-node connectivity, no diagonal)

  • bishop (4-nodes connectivity, diagonals only)

Note

RasterGrid (Python) only supports the queen mode.

Raster connectivity modes

Figure 2: Raster grid connectivity modes: queen (left), rook (center) and bishop (right).#

Caching Neighbor Node Indices#

Fastscapelib implements a cache system for retrieving neighbor node indices, which is particularly useful for speeding-up neighbor lookup on structured (raster) grids. Such a cache helps avoiding repetitive operations like checking if the current visited node is on the boundary or finding neighbors on looped boundaries. Caveat: this speed-up is achieved at the expense of memory consumption.

The cache is exposed as a template parameter for raster_grid_xt so that this behavior may be customized (cache may be disabled). For other grid types like trimesh_xt with explicit topology, the cache is not needed and not used.

See neighbors_cache and neighbors_no_cache for more details.

Note

In the Python bindings the cache is enabled for raster grids and there is currently no option to disable it.