Note
This page was generated from a jupyter notebook.
Understanding and working with Landlab data fields¶
Data fields, or just fields for short, are the primary way that components share model data amongst themselves. This tutorial gives a short introduction to what fields are, what they do, and how to work with them.
Let’s start by importing the modules we’ll need for this tutorial, and instantiating a simple grid to work with for the first part of the tutorial:
[ ]:
import numpy as np
from landlab import FieldError, RasterModelGrid
mg = RasterModelGrid((3, 4))
A discussed in the grid tutorial, all data stored on the grid exists as “flat” one-dimensional arrays. This means that information can be retrieved from these grids using the ID of an grid element as the index:
[ ]:
# demonstrate that arrays of properties are n-elements long
(
mg.x_of_node.shape == (mg.number_of_nodes,)
and mg.length_of_link.shape == (mg.number_of_links,)
)
[ ]:
# what's the length of the link with ID 6 (the 7th link)?
mg.length_of_link[6]
Any values we defined across the grid are indexed in the same way, e.g., an array of elevations would be of shape (n-nodes, ).
A Landlab field, then, is simply an array like this explicitly linked to an element type, and stored within the grid object itself. Doing this serves four main purposes:
It means that if a component has access to the grid, it also has access to all the data defined on the grid.
It allows us to enforce the idea that an array of values of nodes is always n-nodes-long, an array on links is always n-links-long, etc.
It provides a standardized interface where the nomenclature used by a given component for input-output is both unambiguous and clear, in the spirit of the CSDMS standard names.
The field structure also allows us to bind the measurement unit to the field, if we so wish.
Note that Landlab components generally follow a “CSDMS-like” naming conventiomn, where the name looks like thing_that_is_described__quantity_described
, with a double underscore in the middle. In cases where the equivalent Standard Name would be excessively long, a shorter alternatively is usually used.
Making fields on the grid¶
There are several ways to create a field within the grid. These include functions to create fields filled just with ones or zeros, similar to the numpy functions np.ones
and np.zeros
, and functions to create fields from existing value arrays that you want to join to the grid.
The first term supplied is always the element on which the field is defined, i.e., ‘node’, ‘link’, ‘cell’, etc. The second is the name to give the field.
All these creation routines also return a reference to the field. This can be a useful shorthand to get at the grid without having to write out the full field name every time:
[ ]:
no_1a = mg.add_zeros("field__number_one", at="node")
no_1b = mg.add_ones(
"field__number_two", at="link", dtype=int
) # fns can also take dtype
no_1b[mg.active_links] = 0
print(no_1b)
All the field creation routines share two optional keywords: units
and clobber
. units
(default: ‘-’) allows a unit to be associated with a field if desired. clobber
(default: False
) prevents accidental overwriting of an existing field. If you want to overwrite, set it to False
.
Let’s try creating a field from an existing array here (grid.add_field()
). In this case, there’s an additional keyword copy
(default = False
) that controls whether the field refers to the actual first array, or whether a copy of the data is made:
[ ]:
input_array = np.arange(mg.number_of_nodes, dtype=float)
[ ]:
try:
no_1c = mg.add_field(
"field__number_one", input_array, at="node", copy=False, units="m"
)
except FieldError:
print("ERROR: This field name already exists!")
[ ]:
# ...let's try that again:
no_1c = mg.add_field(
"field__number_one", input_array, at="node", copy=False, units="m", clobber=True
)
print(no_1c)
[ ]:
# note that the keyword `copy=False` means that the field array *is* the input_array...
input_array[:] = -1.0
print(no_1c)
Accessing a data field, deleting a data field¶
We’ve already seen that the array creation routines return a reference to the field data. But sometimes, you want to access the field directly.
In practical terms, think of the names themselves as nested inside the grid as if the grid itself were a Python dictionary. The element type is the first key, and the field name is the second key.
(In detail, the type is actually a Landlab-specific object called a ScalarDataField, but it behaves essentially as an enhanced Python dictionary).
[ ]:
mg["node"]["field__number_one"]
You’ll also very commonly see some common “syntactic sugar” for this, where the element key is replaced by a grid property called grid.at_[element]
. i.e.,
[ ]:
mg.at_node["field__number_one"] is mg["node"]["field__number_one"]
Because these structures are dictionary-like, we can use the usual set of Python dictionary methods to interact with them too:
[ ]:
mg.at_node.keys() # see the existing fields at nodes
[ ]:
mg.at_node.clear() # delete all fields at nodes
[ ]:
mg.at_node.keys()
[ ]:
mg.at_link.keys()
[ ]:
mg.at_link.pop("field__number_two") # return the field, and remove it from the array
[ ]:
mg.at_link.keys()
The units are recorded in a further dict-like structure attached to at_[element]
:
[ ]:
z = mg.add_ones("field__number_3", at="node", units="km", clobber=True)
mg.at_node.units["field__number_3"]