{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# What happens when you create a grid object?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "For more Landlab tutorials, click here: https://landlab.readthedocs.io/en/latest/user_guide/tutorials.html\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Landlab supports a range of grid types. These include both rasters (with both square and rectangular cells), and a range of structured and unstructured grids based around the interlocking polygons and triangles of a Voronoi-Delaunay tesselation (radial, hexagonal, and irregular grids).\n", "\n", "Here, we look at some of the features of both of these types.\n", "\n", "We can create **grid** objects with the following lines of code." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from landlab import RasterModelGrid, VoronoiDelaunayGrid, HexModelGrid\n", "\n", "smg = RasterModelGrid(\n", " (3, 4), 1.) # a square-cell raster, 3 rows x 4 columns, unit spacing\n", "rmg = RasterModelGrid((3, 4), xy_spacing=(1., 2.)) # a rectangular-cell raster\n", "hmg = HexModelGrid(shape=(3, 4))\n", "# ^a hexagonal grid with 3 rows, 4 columns from the base row, & node spacing of 1.\n", "x = np.random.rand(100) * 100.\n", "y = np.random.rand(100) * 100.\n", "vmg = VoronoiDelaunayGrid(x, y)\n", "# ^a Voronoi-cell grid with 100 randomly positioned nodes within a 100.x100. square" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All these various `ModelGrid` objects contains various data items (known as *attributes*). These include, for example:\n", "* number nodes and links in the grid\n", "* *x* and *y* coordinates of each each node\n", "* starting (\"tail\") and ending (\"head\") node IDs of each link\n", "* IDs of links that are active\n", "* IDs of core nodes\n", "* etc.\n", "\n", "From here on we'll focus on the square raster grid as its geometry is a bit easier to think through, but all of the following applies to all grid types.\n", "\n", "## Understanding the topology of Landlab grids\n", "\n", "All grids consist of two interlocked sets of *points* joined by *lines* outlining *areas*. If we define data on the points we call **nodes**, then they are joined by **links**, which outline **patches**. Each node within the interior of the grid lies at the geometric center of the area of a **cell**. The cell's edges are **faces**, and the endpoints of the faces---which are also vertices of the cells---are **corners**.\n", "\n", "Note that this kind of scheme requires one set of features to be \"dominant\" over the other; i.e., either not every node has a cell, *or* not every link is crossed by a face. Both cannot be true, because one or other set of features has to define the edge of the grid. Landlab assumes that the node set is primary, so there are always more nodes than corners; more links than faces; and more patches than cells.\n", "\n", "Each of these sets of *\"elements\"* has its own set of IDs. These IDs are what allow us to index the various Landlab fields, which store spatial data. Each feature is ordered by **x, then y**. The origin is always at the bottom left node, unless you choose to move it (`grid.move_origin`)... except in the specific case of a radial grid, where logic and symmetry dictates it must be the central node.\n", "\n", "Whenever Landlab needs to order something rotationally (angles; elements around a different element type), it does so following the standard mathematical convention of **counterclockwise from east**. We'll see this in practical terms a bit later in this tutorial.\n", "\n", "The final thing to know is that **links and faces have directions**. This lets us record fluxes on the grid by associating them with, and mapping them onto, the links (or, much less commonly, the faces). All lines point into the **upper right half-space**. So, on our raster, this means the horizontal links point east and the vertical links point north.\n", "\n", "So, for reference, our raster grid looks like this:\n", "\n", "\n", " NODES: LINKS: PATCHES:\n", " 8 ----- 9 ---- 10 ---- 11 * -14-->* -15-->* -16-->* * ----- * ----- * ----- *\n", " | | | | ^ ^ ^ ^ | | | |\n", " | | | | 10 11 12 13 | 3 | 4 | 5 |\n", " | | | | | | | | | | | |\n", " 4 ----- 5 ----- 6 ----- 7 * --7-->* --8-->* --9-->* * ----- * ----- * ----- *\n", " | | | | ^ ^ ^ ^ | | | |\n", " | | | | 3 4 5 6 | 0 | 1 | 2 |\n", " | | | | | | | | | | | |\n", " 0 ----- 1 ----- 2 ----- 3 * --0-->* --1-->* --2-->* * ----- * ----- * ----- *\n", "\n", " CELLS: FACES: CORNERS:\n", " * ----- * ----- * ----- * * ----- * ----- * ----- * * ----- * ----- * ----- *\n", " | | | | | | | | | | | |\n", " | . ----- . ----- . | | . --5-->. --6-->. | | 3 ----- 4 ----- 5 |\n", " | | | | | | ^ ^ ^ | | | | | |\n", " * --| 0 | 1 |-- * * --2 3 4-- * * --| | |-- *\n", " | | | | | | | | | | | | | | |\n", " | . ----- . ----- . | | . --0-->. --1-->. | | 0 ----- 1 ----- 2 |\n", " | | | | | | | | | | | |\n", " * ----- * ----- * ----- * * ----- * ----- * ----- * * ----- * ----- * ----- *\n", "\n", "\n", "## Recording and indexing the values at elements\n", "\n", "Landlab lets you record values at any element you want. In practice, the most useful places to store data is on the primary elements of nodes, links, and patches, with the nodes being most useful for scalar values (e.g, elevations) and the links for fluxes with direction to them (e.g., velocity or discharge).\n", "\n", "In order to maintain compatibility across data types, *all* landlab data are stored in *number-of-elements*-long arrays. This includes both user-defined data and the properties of the nodes within the grid. This means that these arrays can be immediately indexed by their element ID. For example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# what are the y-coordinates of the pair of nodes in the middle of our 3-by-4 grid?\n", "# the IDs of these nodes are 5 and 6, so:\n", "smg.y_of_node[[5, 6]]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you're working with a raster, you can always reshape the value arrays back into two dimensions so you can take Numpy-style slices through it:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# what are the x-coordinates of nodes in the middle row?\n", "smg.x_of_node.reshape(smg.shape)[1, :]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This same data storage pattern is what underlies the Landlab **data fields**, which are simply one dimensional, number-of-elements-long arrays that store user defined spatial data across the grid, attached to the grid itself." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.add_zeros('elevation', at='node', clobber=True)\n", "# ^Creates a new field of zero data associated with nodes\n", "smg.at_node['elevation'] # Note the use of dictionary syntax" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Or, equivalently, at links:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.add_ones('slope', at='link', clobber=True)\n", "# ^Creates a new array of data associated with links\n", "smg.at_link['slope']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Landlab **components** use fields to share spatial information among themselves. See the *fields* and *components* tutorials for more information.\n", "\n", "\n", "## Getting this information from the grid object\n", "\n", "All of this topological information is recorded within our grid objects, and can be used to work with data arrays that are defined over the grid. The grid records the numbers of each element, their positions, and their relationships with one another. Let's take a look at some of this information for the raster:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.number_of_nodes" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.number_of_links" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The grid contains its geometric information too. Let's look at the *(x,y)* coordinates of the nodes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for i in range(smg.number_of_nodes):\n", " print(i, smg.x_of_node[i], smg.y_of_node[i])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Link connectivity and direction is described by specifying the starting (\"tail\") and ending (\"head\") node IDs for each link (to remember this, think of an arrow: TAIL ===> HEAD)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for i in range(smg.number_of_links):\n", " print('Link', i, ': node', smg.node_at_link_tail[i], '===> node',\n", " smg.node_at_link_head[i])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Boundary conditions are likewise defined on these elements (see also the full boundary conditions tutorial). Landlab is clever enough to ensure that the boundary conditions recorded on, say, the links get updated when you redefine the conditions on, say, the nodes.\n", "\n", "Nodes can be *core*, *fixed value*, *fixed gradient*, or *closed* (flux into or out of node is forbidden). Links can be *active* (can carry flux), *fixed* (always carries the same flux; joined to a fixed gradient node) or *inactive* (forbidden from carrying flux). \n", "\n", "Note that this boundary coding does not mean that a particular boundary condition is automatically enforced. It's up to the user to take advantage of these codes. For example, if you are writing a model that calculates flow velocity on links but wish the velocity to be zero at inactive links, you the programmer must ensure this, for instance by including a line like `my_velocity[grid.inactive_links] = 0.0`, or alternatively `my_velocity[grid.active_links] = ......`.\n", "\n", "Information on boundary coding is available from the grid:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.core_nodes" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.active_links" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# let's demonstrate the auto-updating of boundary conditions:\n", "smg.status_at_node[smg.nodes_at_bottom_edge] = smg.BC_NODE_IS_CLOSED\n", "smg.active_links # the links connected to the bottom edge nodes are now inactive" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Element connectivity\n", "\n", "Importantly, we can also find out which elements are connected to which other elements. This allows us to do computationally vital operations involving mapping values defined at one element onto another, e.g., the net flux at a node; the mean slope at a patch; the node value at a cell.\n", "\n", "In cases where these relationships are one-to-many (e.g., `links_at_node`, `nodes_at_patch`), the shape of the resulting arrays is always (number_of_elements, max-number-of-connected-elements-across-grid). For example, on a raster, `links_at_node` is (nnodes, 4), because the cells are always square. On an irregular Voronoi-cell grid, `links_at_node` will be (nnodes, X) where X is the number of sides of the side-iest cell, and `nodes_at_patch` will be (npatches, 3) because all the patches are Delaunay triangles. And so on.\n", "\n", "Lets take a look. Remember, Landlab orders things **counterclockwise from east**, so for a raster the order will the EAST, NORTH, WEST, SOUTH." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.links_at_node[5]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.links_at_node.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Undefined directions get recorded as `-1`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.links_at_node[8]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.patches_at_node" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.nodes_at_patch" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Where element-to-element mapping is one-to-one, you get simple, one dimensional arrays:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.node_at_cell # shape is (n_cells, )" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.cell_at_node # shape is (n_nodes, ) with -1s as needed" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A bit of thought reveals that things get more complicated for links and faces, because they have direction. You'll need a convenient way to record whether a given flux (which is positive if it goes with the link's inherent direction, and negative if against) actually is travelling into or out of a given node. The grid provides `link_dirs_at_node` and `active_link_dirs_at_node` to help with this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.link_dirs_at_node # all links; positive points INTO the node; zero where no link" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# prove there are zeros where links are missing:\n", "np.all((smg.link_dirs_at_node == 0) == (smg.links_at_node == -1))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smg.active_link_dirs_at_node # in this one, inactive links get zero too" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Multiply the fluxes indexed by `links_at_node` and sum by axis=1 to have a very convenient way to calculate flux divergences at nodes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fluxes_at_node = smg.at_link['slope'][smg.links_at_node]\n", "# ^...remember we defined the slope field as ones, above\n", "fluxes_into_node = fluxes_at_node * smg.active_link_dirs_at_node\n", "flux_div_at_node = fluxes_into_node.sum(axis=1)\n", "print(flux_div_at_node[smg.core_nodes])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Why? Remember that earlier in this tutorial we already set the bottom edge to `BC_NODE_IS_CLOSED`. So each of our core nodes has a flux of +1.0 coming in from the left, but two fluxes of -1.0 leaving from both the top and the right. Hence, the flux divergence is -1. at each node.\n", "\n", "Note as well that Landlab offers the one-line grid method `calc_flux_div_at_node()` to perform this same operation. For more on this, see the **gradient_and_divergence** tutorial." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Click here for more Landlab tutorials" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 1 }