#! /usr/env/python
"""Python implementation of HexModelGrid, a grid class used to create and
manage structured Voronoi-Delaunay grids for 2D numerical models.
"""
import numpy
import xarray as xr
from ..core.utils import as_id_array
from ..graph import DualHexGraph
from .base import ModelGrid
[docs]
class HexModelGrid(DualHexGraph, ModelGrid):
"""A grid of hexagonal cells.
This inherited class implements a regular 2D grid with hexagonal cells and
triangular patches. It is a special type of VoronoiDelaunay grid in which
the initial set of points is arranged in a triangular/hexagonal lattice.
Examples
--------
Create a hex grid with 2 rows of nodes. The first and third rows will
have 2 nodes, and the second nodes.
>>> from landlab import HexModelGrid
>>> grid = HexModelGrid((3, 2), spacing=1.0)
>>> grid.number_of_nodes
7
>>> grid = HexModelGrid((3, 3), node_layout="rect", spacing=2.0)
>>> grid.status_at_node
array([1, 1, 1, 1, 0, 1, 1, 1, 1], dtype=uint8)
>>> grid = HexModelGrid((3, 3), node_layout="rect", orientation="vertical")
>>> grid.status_at_node
array([1, 1, 1, 1, 1, 0, 1, 1, 1], dtype=uint8)
>>> grid = HexModelGrid((4, 4), node_layout="rect", orientation="vertical")
>>> grid.status_at_node
array([1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1], dtype=uint8)
>>> grid.boundary_nodes
array([ 0, 1, 2, 3, 4, 7, 8, 11, 12, 13, 14, 15])
>>> grid = HexModelGrid((3, 4), node_layout="rect")
>>> grid.status_at_node
array([1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1], dtype=uint8)
"""
[docs]
def __init__(
self,
shape,
spacing=1.0,
xy_of_lower_left=(0.0, 0.0),
orientation="horizontal",
node_layout="hex",
reorient_links=True,
xy_of_reference=(0.0, 0.0),
xy_axis_name=("x", "y"),
xy_axis_units="-",
):
"""Create a grid of hexagonal cells.
Create a regular 2D grid with hexagonal cells and triangular patches.
It is a special type of :class:`~.VoronoiModelGrid` in which the initial set
of points is arranged in a triangular/hexagonal lattice.
Parameters
----------
shape : tuple of int
Number of rows and columns of nodes.
spacing : float, optional
Node spacing.
xy_of_lower_left : tuple of float, optional
Minimum x-of-node and y-of-node values. Depending on the grid
no node may be present at this coordinate. Default is (0., 0.).
xy_of_reference : tuple of float, optional
Coordinate value in projected space of the reference point,
`xy_of_lower_left`. Default is (0., 0.)
orientation : str, optional
One of the 3 cardinal directions in the grid, either 'horizontal'
(default) or 'vertical'
node_layout : {"hex", "rect"}
The grid layout of nodes.
reorient_links : bool, optional
Whether or not to re-orient all links to point between -45 deg
and +135 deg clockwise from "north" (i.e., along y axis). default
is True.
Returns
-------
HexModelGrid
A newly-created grid.
Examples
--------
Create a hex grid with 2 rows of nodes. The first and third rows will
have 2 nodes, and the second nodes.
>>> from landlab import HexModelGrid
>>> hmg = HexModelGrid((3, 2), spacing=1.0)
>>> hmg.number_of_nodes
7
"""
self._xy_of_lower_left = tuple(numpy.asarray(xy_of_lower_left, dtype=float))
DualHexGraph.__init__(
self,
shape,
spacing=spacing,
xy_of_lower_left=self.xy_of_lower_left,
orientation=orientation,
node_layout=node_layout,
sort=True,
)
ModelGrid.__init__(
self,
xy_axis_name=xy_axis_name,
xy_axis_units=xy_axis_units,
xy_of_reference=xy_of_reference,
)
self._node_status = numpy.full(
self.number_of_nodes, self.BC_NODE_IS_CORE, dtype=numpy.uint8
)
self._node_status[self.perimeter_nodes] = self.BC_NODE_IS_FIXED_VALUE
[docs]
@classmethod
def from_dict(cls, kwds):
args = (kwds.pop("shape"),)
return cls(*args, **kwds)
[docs]
@classmethod
def from_dataset(cls, dataset):
return cls(
tuple(dataset["shape"].values),
spacing=dataset["spacing"],
xy_of_lower_left=dataset["xy_of_lower_left"],
orientation=dataset.attrs["orientation"],
node_layout=dataset.attrs["node_layout"],
)
[docs]
def as_dataset(self, include="*", exclude=None, time=None):
dataset = xr.Dataset(
{
"shape": (("dim",), list(self.shape)),
"spacing": self.spacing,
"xy_of_lower_left": (("dim",), list(self.xy_of_lower_left)),
},
attrs={
"grid_type": "triangular",
"node_layout": self.node_layout,
"orientation": self.orientation,
},
)
return dataset.update(
super().as_dataset(include=include, exclude=exclude, time=None)
)
@property
def xy_of_lower_left(self):
"""Return (x, y) of the reference point."""
return self._xy_of_lower_left
@xy_of_lower_left.setter
def xy_of_lower_left(self, xy_of_lower_left):
"""Set a new value for the xy_of_lower_left."""
dx = self.xy_of_lower_left[0] - xy_of_lower_left[0]
dy = self.xy_of_lower_left[1] - xy_of_lower_left[1]
# self._xy_of_node -= (dx, dy)
with self.thawed():
self.x_of_node[:] -= dx
self.y_of_node[:] -= dy
self._xy_of_lower_left = tuple(xy_of_lower_left)
@property
def number_of_node_columns(self):
"""Number of node columns hex grid.
Number of node columns in a rectangular-shaped and/or
vertically oriented hex grid.
Returns the number of columns, including boundaries.
Notes
-----
Will generate an error if called with a hex-shaped, horizontally
aligned grid.
Examples
--------
>>> from landlab import HexModelGrid
>>> grid = HexModelGrid((5, 5), node_layout="rect")
>>> grid.number_of_node_columns
5
:meta landlab: info-grid, info-node
"""
return self.shape[1]
@property
def number_of_node_rows(self):
"""Number of node rows in a rectangular-shaped and/or horizontally
oriented hex grid.
Returns the number of rows, including boundaries.
Notes
-----
Will generate an error if called with a hex-shaped, vertically
aligned grid.
Examples
--------
>>> from landlab import HexModelGrid
>>> grid = HexModelGrid((5, 5), node_layout="rect")
>>> grid.number_of_node_rows
5
:meta landlab: info-grid, info-node
"""
return self._shape[0]
[docs]
def node_row_and_column(self, node_id):
"""Row and column from node ID, FOR VERT RECT CONFIGURATION ONLY.
Examples
--------
>>> from landlab import HexModelGrid
>>> grid = HexModelGrid((3, 4), node_layout="rect", orientation="vertical")
>>> grid.node_row_and_column(5)
(1, 2)
>>> grid = HexModelGrid((3, 5), node_layout="rect", orientation="vertical")
>>> grid.node_row_and_column(13)
(2, 1)
"""
assert self.orientation[0] == "v", "grid orientation must be vertical"
try:
(nr, nc) = self._shape
except AttributeError as exc:
raise AttributeError(
"Only rectangular Hex grids have defined rows and columns."
) from exc
row = node_id // nc
n_mod_nc = node_id % nc
half_nc = (nc + 1) // 2
col = 2 * (n_mod_nc % half_nc) + n_mod_nc // half_nc
return (row, col)
def _configure_hexplot(self, data, data_label=None, color_map=None):
"""Sets up necessary information for making plots of the hexagonal grid
colored by a given data element.
Parameters
----------
data : str or (n_link,) ndarray
Data field to be colored
data_label : str, optional
Label for colorbar
color_map : matplotlib colormap object, optional
Color map to apply (defaults to "jet")
Notes
-----
Creates and stores a PatchCollection representing the hexagons. Also
stores a handle to the current plotting axis. Both of these are then
used by hexplot().
"""
import matplotlib
from matplotlib.collections import PatchCollection
from matplotlib.patches import Polygon
from numpy import array
from numpy import sqrt
from numpy import zeros
# color
if color_map is None:
color_map = matplotlib.cm.jet
# geometry
apothem = self.spacing / 2.0
# distance from node to each hexagon cell vertex
radius = 2.0 * apothem / sqrt(3.0)
# offsets from node x,y position
offsets = zeros((6, 2))
poly_verts = zeros((6, 2))
# Figure out whether the orientation is horizontal or vertical
if self.orientation[0] == "h": # horizontal
offsets[:, 0] = array([0.0, apothem, apothem, 0.0, -apothem, -apothem])
offsets[:, 1] = array(
[
radius,
radius / 2.0,
-radius / 2.0,
-radius,
-radius / 2.0,
radius / 2.0,
]
)
else: # vertical
offsets[:, 0] = array(
[
radius / 2.0,
radius,
radius / 2.0,
-radius / 2.0,
-radius,
-radius / 2.0,
]
)
offsets[:, 1] = array([apothem, 0.0, -apothem, -apothem, 0.0, apothem])
patches = []
for i in range(self.number_of_nodes):
poly_verts[:, 0] = self.node_x[i] + offsets[:, 0]
poly_verts[:, 1] = self.node_y[i] + offsets[:, 1]
p = Polygon(poly_verts, True)
patches.append(p)
self._hexplot_pc = PatchCollection(
patches, cmap=color_map, edgecolor="none", linewidth=0.0
)
self._hexplot_configured = True
[docs]
def hexplot(self, data, data_label=None, color_map=None):
"""Create a plot of the grid elements.
Creates a plot of the grid and one node-data field, showing hexagonal
cells colored by values in the field.
Parameters
----------
data : str or (n_nodes,) ndarray
Data field to be colored.
data_label : str, optional
Label for colorbar.
color_map : matplotlib colormap object, None
Color map to apply (defaults to "jet")
See also
--------
:func:`~.plot.imshow_grid`
Another Landlab function capable of producing hexplots, with a
fuller-featured set of options.
:meta landlab: info-grid
"""
import copy
import matplotlib.pyplot as plt
from numpy import amax
from numpy import amin
from numpy import array
try:
self._hexplot_configured
except AttributeError:
self._configure_hexplot(data, data_label, color_map)
else:
if self._hexplot_pc.cmap != color_map:
self._configure_hexplot(data, data_label, color_map)
# Handle *data*: if it's a numpy array, then we consider it the
# data to be plotted. If it's a string, we consider it the name of the
# node-field to plot, and we fetch it.
if type(data) is str:
data_label = data
data = self.at_node[data]
ax = plt.gca()
self._hexplot_pc.set_array(array(data))
copy_of_pc = copy.copy(self._hexplot_pc)
ax.add_collection(copy_of_pc)
plt.xlim([amin(self.node_x) - self.spacing, amax(self.node_x) + self.spacing])
plt.ylim([amin(self.node_y) - self.spacing, amax(self.node_y) + self.spacing])
return ax
[docs]
def set_watershed_boundary_condition_outlet_id(
self, outlet_id, node_data, nodata_value=-9999.0
):
"""Set the boundary conditions for a watershed.
All nodes with ``nodata_value`` are set to :attr:`~.NodeStatus.CLOSED`.
All nodes with data values are set to :attr:`~.NodeStatus.CORE`, with the
exception that the outlet node is set to a :attr:`~.NodeStatus.FIXED_VALUE`.
Note that the outer ring of the HexModelGrid is set to
:attr:`~.NodeStatus.CLOSED`, even if there are nodes that have values.
The only exception to this would be if the outlet node is on the boundary,
which is acceptable.
Assumes that the id of the outlet is already known.
This assumes that the grid has a single watershed. If this is not
the case this will not work.
Parameters
----------
outlet_id : int
id of the outlet node
node_data : str or (n_nodes,) ndarray
At-node field name or at-node data values to use for identifying
watershed location.
nodata_value : float, optional
Value that indicates an invalid value.
Examples
--------
The example will use a *HexModelGrid* with node data values
as illustrated::
1. , 2. , 3. , 4. ,
0.5, 1.5, 2.5, 3.5, 4.5,
0. , 1. , 2. , 3. , 4. , 5.,
0.5, 1.5, 2.5, 3.5, 4.5,
1. , 2. , 3. , 4.
>>> from landlab import HexModelGrid
>>> hmg = HexModelGrid((5, 4))
>>> z = hmg.add_zeros("topographic__elevation", at="node")
>>> z += hmg.x_of_node + 1.0
>>> hmg.status_at_node
array([1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1,
1], dtype=uint8)
>>> outlet = hmg.set_watershed_boundary_condition_outlet_id(9, z, -9999.0)
>>> hmg.status_at_node
array([4, 4, 4, 4, 4, 0, 0, 0, 4, 1, 0, 0, 0, 0, 4, 4, 0, 0, 0, 4, 4, 4, 4,
4], dtype=uint8)
:meta landlab: boundary-condition
"""
# get node_data if a field name
node_data = self.return_array_or_field_values(node_data, at="node")
# make ring of no data nodes
self.status_at_node[self.boundary_nodes] = self.BC_NODE_IS_CLOSED
# set no data nodes to inactive boundaries
self.set_nodata_nodes_to_closed(node_data, nodata_value)
# set the boundary condition (fixed value) at the outlet_node
self.status_at_node[outlet_id] = self.BC_NODE_IS_FIXED_VALUE
[docs]
def set_watershed_boundary_condition(
self, node_data, nodata_value=-9999.0, return_outlet_id=False
):
"""Find the node adjacent to a boundary node with the smallest value.
This node is set as the outlet. The outlet node must have a data
value. Can return the outlet id as a one element numpy array if
``return_outlet_id`` is set to `True`.
All nodes with ``nodata_value`` are set to :attr:`~.NodeStatus.CLOSED`
(grid.status_at_node == 4). All nodes with data values are set to
:attr:`~.NodeStatus.CORE` (grid.status_at_node == 0), with the exception
that the outlet node is set to a :attr:`~.NodeStatus.FIXED_VALUE`
(grid.status_at_node == 1).
Note that the outer ring (perimeter) of the grid is set to
:attr:`~.NodeStatus.CLOSED`, even if there are nodes that have values. The only
exception to this would be if the outlet node is on the perimeter, which
is acceptable.
This routine assumes that all of the ``nodata_value`` are on the outside of
the data values. In other words, there are no islands of ``nodata_value``
surrounded by nodes with data.
This also assumes that the grid has a single watershed (that is a single
outlet node).
Parameters
----------
node_data : str or (n_nodes,) ndarray
At-node field name or at-node data values to use for identifying
watershed location.
nodata_value : float, optional
Value that indicates an invalid value.
return_outlet_id : bool, optional
Indicates whether or not to return the id of the found outlet
Examples
--------
The example will use a :class:`~.HexModelGrid` with node data values
as illustrated::
1. , 2. , 3. , 4. ,
0.5, 1.5, 2.5, 3.5, 4.5,
0. , 1. , 2. , 3. , 4. , 5.,
0.5, 1.5, 2.5, 3.5, 4.5,
1. , 2. , 3. , 4.
>>> from landlab import HexModelGrid
>>> hmg = HexModelGrid((5, 4))
>>> z = hmg.add_zeros("topographic__elevation", at="node")
>>> z += hmg.x_of_node + 1.0
>>> out_id = hmg.set_watershed_boundary_condition(z, -9999.0, True)
>>> out_id
array([9])
>>> hmg.status_at_node
array([4, 4, 4, 4, 4, 0, 0, 0, 4, 1, 0, 0, 0, 0, 4, 4, 0, 0, 0, 4, 4, 4, 4,
4], dtype=uint8)
:meta landlab: boundary-condition
"""
# get node_data if a field name
node_data = self.return_array_or_field_values(node_data, at="node")
# make ring of no data nodes
self.status_at_node[self.boundary_nodes] = self.BC_NODE_IS_CLOSED
# set no data nodes to inactive boundaries
self.set_nodata_nodes_to_closed(node_data, nodata_value)
# locs is a list that contains locations where
# node data is not equal to the nodata value
locs = numpy.where(node_data != nodata_value)
if len(locs) < 1:
raise ValueError("All data values are no_data values")
# now find minimum of the data values
min_val = numpy.min(node_data[locs])
# now find where minimum values are
min_locs = numpy.where(node_data == min_val)[0]
# check all the locations with the minimum value to see if one
not_found = True
while not_found:
# now check the min locations to see if any are next to
# a boundary node
local_not_found = True
# next_to_boundary = []
# check all nodes rather than selecting the first node that meets
# the criteria
# for i in range(len(min_locs)):
# next_to_boundary.append(self.node_has_boundary_neighbor()[min_locs[i])]
next_to_boundary = self.node_has_boundary_neighbor()[(min_locs,)]
# if any of those nodes were adjacent to the boundary, check
# that there is only one. If only one, set as outlet loc, else,
# raise a value error
if numpy.any(next_to_boundary):
local_not_found = False
if sum(next_to_boundary) > 1:
potential_locs = min_locs[
numpy.where(numpy.asarray(next_to_boundary))[0]
]
raise ValueError(
"Grid has two potential outlet nodes."
"They have the following node IDs: \n"
+ str(potential_locs)
+ "\nUse the method set_watershed_boundary_condition_outlet_id "
"to explicitly select one of these "
"IDs as the outlet node."
)
else:
outlet_loc = min_locs[numpy.where(next_to_boundary)[0][0]]
# checked all of the min vals, (so done with inner while)
# and none of the min values were outlet candidates
if local_not_found:
# need to find the next largest minimum value
# first find the locations of all values greater
# than the old minimum
# not done with outer while
locs = numpy.where((node_data > min_val) & (node_data != nodata_value))
# now find new minimum of these values
min_val = numpy.min(node_data[locs])
min_locs = numpy.where(node_data == min_val)[0]
else:
# if locally found, it is also globally found
# so done with outer while
not_found = False
# set outlet boundary condition
self.status_at_node[outlet_loc] = self.BC_NODE_IS_FIXED_VALUE
if return_outlet_id:
return as_id_array(numpy.array([outlet_loc]))