Triangles in hexagons

Aligning different types of grids

Introduction

In this example a triangle and a hexagon grid are defined in such a way that they perfectly line up. A hexagon can be neatly divided into six triangles.

Tip

The process in this example has been greatly simplified with the introduction of HexGrid.subdivide() in v0.12.0. This example shows the more cumbersome approach, but I will leave this example up for now for educational purposes, for it shows how to manipulate cell size and offsets to position the grid the way you want.

For the two grids to align, two things will need to match:
  • the length of the edge connecting two vertices needs to be the same

  • they need an origin point that lines them up properly

When initializing a grid in GridKit, the size specified by the user reflects the distance between the centroids of two neighboring cells (dx). For the hexagonal grid this will be a different value than for the triangle grid. The triangle grid is easiest. The side length is 2*dx. Let’s say we want side lengths of 1, we specify a size for the triangle grid of 0.5. For the hexagonal grid this is less intuitive, it works out to be three over the square root of three: 3 / sqrt(3). At least this is the case if we use a ‘flat’ orientation for our hexagonal grid, which we will need to do in order for the grids to overlap. If this is not obvious, I encourage you to do this exercise with a ‘pointy’ hexagonal grid. It will be obvious from the plot.

Let’s first plot some cells of these two grids and compare them. From there we can figure out how to shift the grids in order to align them.

import matplotlib.pyplot as plt

from gridkit import HexGrid, TriGrid
from gridkit.doc_utils import plot_polygons

tri_grid = TriGrid(size=0.5)
hex_grid = HexGrid(size=3 / 3**0.5, shape="flat")

bounds = (-7.5, -5, 6, 5)

ax = plt.subplot()
tri_bounds = tri_grid.align_bounds(bounds, mode="nearest")
tri_ids = tri_grid.cells_in_bounds(tri_bounds)
tri_geoms = tri_grid.to_shapely(tri_ids)
plot_polygons(tri_geoms, filled=False, colors="pink", linewidth=2, ax=ax)
hex_bounds = hex_grid.align_bounds(bounds, mode="nearest")
hex_ids = hex_grid.cells_in_bounds(hex_bounds)
hex_geoms = hex_grid.to_shapely(hex_ids)
plot_polygons(hex_geoms, filled=False, colors="cyan", linewidth=2, linestyle=":", ax=ax)
# Create lattice with spacing of dx and dy to highlight cell bounds
ax.set_xticks([i * hex_grid.dx for i in range(-5, 5)])
ax.set_yticks([i * hex_grid.dy for i in range(-5, 5)])
ax.grid()
plt.scatter(0, 0, s=10, zorder=10)
plt.show()
triangles in hexes
/home/runner/work/GridKit/GridKit/gridkit/hex_grid.py:100: UserWarning: A 'flat' ``shape`` will be deprecated in version v1.0.0. It is advised to use ``rotation=30`` instead.
  warnings.warn(
/home/runner/work/GridKit/GridKit/gridkit/doc_utils.py:96: UserWarning: The argument 'filled' for doc_utils has been deprecated in favor of 'fill' and will be removed in a future version.
  warnings.warn(

Hooray! The grids are already aligned. It looks like the default origin already works well for this case, but let’s say we want to position the hexagon around zero. If the grids don’t match I often find it easier to figure out how to shift each grid to origin rather than figure out the relative offset between the grids. From visually inspecting the plot I can tell we’d have to shift the hexagon to the right by half a dx. The triangles though will then need to cross at the origin, so we’d need to either shift them half a side length to the left or 3/4rth of a side length to the right. This works out to half a dx to the left or one and a half dx to the right. Let’s do the latter

Warning

When dealing with a ‘flat’ hex grid, the x and y of offset need to be set in reverse order: (y,x). This is a known conceptual inconsistency but will not be fixed since ‘flat’ HexGrids will be removed in a future version in favor of rotation. See: release notes: v0.11.0

Tip

For simpler centering of grids around origin, see BaseGrid.anchor()

tri_grid.offset = (1.5 * tri_grid.dx, 0)
hex_grid.offset = (0, hex_grid.dx / 2)

ax = plt.subplot()
tri_geoms = tri_grid.to_shapely(tri_ids)
plot_polygons(tri_geoms, filled=False, colors="pink", linewidth=2)
hex_geoms = hex_grid.to_shapely(hex_ids)
plot_polygons(hex_geoms, filled=False, colors="cyan", linewidth=2, linestyle=":")
# Create lattice with spacing of dx and dy to highlight cell bounds
ax.set_xticks([i * hex_grid.dx + hex_grid.dx / 2 for i in range(-5, 5)])
ax.set_yticks([i * hex_grid.dy for i in range(-5, 5)])
ax.grid()
plt.scatter(0, 0, s=10, zorder=10)
plt.show()
triangles in hexes

Et voila. Our grids are centered around the origin. Now we can play with the interaction between these grids. We can for example figure out what triangles belong to what hexagon. The simplest way to do this is to get the centroids of the triangles and call cell_at_point on the hexagon grid using these centroids. We can even give every triangle in the top-left of a hexagon the same color for example. Because of the way we ordered the cell-ids, we know that the first triangle will be in the top left of the hexagon. If you obtained the cell id’s in a different manner, you can sort them to make sure this is the case. You can use numpy for this, like numpy.sort(tri_ids.ravel()) for example.

First let’s find all the hex_ids. We can then assign each hex id a different value. Then we plot the triangles using these values and expect the hexagon grid to be reflected in the colors

tri_centroids = tri_grid.centroid(tri_ids)
hex_ids = hex_grid.cell_at_point(tri_centroids).ravel()
value_map = {tuple(cell): i for (i, cell) in enumerate(hex_ids.unique().index)}
color_values = [value_map[tuple(cell)] for cell in hex_ids.index]
plot_polygons(tri_geoms, filled=True, cmap="viridis", colors=color_values, linewidth=2)
plot_polygons(tri_geoms, filled=False, colors="pink", linewidth=0.3, linestyle=":")
plt.show()
triangles in hexes

As mentioned above, we can use the order of the data to color the cells based on whether they are the first, second, third etc. occurrence for the given hexagon. For good measure let’s also make the color dependent on the y-value of the hexagon, for good measure. We can create a dictionary based on the tuple representation of the index to keep track of the number of triangles in the grid. Then we create a colors list that will have a number for each triangle based on whether it was the first, second, etc. triangle in that hexagon.

id_counter = dict()
color_values = []
for id in hex_ids.ravel().index:
    id = tuple(id)
    if id in id_counter:  # increment value for hexagon if present
        id_counter[id] += 1
    else:  # add id to dictionary if not present yet
        id_counter[id] = 1
    color_values.append(id_counter[id] + 2 * id[1])
plot_polygons(tri_geoms, filled=True, cmap="viridis", colors=color_values, linewidth=2)
plt.show()
triangles in hexes

Ain’t that pretty?

This kind of trick can be used for resampling. Each cell is then divided into smaller cells. A nearest neighbour resampling is then used on the smaller cells, after which the mean or majority can be used to determine the value of the larger cell. Maybe I will implement that as a default method some day. It’s fun but also arguably a good way to go about resampling in many cases.

This is called remapping in the following publication:

Accadia, C., S. Mariani, M. Casaioli, A. Lavagnini, and A. Speranza, 2003: Sensitivity of Precipitation Forecast Skill Scores to Bilinear Interpolation and a Simple Nearest-Neighbor Average Method on High-Resolution Verification Grids. Wea. Forecasting, 18, 918–932, https://doi.org/10.1175/1520-0434(2003)018<0918:SOPFSS>2.0.CO;2 .

Total running time of the script: (0 minutes 0.469 seconds)

Gallery generated by Sphinx-Gallery