Introduction to Landlab’s Gridding Library¶
When creating a two-dimensional simulation model, often the most time-consuming and error-prone task involves writing the code to set up the underlying grid. Irregular (or “unstructured”) grids are especially tricky to implement. Landlab’s ModelGrid package makes this process much easier, by providing a set of library routines for creating and managing a 2D grid, attaching data to the grid, performing common input and output operations, and providing library functions that handle common numerical operations such as calculating a field of gradients for a particular state variable. By taking care of much of the overhead involved in writing grid-management code, ModelGrid is designed to help you build 2D models quickly and efficiently, freeing you to concentrate on the science behind the code.
Some of the things you can do with ModelGrid include:
Create and configure a structured or unstructured grid in a one or a few lines of code
Create various data arrays attached to the grid
Easily implement staggered-grid finite-difference/finite-volume schemes
Calculate gradients in state variables in a single line
Calculate net fluxes in/out of grid cells in a single line
Set up and run “link-based” cellular automaton models
Switch between structured and unstructured grids without needing to change the rest of the code
Develop complete, 2D numerical finite-volume or finite-difference models much more quickly and efficiently than would be possible using straight C, Fortran, Matlab, or Python code
Some of the Landlab capabilities that work with ModelGrid to enable easy numerical modeling include:
Easily read in model parameters from a formatted text file
Write grid and data output to netCDF files for import into open-source visualization packages such as ParaView and VisIt
Create grids from ArcGIS-formatted ascii files
Create models by coupling together your own and/or pre-built process components
Use models built by others from process components
This document provides a basic introduction to building applications using Landlab’s ModelGrid. It covers: (1) how grids are represented, and (2) a set of tutorial examples that illustrate how to build models using simple scripts.
How a Grid is Represented¶
Basic Grid Elements¶
Figure 1 illustrates how ModelGrid represents a simulation grid. The grid contains a set of (x,y) points called nodes. In a typical finite-difference or finite-volume model, nodes are the locations at which one tracks scalar state variables, such as water depth, land elevation, sea-surface elevation, or temperature.
Each adjacent pair of nodes is connected by a line segment known as a link. A link has both a position in space, denoted by the coordinates of the two bounding nodes, and a direction: a link runs from one node (known as its from-node or tail-node) to another node (known as its to-node or head-node).
Every node in the grid interior is associated with a polygon known as a cell (illustrated, for example, by the shaded square region in Figure 1a). Each cell is bounded by a set of line segments known as faces, which it shares with its neighboring cells.
In the simple case of a regular (raster) grid, the cells are square, the nodes are the center points of the cells (Figure 1), and the links and faces have identical length (equal to the node spacing). In a Voronoi-Delaunay grid, the cells are Voronoi polygons (also known as Theissen polygons) (Figure 1a). In this case, each cell represents the surface area that is closer to its own node than to any other node in the grid. The faces represent locations that are equidistant between two neighboring nodes. Other grid configurations are possible as well. The spring 2015 version of Landlab includes support for hexagonal and radial grids, which are specialized versions of the Voronoi-Delaunay grid shown in Figure 1a. Note that the node-link-cell-face topology is general enough to represent other types of grid; for example, one could use ModelGrid’s data structures to implement a quad-tree grid, or a Delaunay-Voronoi grid in which cells are triangular elements with nodes at their circumcenters.
Creating a Grid¶
Creating a grid is easy. The first step is to import Landlab’s
RasterModelGrid
class (this
assumes you have installed Landlab)
>>> from landlab import RasterModelGrid
Now, create a regular (raster) grid with 10 rows and 40 columns, with a node spacing (dx) of 5:
>>> mg = RasterModelGrid((10, 40), 5.0)
mg is now a grid object with 400 ( 10*40 )
nodes and 750 ( 40*(10-1) + 10*(40-1) )
links.
>>> mg.number_of_node_columns
40
>>> mg.number_of_nodes
400
>>> mg.number_of_links
750
Adding Data to a Landlab Grid Element using Fields¶
Landlab has a data structure called fields that will store data associated with different types of grid elements. Fields are convenient because 1) fields create data arrays of the proper length for the associated grid element, and 2) fields attach these data to the grid, so that any piece of code that has access to the grid also has access to the data stored in fields.
Suppose you would like like to track the elevation at each node. The following code creates a data field (array) called elevation. In this case, we’ll use the grid method add_zeros to create a field that initially sets all values in the field to zero (we’ll explain how to read in elevation values from a file in the section on DEMs below). The add_zeros method takes two arguments: the name of the grid element (in this case, node, in the singular) and a name we choose for the value in the data field (here we’ll just call it elevation). Each elevation value in the data field is then associated with a specific grid node. The data field is just a NumPy array whose length is equal to the number of nodes in the grid.
>>> z = mg.add_zeros("elevation", at="node")
Here z is an array of zeros. We can verify that z has the same length as the number of nodes:
>>> z.size # or len(z)
400
Note that z is a reference to the data stored in the model field. This means that if you change z, you also change the data in the ModelGrid’s elevation field. Therefore, you can access and manipulate data in the field either through the variable z or through the grid, as in the following examples:
>>> mg.at_node["elevation"][5] = 1000.0
or the alternative notation:
>>> mg["node"]["elevation"][5]
1000.
Now the sixth element in the model’s elevation field array, or in z, is equal to 1000. (Remember that the first element of a Python array has an index of 0 (zero)).
You can see all of the field data available at the nodes on mg with the following:
>>> mg.at_node.keys()
['elevation']
You may recognize this as a dictionary-type structure, where the keys are the names (as strings) of the data arrays.
There are currently no data values (fields) assigned to the links, as shown by the following:
>>> mg.at_link.keys()
[]
It is also possible, and indeed, often quite useful, to initialize a field from an
existing NumPy array of data. You can do this with the
add_field
method.
This method allows slightly more granular control over how the field gets
created. In addition to the grid element and field name, this method takes an
array of values to assign to the field. Optional arguments include: units=
to assign a unit of measurement (as a string) to the value, copy=
a boolean
to determine whether to make a copy of the data, and clobber=
a boolean
that prevents accidentally overwriting an existing field.
>>> import numpy as np
>>> elevs_in = np.random.rand(mg.number_of_nodes)
>>> mg.add_field(
... "elevation", elevs_in, at="node", units="m", copy=True, clobber=True
... )
Fields can store data at nodes, cells, links, faces, patches, junctions, and corners (though the latter two or three are very rarely, if ever, used). The grid element you select is described in Landlab jargon as that field’s centering or group, and you will sometimes see these terms used as input parameters to various grid methods.
To access only the core nodes, core cells, active links, or some other subset of node values using the properties available through the ModelGrid, you can specify a subset of the field data array. For example, if we wanted to determine the elevations at core nodes only we can do the following:
>>> core_node_elevs = mg.at_node["elevation"][mg.core_nodes]
The first set of brackets, in this case elevation, indicates the field data array, and the second set of brackets, in this case mg.core_nodes (itself an array of core node IDs), is a NumPy filter that specifies which elevation elements to return.
Here is another example of initializing a field with the add_ones method. Note that when initializing a field, the singular of the grid element type is provided:
>>> veg = mg.add_ones("percent_vegetation", at="cell")
>>> mg.at_cell.keys()
['percent_vegetation']
Here veg is an array of ones that has the same length as the number of cells. Because there are no cells around the edge of a grid, there are fewer cells than nodes:
>>> mg.at_cell["percent_vegetation"].size
304
As you can see, fields are convenient because you don’t have to keep track of how many nodes, links, cells, etc. there are on the grid. It is easy for any part of the code to query what data are already associated with the grid and operate on these data.
You are free to call your fields whatever you want. However, field names are more useful if standardized across components. If you are writing a Landlab component you should use Landlab’s standard names. Standard names for fields in a particular component can be accessed individually through the properties component_instance._input_var_names and component_instance._output_var_names (returned as dictionaries), and are listed in the docstring for each component.
>>> from landlab.components.flexure import Flexure
>>> flexer = Flexure(rg)
>>> flexer._input_var_names
{'lithosphere__elevation',
'lithosphere__overlying_pressure',
'planet_surface_sediment__deposition_increment'}
>>> flexer._output_var_names
{'lithosphere__elevation', 'lithosphere__elevation_increment'}
We also maintain this list of all the Landlab standard names.
Our fields also offer direct compatibility with CSDMS’s standard naming system for variables. However, note that, for ease of use and readability, Landlab standard names are typically much shorter than CSDMS standard names. We anticipate that future Landlab versions will be able to automatically map from Landlab standard names to CSDMS standard names as part of Landlab’s built-in Basic Model Interface for CSDMS compatibility.
The following gives an overview of the commands you can use to interact with the grid fields.
Field initialization¶
grid.add_empty(name, at="group", units="-")
grid.add_ones(name, at="group", units="-")
grid.add_zeros(name, at="group", units="-")
“group” is one of ‘node’, ‘link’, ‘cell’, ‘face’, ‘corner’, ‘junction’, ‘patch’
“name” is a string giving the field name
“units” (optional) is a string denoting the units associated with the field values.
Field creation from existing data¶
grid.add_field(name, value_array, at="group", units="-", copy=False, clobber=True)
Arguments as above, plus:
“value_array” is a correctly sized numpy array of data from which you want to create the field.
“copy” (optional) if True adds a copy of value_array to the field; if False, creates a reference to value_array.
“clobber” (optional) if False
, raises an exception if a field called name already exists.
Field access¶
grid.at_node
orgrid['node']
grid.at_cell
orgrid['cell']
grid.at_link
orgrid['link']
grid.at_face
orgrid['face']
grid.at_corner
orgrid['corner']
grid.at_junction
orgrid['junction']
grid.at_patch
orgrid['patch']
Each of these is then followed by the field name as a string in square brackets, e.g.,
>>> grid.at_node["my_field_name"] # or
>>> grid["node"]["my_field_name"]
You can also use these commands to create fields from existing arrays,
as long as you don’t want to take advantage of the added control add_field()
gives you.
Getting information about fields¶
Landlab offers a command line interface that lets you find out about all the fields that are in use across all the Landlab components. You can find out the following:
$ landlab used_by [ComponentName]
# What fields does ComponentName take as inputs?
$ landlab provided_by [ComponentName]
# What fields does ComponentName give as outputs?
$ landlab uses [field__name]
# What components take the field field__name as an input?
$ landlab provides [field__name]
# What components give the field field__name as an output?
$ landlab list
# list all the components
$ (landlab provided_by && landlab used_by) | sort | uniq
# some command line magic to see all the fields currently used in components
Representing Gradients in a Landlab Grid¶
Finite-difference and finite-volume models usually need to calculate spatial gradients in one or more scalar variables, and often these gradients are evaluated between pairs of adjacent nodes. ModelGrid makes these calculations easier for programmers by providing built-in functions to calculate gradients along links and allowing applications to associate an array of gradient values with their corresponding links or edges. The tutorial examples illustrate how this capability can be used to create models of processes such as diffusion and overland flow.
Here we simply illustrate the method for calculating gradients on the links. Remember that we have already created the elevation array z, which is also accessible from the elevation field on mg.
>>> gradients = mg.calculate_gradients_at_active_links(z)
Now gradients have been calculated at all links that are active, or links on which flow is possible (see boundary conditions below).
Other Grid Elements¶
The cell vertices are called corners (Figure 1, solid squares <basic-grid-elements>
).
Each face is therefore a line segment connecting two corners. The intersection
of a face and a link (or directed edge) is known as a junction
(Figure 1, open diamonds). Often, it is useful to calculate scalar
values (say, ice thickness in a glacier) at nodes, and vector values (say, ice
velocity) at junctions. This approach is sometimes referred to as a
staggered-grid scheme. It lends itself naturally to finite-volume methods, in
which one computes fluxes of mass, momentum, or energy across cell faces, and
maintains conservation of mass within cells. (In the spring 2015 version of Landlab,
there are no supporting functions for the use of junctions, but support is imminent.)
Notice that the links also enclose a set of polygons that are offset from the cells. These secondary polygons are known as patches (Figure 1, dotted). This means that any grid comprises two complementary tesselations: one made of cells, and one made of patches. If one of these is a Voronoi tessellation, the other is a Delaunay triangulation. For this reason, Delaunay triangulations and Voronoi diagrams are said to be dual to one another: for any given Delaunay triangulation, there is a unique corresponding Voronoi diagram. With ModelGrid, one can create a mesh with Voronoi polygons as cells and Delaunay triangles as patches (Figure 1b). Alternatively, with a raster grid, one simply has two sets of square elements that are offset by half the grid spacing (Figure 1a). Whatever the form of the tessellation, ModelGrid keeps track of the geometry and topology of the grid. patches can be useful for processes like calculating the mean gradient at a node, incorporating influence from its neighbors.
Managing Grid Boundaries¶
An important component of any numerical model is the method for handling boundary conditions. In general, it’s up to the application developer to manage boundary conditions for each variable. However, ModelGrid makes this task a bit easier by tagging nodes that are treated as boundaries (boundary nodes) and those that are treated as regular nodes belonging to the interior computational domain (core nodes). It also allows you to de-activate (“close”) portions of the grid perimeter, so that they effectively act as walls.
Let’s look first at how ModelGrid treats its own geometrical boundaries. The outermost elements of a grid are nodes and links (as opposed to corners and faces). For example, Figure 2 shows a sketch of a regular four-row by five-column grid created by RasterModelGrid. The edges of the grid are composed of nodes and links. Only the inner six nodes have cells around them; the remaining 14 nodes form the perimeter of the grid.
All nodes are tagged as either boundary or core. Those on the perimeter of the grid are automatically tagged as boundary nodes. Nodes on the inside are core by default, but it is possible to tag some of them as boundary instead (this would be useful, for example, if you wanted to represent an irregular region, such as a watershed, inside a regular grid). In the example shown in Figure 2, all the interior nodes are core, and all perimeter nodes are open boundary.
Boundary nodes are flagged as either open or closed, and links are tagged as either active or inactive (Figure 3).
A closed boundary is one at which no flux is permitted enter or leave, ever. By definition, all links coming into or out of a closed boundary node must be inactive. There is effectively no value assigned to a closed boundary; it will probably have a grid.BAD_INDEX_VALUE or null value of some kind. An open boundary is one at which flux can enter or leave, but whose value is controlled by some boundary condition rule, updated at the end of each timestep.
An active link is one that joins either two core nodes, or one core and one open boundary node (Figure 3). You can use this distinction in models to implement closed boundaries by performing flow calculations only on active links, as seen in this tutorial.
Boundary condition details and methods¶
A call to mg.node_status returns the codes representing the boundary condition of each node in the grid. There are 5 possible types, they are stored on the model grid:
mg.BC_NODE_IS_CORE (Type 0)
mg.BC_NODE_IS_FIXED_VALUE (Type 1)
mg.BC_NODE_IS_FIXED_GRADIENT (Type 2)
mg.BC_NODE_IS_LOOPED (Type 3, used for looped boundaries)
mg.BC_NODE_IS_CLOSED (Type 4)
A number of different methods are available to you to interact with (i.e., set and update) boundary conditions at nodes. Landlab is smart enough to automatically initialize new grids with fixed value boundary conditions at all perimeters and core nodes for all interior nodes, but if you want something else, you’ll need to modify the boundary conditions.
If you are working with a simple Landlab raster where all interior nodes are core and all perimeter nodes are boundaries, you will find useful the set of commands:
mg.set_closed_boundaries_at_grid_edges(right, top, left, bottom)
mg.set_fixed_value_boundaries_at_grid_edges(right, top, left, bottom)
mg.set_fixed_link_boundaries_at_grid_edges(right, top, left, bottom, link_value=None)
mg.set_looped_boundaries(top_bottom_are_looped, left_right_are_looped)
Where right, top, left, bottom are all booleans. See the relevant docstring for each method for more detailed information.
If you are working with an imported irregularly shaped raster grid, you can close nodes which have some fixed NODATA value in the raster using:
mg.set_nodata_nodes_to_closed(node_data, nodata_value)
Note that all of these commands will treat the status of node links as slave to the status of the nodes, as indicated in Figure 3. Links will be set to active or inactive according to what you set the node boundary conditions as, when you call each method.
If you are working on an irregular grid, or want to do something more complicated
with your raster boundary conditions, you will need to modify the
grid.status_at_node
array by hand, using indexes to node IDs. Simply import the
boundary types from landlab then set the node statuses. The links will be updated
alongside these changes automatically:
>>> grid = RasterModelGrid((5, 5))
>>> grid.set_closed_boundaries_at_grid_edges(False, True, False, True)
>>> grid.number_of_active_links
18
>>> grid.status_at_node[[6, 8]] = mg.BC_NODE_IS_CLOSED
>>> grid.status_at_node.reshape((5, 5))
array([[4, 4, 4, 4, 4],
[1, 4, 0, 4, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[4, 4, 4, 4, 4]], dtype=int8)
>>> grid.number_of_active_links # links were inactivated automatically when we closed nodes
12
Note that while setting Landlab boundary conditions on the grid is straightforward, it is up to the individual developer of each Landlab component to ensure it is compatible with these boundary condition schemes! Almost all existing components work fine with core, closed, and fixed_value conditions, but some may struggle with fixed_gradient, and most will struggle with looped. If you’re working with the component library, take a moment to check your components can understand your implemented boundary conditions! See the Component Developer’s Guide for more information.
Using a Different Grid Type¶
As noted earlier, Landlab provides several different types of grid. Available grids (as of this writing) are listed in the table below. Grids are designed using Python classes, with more specialized grids inheriting properties and behavior from more general types. The class hierarchy is given in the second column, Inherits from.
Grid type |
Inherits from |
Node arrangement |
Cell geometry |
---|---|---|---|
|
|
raster |
squares |
|
|
Delaunay triangles |
Voronoi polygons |
|
|
Delaunay triangles |
Voronoi polygons |
|
|
triagonal |
hexagons |
|
|
concentric |
Voronoi polygons |
|
|
ad libitum |
No cells |
|
|
spherical |
hexagons & pentagons |
landlab.grid.raster.RasterModelGrid
gives a regular (square) grid, initialized
with number_of_node_rows, number_of_node_columns, and a spacing.
In a landlab.grid.voronoi.VoronoiDelaunayGrid
,
a set of node coordinates
is given as an initial condition.
Landlab then forms a Delaunay triangulation, so that the links between nodes are the
edges of the triangles, and the cells are Voronoi polygons.
A landlab.grid.framed_voronoi.FramedVoronoiGrid
is Voronoi grid where nodes coordinates are randomly moved from an initial rectangular regular grid.
A landlab.grid.hex.HexModelGrid
is a
special type of VoronoiDelaunayGrid in which the Voronoi cells happen to be
regular hexagons.
In a landlab.grid.radial.RadialModelGrid
, nodes are created in concentric
circles and then connected to
form a Delaunay triangulation (again with Voronoi polygons as cells).
landlab.grid.network.NetworkModelGrid
represents a branching network of nodes and links,
without cells or patches.
An landlab.grid.icosphere.IcosphereGlobalGrid
represents the surface of a sphere.
The default configuration is a spherical grid of unit radius that
forms the spherical version an icosahedron (20 triangular patches,
12 nodes), with the dual complement representing a dodecahedron
(12 hexagonal cells, 20 corners). The mesh_densification_level
parameter allows you to densify this initial shape by subdividing
each triangular patch into four triangles, with corresponding
addition of nodes (as the triangle vertices), together with
corresponding cells and corners.
Importing a DEM¶
Landlab offers the methods
landlab.io.esri_ascii.read_esri_ascii
and
landlab.io.netcdf.read_netcdf
to allow ingestion of
existing digital elevation models as raster grids.
read_esri_ascii allows import of an ARCmap formatted ascii file (.asc or .txt) as a grid. It returns a tuple, containing the grid and the elevations in Landlab ID order. Use the name keyword to add the elevation to a field in the imported grid.
>>> from landlab.io import read_esri_ascii
>>> (mg, z) = read_esri_ascii("myARCoutput.txt", name="topographic__elevation")
>>> mg.at_node.keys()
['topographic__elevation']
read_netcdf allows import of the open source netCDF format for DEMs. Fields will
automatically be created according to the names of variables found in the file.
Returns a landlab.grid.raster.RasterModelGrid
.
>>> from landlab.io.netcdf import read_netcdf
>>> mg = read_netcdf("mynetcdf.nc")
After import, you can use landlab.grid.base.ModelGrid.set_nodata_nodes_to_closed
to handle the boundary conditions in your imported DEM.
Equivalent methods for output are also available for both esri ascii
(landlab.io.esri_ascii.write_esri_ascii
)
and netCDF
(landlab.io.netcdf.write_netcdf
) formats.
Plotting and Visualization¶
Visualizing a Grid¶
Landlab offers a set of matplotlib-based plotting routines for your data. These exist
in the landlab.plot library. You’ll also need to import some basic plotting functions
from pylab (or matplotlib) to let you control your plotting output: at a minimum show
and figure. The most useful function is called
landlab.plot.imshow.imshow_node_grid
, and is imported
and used as follows:
>>> from landlab.plot.imshow import imshow_node_grid
>>> from pylab import show, figure
>>> mg = RasterModelGrid((50, 50), 1.0) # make a grid to plot
>>> z - mg.node_x * 0.1 # Make an arbitrary sloping surface
>>> mg.add_field(
... "topographic_elevation", z, at="node", units="meters", copy=True
... ) # Create the data as a field
>>> figure("Elevations from the field") # new fig, with a name
>>> imshow_node_grid(mg, "topographic_elevation")
>>> figure(
... "You can also use values directly, not fields"
... ) # ...but if you, do you'll lose the units, figure naming capabilities, etc
>>> imshow_node_grid(mg, z)
>>> show()
Note that landlab.plot.imshow.imshow_node_grid
is clever enough to examine the grid object you pass it,
work out whether the grid is irregular or regular, and plot the data appropriately.
By default, Landlab uses a Python colormap called ‘pink’. This was a deliberate choice to improve Landlab’s user-friendliness to the colorblind in the science community. Nonetheless, you can easily override this color scheme using the keyword cmap as an argument to imshow_node_grid. Other useful built in colorschemes are ‘bone’ (black to white), ‘jet’, (blue to red, through green), ‘Blues’ (white to blue), and ‘terrain’ (blue-green-brown-white) (note these names are case sensitive). See the matplotlib reference guide for more options. Note that imshow_node_grid takes many of the same keyword arguments as, and is designed to resemble, the standard matplotlib function imshow. See also the method help for more details. In particular, note you can set the maximum and minimum you want for your colorbar using the keywords vmin and vmax, much as in similar functions in the matplotlib library.
Note if using Anaconda: there have been documented issues with resolution with default inline plotting within the Spyder IDE iPython console. To generate dynamic plots (e.g. Matlab-like plots), change the graphics settings in Spyder by following this work flow:
In Spyder -> Preferences -> iPython console -> Graphics -> Graphics Backend -> Automatic -> Apply -> OK -> Make sure to restart Spyder to update the preferences.
Visualizing transects through your data¶
If you are working with a regular grid, it is trivial to plot horizontal and vertical
sections through your data. The grid provides the method
landlab.grid.raster.RasterModelGrid.node_vector_to_raster
,
which will turn a Landlab 1D node data array into a two dimensional rows*columns NumPy array,
which you can then take slices of, e.g., we can do this:
>>> from pylab import plot, show
>>> mg = RasterModelGrid((10, 10), 1.0)
>>> z = mg.node_x * 0.1
>>> my_section = mg.node_vector_to_raster(z, flip_vertically=True)[:, 5]
>>> my_ycoords = mg.node_vector_to_raster(mg.node_y, flip_vertically=True)[:, 5]
>>> plot(my_ycoords, my_section)
>>> show()
Visualizing river profiles¶
See the ChannelProfiler
component.
Making Movies¶
Landlab does have an experimental movie making component. However, it has come to the
developers’ attention that the matplotlib functions it relies on in turn demand that
your machine already has installed one of a small set of highly temperamental open
source video codecs. It is quite likely using the component in its current form is
more trouble than it’s worth; however, the brave can take a look at the library
landlab.plot.video_out
. We intend to improve
video out in future Landlab releases.
For now, we advocate the approach of creating an animation by saving separately
individual plots from, e.g., plot() or
landlab.plot.imshow.imshow_node_grid
,
then stitching them together
into, e.g., a gif using external software. Note it’s possible to do this directly from
Preview on a Mac.